##// END OF EJS Templates
procutil: unroll uin/uout loop in protectstdio()...
Yuya Nishihara -
r37236:ac71cbad default
parent child Browse files
Show More
@@ -1,358 +1,359
1 # procutil.py - utility for managing processes and executable environment
1 # procutil.py - utility for managing processes and executable environment
2 #
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
6 #
7 # This software may be used and distributed according to the terms of the
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
8 # GNU General Public License version 2 or any later version.
9
9
10 from __future__ import absolute_import
10 from __future__ import absolute_import
11
11
12 import contextlib
12 import contextlib
13 import imp
13 import imp
14 import io
14 import io
15 import os
15 import os
16 import signal
16 import signal
17 import subprocess
17 import subprocess
18 import sys
18 import sys
19 import tempfile
19 import tempfile
20 import time
20 import time
21
21
22 from ..i18n import _
22 from ..i18n import _
23
23
24 from .. import (
24 from .. import (
25 encoding,
25 encoding,
26 error,
26 error,
27 policy,
27 policy,
28 pycompat,
28 pycompat,
29 )
29 )
30
30
31 osutil = policy.importmod(r'osutil')
31 osutil = policy.importmod(r'osutil')
32
32
33 stderr = pycompat.stderr
33 stderr = pycompat.stderr
34 stdin = pycompat.stdin
34 stdin = pycompat.stdin
35 stdout = pycompat.stdout
35 stdout = pycompat.stdout
36
36
37 def isatty(fp):
37 def isatty(fp):
38 try:
38 try:
39 return fp.isatty()
39 return fp.isatty()
40 except AttributeError:
40 except AttributeError:
41 return False
41 return False
42
42
43 # glibc determines buffering on first write to stdout - if we replace a TTY
43 # glibc determines buffering on first write to stdout - if we replace a TTY
44 # destined stdout with a pipe destined stdout (e.g. pager), we want line
44 # destined stdout with a pipe destined stdout (e.g. pager), we want line
45 # buffering
45 # buffering
46 if isatty(stdout):
46 if isatty(stdout):
47 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
47 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
48
48
49 if pycompat.iswindows:
49 if pycompat.iswindows:
50 from .. import windows as platform
50 from .. import windows as platform
51 stdout = platform.winstdout(stdout)
51 stdout = platform.winstdout(stdout)
52 else:
52 else:
53 from .. import posix as platform
53 from .. import posix as platform
54
54
55 explainexit = platform.explainexit
55 explainexit = platform.explainexit
56 findexe = platform.findexe
56 findexe = platform.findexe
57 _gethgcmd = platform.gethgcmd
57 _gethgcmd = platform.gethgcmd
58 getuser = platform.getuser
58 getuser = platform.getuser
59 getpid = os.getpid
59 getpid = os.getpid
60 hidewindow = platform.hidewindow
60 hidewindow = platform.hidewindow
61 popen = platform.popen
61 popen = platform.popen
62 quotecommand = platform.quotecommand
62 quotecommand = platform.quotecommand
63 readpipe = platform.readpipe
63 readpipe = platform.readpipe
64 setbinary = platform.setbinary
64 setbinary = platform.setbinary
65 setsignalhandler = platform.setsignalhandler
65 setsignalhandler = platform.setsignalhandler
66 shellquote = platform.shellquote
66 shellquote = platform.shellquote
67 shellsplit = platform.shellsplit
67 shellsplit = platform.shellsplit
68 spawndetached = platform.spawndetached
68 spawndetached = platform.spawndetached
69 sshargs = platform.sshargs
69 sshargs = platform.sshargs
70 testpid = platform.testpid
70 testpid = platform.testpid
71
71
72 try:
72 try:
73 setprocname = osutil.setprocname
73 setprocname = osutil.setprocname
74 except AttributeError:
74 except AttributeError:
75 pass
75 pass
76 try:
76 try:
77 unblocksignal = osutil.unblocksignal
77 unblocksignal = osutil.unblocksignal
78 except AttributeError:
78 except AttributeError:
79 pass
79 pass
80
80
81 closefds = pycompat.isposix
81 closefds = pycompat.isposix
82
82
83 def popen2(cmd, env=None, newlines=False):
83 def popen2(cmd, env=None, newlines=False):
84 # Setting bufsize to -1 lets the system decide the buffer size.
84 # Setting bufsize to -1 lets the system decide the buffer size.
85 # The default for bufsize is 0, meaning unbuffered. This leads to
85 # The default for bufsize is 0, meaning unbuffered. This leads to
86 # poor performance on Mac OS X: http://bugs.python.org/issue4194
86 # poor performance on Mac OS X: http://bugs.python.org/issue4194
87 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
87 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
88 close_fds=closefds,
88 close_fds=closefds,
89 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
89 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
90 universal_newlines=newlines,
90 universal_newlines=newlines,
91 env=env)
91 env=env)
92 return p.stdin, p.stdout
92 return p.stdin, p.stdout
93
93
94 def popen3(cmd, env=None, newlines=False):
94 def popen3(cmd, env=None, newlines=False):
95 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
95 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
96 return stdin, stdout, stderr
96 return stdin, stdout, stderr
97
97
98 def popen4(cmd, env=None, newlines=False, bufsize=-1):
98 def popen4(cmd, env=None, newlines=False, bufsize=-1):
99 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
99 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
100 close_fds=closefds,
100 close_fds=closefds,
101 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
101 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
102 stderr=subprocess.PIPE,
102 stderr=subprocess.PIPE,
103 universal_newlines=newlines,
103 universal_newlines=newlines,
104 env=env)
104 env=env)
105 return p.stdin, p.stdout, p.stderr, p
105 return p.stdin, p.stdout, p.stderr, p
106
106
107 def pipefilter(s, cmd):
107 def pipefilter(s, cmd):
108 '''filter string S through command CMD, returning its output'''
108 '''filter string S through command CMD, returning its output'''
109 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
109 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
110 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
110 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
111 pout, perr = p.communicate(s)
111 pout, perr = p.communicate(s)
112 return pout
112 return pout
113
113
114 def tempfilter(s, cmd):
114 def tempfilter(s, cmd):
115 '''filter string S through a pair of temporary files with CMD.
115 '''filter string S through a pair of temporary files with CMD.
116 CMD is used as a template to create the real command to be run,
116 CMD is used as a template to create the real command to be run,
117 with the strings INFILE and OUTFILE replaced by the real names of
117 with the strings INFILE and OUTFILE replaced by the real names of
118 the temporary files generated.'''
118 the temporary files generated.'''
119 inname, outname = None, None
119 inname, outname = None, None
120 try:
120 try:
121 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
121 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
122 fp = os.fdopen(infd, r'wb')
122 fp = os.fdopen(infd, r'wb')
123 fp.write(s)
123 fp.write(s)
124 fp.close()
124 fp.close()
125 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
125 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
126 os.close(outfd)
126 os.close(outfd)
127 cmd = cmd.replace('INFILE', inname)
127 cmd = cmd.replace('INFILE', inname)
128 cmd = cmd.replace('OUTFILE', outname)
128 cmd = cmd.replace('OUTFILE', outname)
129 code = os.system(cmd)
129 code = os.system(cmd)
130 if pycompat.sysplatform == 'OpenVMS' and code & 1:
130 if pycompat.sysplatform == 'OpenVMS' and code & 1:
131 code = 0
131 code = 0
132 if code:
132 if code:
133 raise error.Abort(_("command '%s' failed: %s") %
133 raise error.Abort(_("command '%s' failed: %s") %
134 (cmd, explainexit(code)))
134 (cmd, explainexit(code)))
135 with open(outname, 'rb') as fp:
135 with open(outname, 'rb') as fp:
136 return fp.read()
136 return fp.read()
137 finally:
137 finally:
138 try:
138 try:
139 if inname:
139 if inname:
140 os.unlink(inname)
140 os.unlink(inname)
141 except OSError:
141 except OSError:
142 pass
142 pass
143 try:
143 try:
144 if outname:
144 if outname:
145 os.unlink(outname)
145 os.unlink(outname)
146 except OSError:
146 except OSError:
147 pass
147 pass
148
148
149 _filtertable = {
149 _filtertable = {
150 'tempfile:': tempfilter,
150 'tempfile:': tempfilter,
151 'pipe:': pipefilter,
151 'pipe:': pipefilter,
152 }
152 }
153
153
154 def filter(s, cmd):
154 def filter(s, cmd):
155 "filter a string through a command that transforms its input to its output"
155 "filter a string through a command that transforms its input to its output"
156 for name, fn in _filtertable.iteritems():
156 for name, fn in _filtertable.iteritems():
157 if cmd.startswith(name):
157 if cmd.startswith(name):
158 return fn(s, cmd[len(name):].lstrip())
158 return fn(s, cmd[len(name):].lstrip())
159 return pipefilter(s, cmd)
159 return pipefilter(s, cmd)
160
160
161 def mainfrozen():
161 def mainfrozen():
162 """return True if we are a frozen executable.
162 """return True if we are a frozen executable.
163
163
164 The code supports py2exe (most common, Windows only) and tools/freeze
164 The code supports py2exe (most common, Windows only) and tools/freeze
165 (portable, not much used).
165 (portable, not much used).
166 """
166 """
167 return (pycompat.safehasattr(sys, "frozen") or # new py2exe
167 return (pycompat.safehasattr(sys, "frozen") or # new py2exe
168 pycompat.safehasattr(sys, "importers") or # old py2exe
168 pycompat.safehasattr(sys, "importers") or # old py2exe
169 imp.is_frozen(u"__main__")) # tools/freeze
169 imp.is_frozen(u"__main__")) # tools/freeze
170
170
171 _hgexecutable = None
171 _hgexecutable = None
172
172
173 def hgexecutable():
173 def hgexecutable():
174 """return location of the 'hg' executable.
174 """return location of the 'hg' executable.
175
175
176 Defaults to $HG or 'hg' in the search path.
176 Defaults to $HG or 'hg' in the search path.
177 """
177 """
178 if _hgexecutable is None:
178 if _hgexecutable is None:
179 hg = encoding.environ.get('HG')
179 hg = encoding.environ.get('HG')
180 mainmod = sys.modules[r'__main__']
180 mainmod = sys.modules[r'__main__']
181 if hg:
181 if hg:
182 _sethgexecutable(hg)
182 _sethgexecutable(hg)
183 elif mainfrozen():
183 elif mainfrozen():
184 if getattr(sys, 'frozen', None) == 'macosx_app':
184 if getattr(sys, 'frozen', None) == 'macosx_app':
185 # Env variable set by py2app
185 # Env variable set by py2app
186 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
186 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
187 else:
187 else:
188 _sethgexecutable(pycompat.sysexecutable)
188 _sethgexecutable(pycompat.sysexecutable)
189 elif (os.path.basename(
189 elif (os.path.basename(
190 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
190 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
191 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
191 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
192 else:
192 else:
193 exe = findexe('hg') or os.path.basename(sys.argv[0])
193 exe = findexe('hg') or os.path.basename(sys.argv[0])
194 _sethgexecutable(exe)
194 _sethgexecutable(exe)
195 return _hgexecutable
195 return _hgexecutable
196
196
197 def _sethgexecutable(path):
197 def _sethgexecutable(path):
198 """set location of the 'hg' executable"""
198 """set location of the 'hg' executable"""
199 global _hgexecutable
199 global _hgexecutable
200 _hgexecutable = path
200 _hgexecutable = path
201
201
202 def _testfileno(f, stdf):
202 def _testfileno(f, stdf):
203 fileno = getattr(f, 'fileno', None)
203 fileno = getattr(f, 'fileno', None)
204 try:
204 try:
205 return fileno and fileno() == stdf.fileno()
205 return fileno and fileno() == stdf.fileno()
206 except io.UnsupportedOperation:
206 except io.UnsupportedOperation:
207 return False # fileno() raised UnsupportedOperation
207 return False # fileno() raised UnsupportedOperation
208
208
209 def isstdin(f):
209 def isstdin(f):
210 return _testfileno(f, sys.__stdin__)
210 return _testfileno(f, sys.__stdin__)
211
211
212 def isstdout(f):
212 def isstdout(f):
213 return _testfileno(f, sys.__stdout__)
213 return _testfileno(f, sys.__stdout__)
214
214
215 def protectstdio(uin, uout):
215 def protectstdio(uin, uout):
216 """Duplicate streams and redirect original to null if (uin, uout) are
216 """Duplicate streams and redirect original to null if (uin, uout) are
217 stdio
217 stdio
218
218
219 Returns (fin, fout) which point to the original (uin, uout) fds, but
219 Returns (fin, fout) which point to the original (uin, uout) fds, but
220 may be copy of (uin, uout). The returned streams can be considered
220 may be copy of (uin, uout). The returned streams can be considered
221 "owned" in that print(), exec(), etc. never reach to them.
221 "owned" in that print(), exec(), etc. never reach to them.
222 """
222 """
223 uout.flush()
223 uout.flush()
224 newfiles = []
225 nullfd = os.open(os.devnull, os.O_RDWR)
224 nullfd = os.open(os.devnull, os.O_RDWR)
226 for f, sysf, mode in [(uin, stdin, r'rb'),
225 fin, fout = uin, uout
227 (uout, stdout, r'wb')]:
226 if uin is stdin:
228 if f is sysf:
227 newfd = os.dup(uin.fileno())
229 newfd = os.dup(f.fileno())
228 os.dup2(nullfd, uin.fileno())
230 os.dup2(nullfd, f.fileno())
229 fin = os.fdopen(newfd, r'rb')
231 f = os.fdopen(newfd, mode)
230 if uout is stdout:
232 newfiles.append(f)
231 newfd = os.dup(uout.fileno())
232 os.dup2(nullfd, uout.fileno())
233 fout = os.fdopen(newfd, r'wb')
233 os.close(nullfd)
234 os.close(nullfd)
234 return tuple(newfiles)
235 return fin, fout
235
236
236 def restorestdio(uin, uout, fin, fout):
237 def restorestdio(uin, uout, fin, fout):
237 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
238 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
238 uout.flush()
239 uout.flush()
239 for f, uif in [(fin, uin), (fout, uout)]:
240 for f, uif in [(fin, uin), (fout, uout)]:
240 if f is not uif:
241 if f is not uif:
241 os.dup2(f.fileno(), uif.fileno())
242 os.dup2(f.fileno(), uif.fileno())
242 f.close()
243 f.close()
243
244
244 @contextlib.contextmanager
245 @contextlib.contextmanager
245 def protectedstdio(uin, uout):
246 def protectedstdio(uin, uout):
246 """Run code block with protected standard streams"""
247 """Run code block with protected standard streams"""
247 fin, fout = protectstdio(uin, uout)
248 fin, fout = protectstdio(uin, uout)
248 try:
249 try:
249 yield fin, fout
250 yield fin, fout
250 finally:
251 finally:
251 restorestdio(uin, uout, fin, fout)
252 restorestdio(uin, uout, fin, fout)
252
253
253 def shellenviron(environ=None):
254 def shellenviron(environ=None):
254 """return environ with optional override, useful for shelling out"""
255 """return environ with optional override, useful for shelling out"""
255 def py2shell(val):
256 def py2shell(val):
256 'convert python object into string that is useful to shell'
257 'convert python object into string that is useful to shell'
257 if val is None or val is False:
258 if val is None or val is False:
258 return '0'
259 return '0'
259 if val is True:
260 if val is True:
260 return '1'
261 return '1'
261 return pycompat.bytestr(val)
262 return pycompat.bytestr(val)
262 env = dict(encoding.environ)
263 env = dict(encoding.environ)
263 if environ:
264 if environ:
264 env.update((k, py2shell(v)) for k, v in environ.iteritems())
265 env.update((k, py2shell(v)) for k, v in environ.iteritems())
265 env['HG'] = hgexecutable()
266 env['HG'] = hgexecutable()
266 return env
267 return env
267
268
268 def system(cmd, environ=None, cwd=None, out=None):
269 def system(cmd, environ=None, cwd=None, out=None):
269 '''enhanced shell command execution.
270 '''enhanced shell command execution.
270 run with environment maybe modified, maybe in different dir.
271 run with environment maybe modified, maybe in different dir.
271
272
272 if out is specified, it is assumed to be a file-like object that has a
273 if out is specified, it is assumed to be a file-like object that has a
273 write() method. stdout and stderr will be redirected to out.'''
274 write() method. stdout and stderr will be redirected to out.'''
274 try:
275 try:
275 stdout.flush()
276 stdout.flush()
276 except Exception:
277 except Exception:
277 pass
278 pass
278 cmd = quotecommand(cmd)
279 cmd = quotecommand(cmd)
279 env = shellenviron(environ)
280 env = shellenviron(environ)
280 if out is None or isstdout(out):
281 if out is None or isstdout(out):
281 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
282 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
282 env=env, cwd=cwd)
283 env=env, cwd=cwd)
283 else:
284 else:
284 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
285 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
285 env=env, cwd=cwd, stdout=subprocess.PIPE,
286 env=env, cwd=cwd, stdout=subprocess.PIPE,
286 stderr=subprocess.STDOUT)
287 stderr=subprocess.STDOUT)
287 for line in iter(proc.stdout.readline, ''):
288 for line in iter(proc.stdout.readline, ''):
288 out.write(line)
289 out.write(line)
289 proc.wait()
290 proc.wait()
290 rc = proc.returncode
291 rc = proc.returncode
291 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
292 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
292 rc = 0
293 rc = 0
293 return rc
294 return rc
294
295
295 def gui():
296 def gui():
296 '''Are we running in a GUI?'''
297 '''Are we running in a GUI?'''
297 if pycompat.isdarwin:
298 if pycompat.isdarwin:
298 if 'SSH_CONNECTION' in encoding.environ:
299 if 'SSH_CONNECTION' in encoding.environ:
299 # handle SSH access to a box where the user is logged in
300 # handle SSH access to a box where the user is logged in
300 return False
301 return False
301 elif getattr(osutil, 'isgui', None):
302 elif getattr(osutil, 'isgui', None):
302 # check if a CoreGraphics session is available
303 # check if a CoreGraphics session is available
303 return osutil.isgui()
304 return osutil.isgui()
304 else:
305 else:
305 # pure build; use a safe default
306 # pure build; use a safe default
306 return True
307 return True
307 else:
308 else:
308 return pycompat.iswindows or encoding.environ.get("DISPLAY")
309 return pycompat.iswindows or encoding.environ.get("DISPLAY")
309
310
310 def hgcmd():
311 def hgcmd():
311 """Return the command used to execute current hg
312 """Return the command used to execute current hg
312
313
313 This is different from hgexecutable() because on Windows we want
314 This is different from hgexecutable() because on Windows we want
314 to avoid things opening new shell windows like batch files, so we
315 to avoid things opening new shell windows like batch files, so we
315 get either the python call or current executable.
316 get either the python call or current executable.
316 """
317 """
317 if mainfrozen():
318 if mainfrozen():
318 if getattr(sys, 'frozen', None) == 'macosx_app':
319 if getattr(sys, 'frozen', None) == 'macosx_app':
319 # Env variable set by py2app
320 # Env variable set by py2app
320 return [encoding.environ['EXECUTABLEPATH']]
321 return [encoding.environ['EXECUTABLEPATH']]
321 else:
322 else:
322 return [pycompat.sysexecutable]
323 return [pycompat.sysexecutable]
323 return _gethgcmd()
324 return _gethgcmd()
324
325
325 def rundetached(args, condfn):
326 def rundetached(args, condfn):
326 """Execute the argument list in a detached process.
327 """Execute the argument list in a detached process.
327
328
328 condfn is a callable which is called repeatedly and should return
329 condfn is a callable which is called repeatedly and should return
329 True once the child process is known to have started successfully.
330 True once the child process is known to have started successfully.
330 At this point, the child process PID is returned. If the child
331 At this point, the child process PID is returned. If the child
331 process fails to start or finishes before condfn() evaluates to
332 process fails to start or finishes before condfn() evaluates to
332 True, return -1.
333 True, return -1.
333 """
334 """
334 # Windows case is easier because the child process is either
335 # Windows case is easier because the child process is either
335 # successfully starting and validating the condition or exiting
336 # successfully starting and validating the condition or exiting
336 # on failure. We just poll on its PID. On Unix, if the child
337 # on failure. We just poll on its PID. On Unix, if the child
337 # process fails to start, it will be left in a zombie state until
338 # process fails to start, it will be left in a zombie state until
338 # the parent wait on it, which we cannot do since we expect a long
339 # the parent wait on it, which we cannot do since we expect a long
339 # running process on success. Instead we listen for SIGCHLD telling
340 # running process on success. Instead we listen for SIGCHLD telling
340 # us our child process terminated.
341 # us our child process terminated.
341 terminated = set()
342 terminated = set()
342 def handler(signum, frame):
343 def handler(signum, frame):
343 terminated.add(os.wait())
344 terminated.add(os.wait())
344 prevhandler = None
345 prevhandler = None
345 SIGCHLD = getattr(signal, 'SIGCHLD', None)
346 SIGCHLD = getattr(signal, 'SIGCHLD', None)
346 if SIGCHLD is not None:
347 if SIGCHLD is not None:
347 prevhandler = signal.signal(SIGCHLD, handler)
348 prevhandler = signal.signal(SIGCHLD, handler)
348 try:
349 try:
349 pid = spawndetached(args)
350 pid = spawndetached(args)
350 while not condfn():
351 while not condfn():
351 if ((pid in terminated or not testpid(pid))
352 if ((pid in terminated or not testpid(pid))
352 and not condfn()):
353 and not condfn()):
353 return -1
354 return -1
354 time.sleep(0.1)
355 time.sleep(0.1)
355 return pid
356 return pid
356 finally:
357 finally:
357 if prevhandler is not None:
358 if prevhandler is not None:
358 signal.signal(signal.SIGCHLD, prevhandler)
359 signal.signal(signal.SIGCHLD, prevhandler)
General Comments 0
You need to be logged in to leave comments. Login now