##// END OF EJS Templates
procutil: always waiting on child processes to prevent zombies with 'hg serve'...
Rodrigo Damazio Bovendorp -
r45304:ed684a82 stable
parent child Browse files
Show More
@@ -1,669 +1,678 b''
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 errno
13 import errno
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 threading
19 import time
20 import time
20
21
21 from ..i18n import _
22 from ..i18n import _
22 from ..pycompat import (
23 from ..pycompat import (
23 getattr,
24 getattr,
24 open,
25 open,
25 )
26 )
26
27
27 from .. import (
28 from .. import (
28 encoding,
29 encoding,
29 error,
30 error,
30 policy,
31 policy,
31 pycompat,
32 pycompat,
32 )
33 )
33
34
34 # Import like this to keep import-checker happy
35 # Import like this to keep import-checker happy
35 from ..utils import resourceutil
36 from ..utils import resourceutil
36
37
37 osutil = policy.importmod('osutil')
38 osutil = policy.importmod('osutil')
38
39
39 stderr = pycompat.stderr
40 stderr = pycompat.stderr
40 stdin = pycompat.stdin
41 stdin = pycompat.stdin
41 stdout = pycompat.stdout
42 stdout = pycompat.stdout
42
43
43
44
44 def isatty(fp):
45 def isatty(fp):
45 try:
46 try:
46 return fp.isatty()
47 return fp.isatty()
47 except AttributeError:
48 except AttributeError:
48 return False
49 return False
49
50
50
51
51 # glibc determines buffering on first write to stdout - if we replace a TTY
52 # glibc determines buffering on first write to stdout - if we replace a TTY
52 # destined stdout with a pipe destined stdout (e.g. pager), we want line
53 # destined stdout with a pipe destined stdout (e.g. pager), we want line
53 # buffering (or unbuffered, on Windows)
54 # buffering (or unbuffered, on Windows)
54 if isatty(stdout):
55 if isatty(stdout):
55 if pycompat.iswindows:
56 if pycompat.iswindows:
56 # Windows doesn't support line buffering
57 # Windows doesn't support line buffering
57 stdout = os.fdopen(stdout.fileno(), 'wb', 0)
58 stdout = os.fdopen(stdout.fileno(), 'wb', 0)
58 elif not pycompat.ispy3:
59 elif not pycompat.ispy3:
59 # on Python 3, stdout (sys.stdout.buffer) is already line buffered and
60 # on Python 3, stdout (sys.stdout.buffer) is already line buffered and
60 # buffering=1 is not handled in binary mode
61 # buffering=1 is not handled in binary mode
61 stdout = os.fdopen(stdout.fileno(), 'wb', 1)
62 stdout = os.fdopen(stdout.fileno(), 'wb', 1)
62
63
63 if pycompat.iswindows:
64 if pycompat.iswindows:
64 from .. import windows as platform
65 from .. import windows as platform
65
66
66 stdout = platform.winstdout(stdout)
67 stdout = platform.winstdout(stdout)
67 else:
68 else:
68 from .. import posix as platform
69 from .. import posix as platform
69
70
70 findexe = platform.findexe
71 findexe = platform.findexe
71 _gethgcmd = platform.gethgcmd
72 _gethgcmd = platform.gethgcmd
72 getuser = platform.getuser
73 getuser = platform.getuser
73 getpid = os.getpid
74 getpid = os.getpid
74 hidewindow = platform.hidewindow
75 hidewindow = platform.hidewindow
75 quotecommand = platform.quotecommand
76 quotecommand = platform.quotecommand
76 readpipe = platform.readpipe
77 readpipe = platform.readpipe
77 setbinary = platform.setbinary
78 setbinary = platform.setbinary
78 setsignalhandler = platform.setsignalhandler
79 setsignalhandler = platform.setsignalhandler
79 shellquote = platform.shellquote
80 shellquote = platform.shellquote
80 shellsplit = platform.shellsplit
81 shellsplit = platform.shellsplit
81 spawndetached = platform.spawndetached
82 spawndetached = platform.spawndetached
82 sshargs = platform.sshargs
83 sshargs = platform.sshargs
83 testpid = platform.testpid
84 testpid = platform.testpid
84
85
85 try:
86 try:
86 setprocname = osutil.setprocname
87 setprocname = osutil.setprocname
87 except AttributeError:
88 except AttributeError:
88 pass
89 pass
89 try:
90 try:
90 unblocksignal = osutil.unblocksignal
91 unblocksignal = osutil.unblocksignal
91 except AttributeError:
92 except AttributeError:
92 pass
93 pass
93
94
94 closefds = pycompat.isposix
95 closefds = pycompat.isposix
95
96
96
97
97 def explainexit(code):
98 def explainexit(code):
98 """return a message describing a subprocess status
99 """return a message describing a subprocess status
99 (codes from kill are negative - not os.system/wait encoding)"""
100 (codes from kill are negative - not os.system/wait encoding)"""
100 if code >= 0:
101 if code >= 0:
101 return _(b"exited with status %d") % code
102 return _(b"exited with status %d") % code
102 return _(b"killed by signal %d") % -code
103 return _(b"killed by signal %d") % -code
103
104
104
105
105 class _pfile(object):
106 class _pfile(object):
106 """File-like wrapper for a stream opened by subprocess.Popen()"""
107 """File-like wrapper for a stream opened by subprocess.Popen()"""
107
108
108 def __init__(self, proc, fp):
109 def __init__(self, proc, fp):
109 self._proc = proc
110 self._proc = proc
110 self._fp = fp
111 self._fp = fp
111
112
112 def close(self):
113 def close(self):
113 # unlike os.popen(), this returns an integer in subprocess coding
114 # unlike os.popen(), this returns an integer in subprocess coding
114 self._fp.close()
115 self._fp.close()
115 return self._proc.wait()
116 return self._proc.wait()
116
117
117 def __iter__(self):
118 def __iter__(self):
118 return iter(self._fp)
119 return iter(self._fp)
119
120
120 def __getattr__(self, attr):
121 def __getattr__(self, attr):
121 return getattr(self._fp, attr)
122 return getattr(self._fp, attr)
122
123
123 def __enter__(self):
124 def __enter__(self):
124 return self
125 return self
125
126
126 def __exit__(self, exc_type, exc_value, exc_tb):
127 def __exit__(self, exc_type, exc_value, exc_tb):
127 self.close()
128 self.close()
128
129
129
130
130 def popen(cmd, mode=b'rb', bufsize=-1):
131 def popen(cmd, mode=b'rb', bufsize=-1):
131 if mode == b'rb':
132 if mode == b'rb':
132 return _popenreader(cmd, bufsize)
133 return _popenreader(cmd, bufsize)
133 elif mode == b'wb':
134 elif mode == b'wb':
134 return _popenwriter(cmd, bufsize)
135 return _popenwriter(cmd, bufsize)
135 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
136 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
136
137
137
138
138 def _popenreader(cmd, bufsize):
139 def _popenreader(cmd, bufsize):
139 p = subprocess.Popen(
140 p = subprocess.Popen(
140 tonativestr(quotecommand(cmd)),
141 tonativestr(quotecommand(cmd)),
141 shell=True,
142 shell=True,
142 bufsize=bufsize,
143 bufsize=bufsize,
143 close_fds=closefds,
144 close_fds=closefds,
144 stdout=subprocess.PIPE,
145 stdout=subprocess.PIPE,
145 )
146 )
146 return _pfile(p, p.stdout)
147 return _pfile(p, p.stdout)
147
148
148
149
149 def _popenwriter(cmd, bufsize):
150 def _popenwriter(cmd, bufsize):
150 p = subprocess.Popen(
151 p = subprocess.Popen(
151 tonativestr(quotecommand(cmd)),
152 tonativestr(quotecommand(cmd)),
152 shell=True,
153 shell=True,
153 bufsize=bufsize,
154 bufsize=bufsize,
154 close_fds=closefds,
155 close_fds=closefds,
155 stdin=subprocess.PIPE,
156 stdin=subprocess.PIPE,
156 )
157 )
157 return _pfile(p, p.stdin)
158 return _pfile(p, p.stdin)
158
159
159
160
160 def popen2(cmd, env=None):
161 def popen2(cmd, env=None):
161 # Setting bufsize to -1 lets the system decide the buffer size.
162 # Setting bufsize to -1 lets the system decide the buffer size.
162 # The default for bufsize is 0, meaning unbuffered. This leads to
163 # The default for bufsize is 0, meaning unbuffered. This leads to
163 # poor performance on Mac OS X: http://bugs.python.org/issue4194
164 # poor performance on Mac OS X: http://bugs.python.org/issue4194
164 p = subprocess.Popen(
165 p = subprocess.Popen(
165 tonativestr(cmd),
166 tonativestr(cmd),
166 shell=True,
167 shell=True,
167 bufsize=-1,
168 bufsize=-1,
168 close_fds=closefds,
169 close_fds=closefds,
169 stdin=subprocess.PIPE,
170 stdin=subprocess.PIPE,
170 stdout=subprocess.PIPE,
171 stdout=subprocess.PIPE,
171 env=tonativeenv(env),
172 env=tonativeenv(env),
172 )
173 )
173 return p.stdin, p.stdout
174 return p.stdin, p.stdout
174
175
175
176
176 def popen3(cmd, env=None):
177 def popen3(cmd, env=None):
177 stdin, stdout, stderr, p = popen4(cmd, env)
178 stdin, stdout, stderr, p = popen4(cmd, env)
178 return stdin, stdout, stderr
179 return stdin, stdout, stderr
179
180
180
181
181 def popen4(cmd, env=None, bufsize=-1):
182 def popen4(cmd, env=None, bufsize=-1):
182 p = subprocess.Popen(
183 p = subprocess.Popen(
183 tonativestr(cmd),
184 tonativestr(cmd),
184 shell=True,
185 shell=True,
185 bufsize=bufsize,
186 bufsize=bufsize,
186 close_fds=closefds,
187 close_fds=closefds,
187 stdin=subprocess.PIPE,
188 stdin=subprocess.PIPE,
188 stdout=subprocess.PIPE,
189 stdout=subprocess.PIPE,
189 stderr=subprocess.PIPE,
190 stderr=subprocess.PIPE,
190 env=tonativeenv(env),
191 env=tonativeenv(env),
191 )
192 )
192 return p.stdin, p.stdout, p.stderr, p
193 return p.stdin, p.stdout, p.stderr, p
193
194
194
195
195 def pipefilter(s, cmd):
196 def pipefilter(s, cmd):
196 '''filter string S through command CMD, returning its output'''
197 '''filter string S through command CMD, returning its output'''
197 p = subprocess.Popen(
198 p = subprocess.Popen(
198 tonativestr(cmd),
199 tonativestr(cmd),
199 shell=True,
200 shell=True,
200 close_fds=closefds,
201 close_fds=closefds,
201 stdin=subprocess.PIPE,
202 stdin=subprocess.PIPE,
202 stdout=subprocess.PIPE,
203 stdout=subprocess.PIPE,
203 )
204 )
204 pout, perr = p.communicate(s)
205 pout, perr = p.communicate(s)
205 return pout
206 return pout
206
207
207
208
208 def tempfilter(s, cmd):
209 def tempfilter(s, cmd):
209 '''filter string S through a pair of temporary files with CMD.
210 '''filter string S through a pair of temporary files with CMD.
210 CMD is used as a template to create the real command to be run,
211 CMD is used as a template to create the real command to be run,
211 with the strings INFILE and OUTFILE replaced by the real names of
212 with the strings INFILE and OUTFILE replaced by the real names of
212 the temporary files generated.'''
213 the temporary files generated.'''
213 inname, outname = None, None
214 inname, outname = None, None
214 try:
215 try:
215 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
216 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
216 fp = os.fdopen(infd, 'wb')
217 fp = os.fdopen(infd, 'wb')
217 fp.write(s)
218 fp.write(s)
218 fp.close()
219 fp.close()
219 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
220 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
220 os.close(outfd)
221 os.close(outfd)
221 cmd = cmd.replace(b'INFILE', inname)
222 cmd = cmd.replace(b'INFILE', inname)
222 cmd = cmd.replace(b'OUTFILE', outname)
223 cmd = cmd.replace(b'OUTFILE', outname)
223 code = system(cmd)
224 code = system(cmd)
224 if pycompat.sysplatform == b'OpenVMS' and code & 1:
225 if pycompat.sysplatform == b'OpenVMS' and code & 1:
225 code = 0
226 code = 0
226 if code:
227 if code:
227 raise error.Abort(
228 raise error.Abort(
228 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
229 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
229 )
230 )
230 with open(outname, b'rb') as fp:
231 with open(outname, b'rb') as fp:
231 return fp.read()
232 return fp.read()
232 finally:
233 finally:
233 try:
234 try:
234 if inname:
235 if inname:
235 os.unlink(inname)
236 os.unlink(inname)
236 except OSError:
237 except OSError:
237 pass
238 pass
238 try:
239 try:
239 if outname:
240 if outname:
240 os.unlink(outname)
241 os.unlink(outname)
241 except OSError:
242 except OSError:
242 pass
243 pass
243
244
244
245
245 _filtertable = {
246 _filtertable = {
246 b'tempfile:': tempfilter,
247 b'tempfile:': tempfilter,
247 b'pipe:': pipefilter,
248 b'pipe:': pipefilter,
248 }
249 }
249
250
250
251
251 def filter(s, cmd):
252 def filter(s, cmd):
252 """filter a string through a command that transforms its input to its
253 """filter a string through a command that transforms its input to its
253 output"""
254 output"""
254 for name, fn in pycompat.iteritems(_filtertable):
255 for name, fn in pycompat.iteritems(_filtertable):
255 if cmd.startswith(name):
256 if cmd.startswith(name):
256 return fn(s, cmd[len(name) :].lstrip())
257 return fn(s, cmd[len(name) :].lstrip())
257 return pipefilter(s, cmd)
258 return pipefilter(s, cmd)
258
259
259
260
260 _hgexecutable = None
261 _hgexecutable = None
261
262
262
263
263 def hgexecutable():
264 def hgexecutable():
264 """return location of the 'hg' executable.
265 """return location of the 'hg' executable.
265
266
266 Defaults to $HG or 'hg' in the search path.
267 Defaults to $HG or 'hg' in the search path.
267 """
268 """
268 if _hgexecutable is None:
269 if _hgexecutable is None:
269 hg = encoding.environ.get(b'HG')
270 hg = encoding.environ.get(b'HG')
270 mainmod = sys.modules['__main__']
271 mainmod = sys.modules['__main__']
271 if hg:
272 if hg:
272 _sethgexecutable(hg)
273 _sethgexecutable(hg)
273 elif resourceutil.mainfrozen():
274 elif resourceutil.mainfrozen():
274 if getattr(sys, 'frozen', None) == 'macosx_app':
275 if getattr(sys, 'frozen', None) == 'macosx_app':
275 # Env variable set by py2app
276 # Env variable set by py2app
276 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
277 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
277 else:
278 else:
278 _sethgexecutable(pycompat.sysexecutable)
279 _sethgexecutable(pycompat.sysexecutable)
279 elif (
280 elif (
280 not pycompat.iswindows
281 not pycompat.iswindows
281 and os.path.basename(getattr(mainmod, '__file__', '')) == 'hg'
282 and os.path.basename(getattr(mainmod, '__file__', '')) == 'hg'
282 ):
283 ):
283 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
284 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
284 else:
285 else:
285 _sethgexecutable(
286 _sethgexecutable(
286 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
287 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
287 )
288 )
288 return _hgexecutable
289 return _hgexecutable
289
290
290
291
291 def _sethgexecutable(path):
292 def _sethgexecutable(path):
292 """set location of the 'hg' executable"""
293 """set location of the 'hg' executable"""
293 global _hgexecutable
294 global _hgexecutable
294 _hgexecutable = path
295 _hgexecutable = path
295
296
296
297
297 def _testfileno(f, stdf):
298 def _testfileno(f, stdf):
298 fileno = getattr(f, 'fileno', None)
299 fileno = getattr(f, 'fileno', None)
299 try:
300 try:
300 return fileno and fileno() == stdf.fileno()
301 return fileno and fileno() == stdf.fileno()
301 except io.UnsupportedOperation:
302 except io.UnsupportedOperation:
302 return False # fileno() raised UnsupportedOperation
303 return False # fileno() raised UnsupportedOperation
303
304
304
305
305 def isstdin(f):
306 def isstdin(f):
306 return _testfileno(f, sys.__stdin__)
307 return _testfileno(f, sys.__stdin__)
307
308
308
309
309 def isstdout(f):
310 def isstdout(f):
310 return _testfileno(f, sys.__stdout__)
311 return _testfileno(f, sys.__stdout__)
311
312
312
313
313 def protectstdio(uin, uout):
314 def protectstdio(uin, uout):
314 """Duplicate streams and redirect original if (uin, uout) are stdio
315 """Duplicate streams and redirect original if (uin, uout) are stdio
315
316
316 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
317 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
317 redirected to stderr so the output is still readable.
318 redirected to stderr so the output is still readable.
318
319
319 Returns (fin, fout) which point to the original (uin, uout) fds, but
320 Returns (fin, fout) which point to the original (uin, uout) fds, but
320 may be copy of (uin, uout). The returned streams can be considered
321 may be copy of (uin, uout). The returned streams can be considered
321 "owned" in that print(), exec(), etc. never reach to them.
322 "owned" in that print(), exec(), etc. never reach to them.
322 """
323 """
323 uout.flush()
324 uout.flush()
324 fin, fout = uin, uout
325 fin, fout = uin, uout
325 if _testfileno(uin, stdin):
326 if _testfileno(uin, stdin):
326 newfd = os.dup(uin.fileno())
327 newfd = os.dup(uin.fileno())
327 nullfd = os.open(os.devnull, os.O_RDONLY)
328 nullfd = os.open(os.devnull, os.O_RDONLY)
328 os.dup2(nullfd, uin.fileno())
329 os.dup2(nullfd, uin.fileno())
329 os.close(nullfd)
330 os.close(nullfd)
330 fin = os.fdopen(newfd, 'rb')
331 fin = os.fdopen(newfd, 'rb')
331 if _testfileno(uout, stdout):
332 if _testfileno(uout, stdout):
332 newfd = os.dup(uout.fileno())
333 newfd = os.dup(uout.fileno())
333 os.dup2(stderr.fileno(), uout.fileno())
334 os.dup2(stderr.fileno(), uout.fileno())
334 fout = os.fdopen(newfd, 'wb')
335 fout = os.fdopen(newfd, 'wb')
335 return fin, fout
336 return fin, fout
336
337
337
338
338 def restorestdio(uin, uout, fin, fout):
339 def restorestdio(uin, uout, fin, fout):
339 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
340 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
340 uout.flush()
341 uout.flush()
341 for f, uif in [(fin, uin), (fout, uout)]:
342 for f, uif in [(fin, uin), (fout, uout)]:
342 if f is not uif:
343 if f is not uif:
343 os.dup2(f.fileno(), uif.fileno())
344 os.dup2(f.fileno(), uif.fileno())
344 f.close()
345 f.close()
345
346
346
347
347 def shellenviron(environ=None):
348 def shellenviron(environ=None):
348 """return environ with optional override, useful for shelling out"""
349 """return environ with optional override, useful for shelling out"""
349
350
350 def py2shell(val):
351 def py2shell(val):
351 """convert python object into string that is useful to shell"""
352 """convert python object into string that is useful to shell"""
352 if val is None or val is False:
353 if val is None or val is False:
353 return b'0'
354 return b'0'
354 if val is True:
355 if val is True:
355 return b'1'
356 return b'1'
356 return pycompat.bytestr(val)
357 return pycompat.bytestr(val)
357
358
358 env = dict(encoding.environ)
359 env = dict(encoding.environ)
359 if environ:
360 if environ:
360 env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
361 env.update((k, py2shell(v)) for k, v in pycompat.iteritems(environ))
361 env[b'HG'] = hgexecutable()
362 env[b'HG'] = hgexecutable()
362 return env
363 return env
363
364
364
365
365 if pycompat.iswindows:
366 if pycompat.iswindows:
366
367
367 def shelltonative(cmd, env):
368 def shelltonative(cmd, env):
368 return platform.shelltocmdexe( # pytype: disable=module-attr
369 return platform.shelltocmdexe( # pytype: disable=module-attr
369 cmd, shellenviron(env)
370 cmd, shellenviron(env)
370 )
371 )
371
372
372 tonativestr = encoding.strfromlocal
373 tonativestr = encoding.strfromlocal
373 else:
374 else:
374
375
375 def shelltonative(cmd, env):
376 def shelltonative(cmd, env):
376 return cmd
377 return cmd
377
378
378 tonativestr = pycompat.identity
379 tonativestr = pycompat.identity
379
380
380
381
381 def tonativeenv(env):
382 def tonativeenv(env):
382 '''convert the environment from bytes to strings suitable for Popen(), etc.
383 '''convert the environment from bytes to strings suitable for Popen(), etc.
383 '''
384 '''
384 return pycompat.rapply(tonativestr, env)
385 return pycompat.rapply(tonativestr, env)
385
386
386
387
387 def system(cmd, environ=None, cwd=None, out=None):
388 def system(cmd, environ=None, cwd=None, out=None):
388 '''enhanced shell command execution.
389 '''enhanced shell command execution.
389 run with environment maybe modified, maybe in different dir.
390 run with environment maybe modified, maybe in different dir.
390
391
391 if out is specified, it is assumed to be a file-like object that has a
392 if out is specified, it is assumed to be a file-like object that has a
392 write() method. stdout and stderr will be redirected to out.'''
393 write() method. stdout and stderr will be redirected to out.'''
393 try:
394 try:
394 stdout.flush()
395 stdout.flush()
395 except Exception:
396 except Exception:
396 pass
397 pass
397 cmd = quotecommand(cmd)
398 cmd = quotecommand(cmd)
398 env = shellenviron(environ)
399 env = shellenviron(environ)
399 if out is None or isstdout(out):
400 if out is None or isstdout(out):
400 rc = subprocess.call(
401 rc = subprocess.call(
401 tonativestr(cmd),
402 tonativestr(cmd),
402 shell=True,
403 shell=True,
403 close_fds=closefds,
404 close_fds=closefds,
404 env=tonativeenv(env),
405 env=tonativeenv(env),
405 cwd=pycompat.rapply(tonativestr, cwd),
406 cwd=pycompat.rapply(tonativestr, cwd),
406 )
407 )
407 else:
408 else:
408 proc = subprocess.Popen(
409 proc = subprocess.Popen(
409 tonativestr(cmd),
410 tonativestr(cmd),
410 shell=True,
411 shell=True,
411 close_fds=closefds,
412 close_fds=closefds,
412 env=tonativeenv(env),
413 env=tonativeenv(env),
413 cwd=pycompat.rapply(tonativestr, cwd),
414 cwd=pycompat.rapply(tonativestr, cwd),
414 stdout=subprocess.PIPE,
415 stdout=subprocess.PIPE,
415 stderr=subprocess.STDOUT,
416 stderr=subprocess.STDOUT,
416 )
417 )
417 for line in iter(proc.stdout.readline, b''):
418 for line in iter(proc.stdout.readline, b''):
418 out.write(line)
419 out.write(line)
419 proc.wait()
420 proc.wait()
420 rc = proc.returncode
421 rc = proc.returncode
421 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
422 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
422 rc = 0
423 rc = 0
423 return rc
424 return rc
424
425
425
426
426 _is_gui = None
427 _is_gui = None
427
428
428
429
429 def _gui():
430 def _gui():
430 '''Are we running in a GUI?'''
431 '''Are we running in a GUI?'''
431 if pycompat.isdarwin:
432 if pycompat.isdarwin:
432 if b'SSH_CONNECTION' in encoding.environ:
433 if b'SSH_CONNECTION' in encoding.environ:
433 # handle SSH access to a box where the user is logged in
434 # handle SSH access to a box where the user is logged in
434 return False
435 return False
435 elif getattr(osutil, 'isgui', None):
436 elif getattr(osutil, 'isgui', None):
436 # check if a CoreGraphics session is available
437 # check if a CoreGraphics session is available
437 return osutil.isgui()
438 return osutil.isgui()
438 else:
439 else:
439 # pure build; use a safe default
440 # pure build; use a safe default
440 return True
441 return True
441 else:
442 else:
442 return pycompat.iswindows or encoding.environ.get(b"DISPLAY")
443 return pycompat.iswindows or encoding.environ.get(b"DISPLAY")
443
444
444
445
445 def gui():
446 def gui():
446 global _is_gui
447 global _is_gui
447 if _is_gui is None:
448 if _is_gui is None:
448 _is_gui = _gui()
449 _is_gui = _gui()
449 return _is_gui
450 return _is_gui
450
451
451
452
452 def hgcmd():
453 def hgcmd():
453 """Return the command used to execute current hg
454 """Return the command used to execute current hg
454
455
455 This is different from hgexecutable() because on Windows we want
456 This is different from hgexecutable() because on Windows we want
456 to avoid things opening new shell windows like batch files, so we
457 to avoid things opening new shell windows like batch files, so we
457 get either the python call or current executable.
458 get either the python call or current executable.
458 """
459 """
459 if resourceutil.mainfrozen():
460 if resourceutil.mainfrozen():
460 if getattr(sys, 'frozen', None) == 'macosx_app':
461 if getattr(sys, 'frozen', None) == 'macosx_app':
461 # Env variable set by py2app
462 # Env variable set by py2app
462 return [encoding.environ[b'EXECUTABLEPATH']]
463 return [encoding.environ[b'EXECUTABLEPATH']]
463 else:
464 else:
464 return [pycompat.sysexecutable]
465 return [pycompat.sysexecutable]
465 return _gethgcmd()
466 return _gethgcmd()
466
467
467
468
468 def rundetached(args, condfn):
469 def rundetached(args, condfn):
469 """Execute the argument list in a detached process.
470 """Execute the argument list in a detached process.
470
471
471 condfn is a callable which is called repeatedly and should return
472 condfn is a callable which is called repeatedly and should return
472 True once the child process is known to have started successfully.
473 True once the child process is known to have started successfully.
473 At this point, the child process PID is returned. If the child
474 At this point, the child process PID is returned. If the child
474 process fails to start or finishes before condfn() evaluates to
475 process fails to start or finishes before condfn() evaluates to
475 True, return -1.
476 True, return -1.
476 """
477 """
477 # Windows case is easier because the child process is either
478 # Windows case is easier because the child process is either
478 # successfully starting and validating the condition or exiting
479 # successfully starting and validating the condition or exiting
479 # on failure. We just poll on its PID. On Unix, if the child
480 # on failure. We just poll on its PID. On Unix, if the child
480 # process fails to start, it will be left in a zombie state until
481 # process fails to start, it will be left in a zombie state until
481 # the parent wait on it, which we cannot do since we expect a long
482 # the parent wait on it, which we cannot do since we expect a long
482 # running process on success. Instead we listen for SIGCHLD telling
483 # running process on success. Instead we listen for SIGCHLD telling
483 # us our child process terminated.
484 # us our child process terminated.
484 terminated = set()
485 terminated = set()
485
486
486 def handler(signum, frame):
487 def handler(signum, frame):
487 terminated.add(os.wait())
488 terminated.add(os.wait())
488
489
489 prevhandler = None
490 prevhandler = None
490 SIGCHLD = getattr(signal, 'SIGCHLD', None)
491 SIGCHLD = getattr(signal, 'SIGCHLD', None)
491 if SIGCHLD is not None:
492 if SIGCHLD is not None:
492 prevhandler = signal.signal(SIGCHLD, handler)
493 prevhandler = signal.signal(SIGCHLD, handler)
493 try:
494 try:
494 pid = spawndetached(args)
495 pid = spawndetached(args)
495 while not condfn():
496 while not condfn():
496 if (pid in terminated or not testpid(pid)) and not condfn():
497 if (pid in terminated or not testpid(pid)) and not condfn():
497 return -1
498 return -1
498 time.sleep(0.1)
499 time.sleep(0.1)
499 return pid
500 return pid
500 finally:
501 finally:
501 if prevhandler is not None:
502 if prevhandler is not None:
502 signal.signal(signal.SIGCHLD, prevhandler)
503 signal.signal(signal.SIGCHLD, prevhandler)
503
504
504
505
505 @contextlib.contextmanager
506 @contextlib.contextmanager
506 def uninterruptible(warn):
507 def uninterruptible(warn):
507 """Inhibit SIGINT handling on a region of code.
508 """Inhibit SIGINT handling on a region of code.
508
509
509 Note that if this is called in a non-main thread, it turns into a no-op.
510 Note that if this is called in a non-main thread, it turns into a no-op.
510
511
511 Args:
512 Args:
512 warn: A callable which takes no arguments, and returns True if the
513 warn: A callable which takes no arguments, and returns True if the
513 previous signal handling should be restored.
514 previous signal handling should be restored.
514 """
515 """
515
516
516 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
517 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
517 shouldbail = []
518 shouldbail = []
518
519
519 def disabledsiginthandler(*args):
520 def disabledsiginthandler(*args):
520 if warn():
521 if warn():
521 signal.signal(signal.SIGINT, oldsiginthandler[0])
522 signal.signal(signal.SIGINT, oldsiginthandler[0])
522 del oldsiginthandler[0]
523 del oldsiginthandler[0]
523 shouldbail.append(True)
524 shouldbail.append(True)
524
525
525 try:
526 try:
526 try:
527 try:
527 signal.signal(signal.SIGINT, disabledsiginthandler)
528 signal.signal(signal.SIGINT, disabledsiginthandler)
528 except ValueError:
529 except ValueError:
529 # wrong thread, oh well, we tried
530 # wrong thread, oh well, we tried
530 del oldsiginthandler[0]
531 del oldsiginthandler[0]
531 yield
532 yield
532 finally:
533 finally:
533 if oldsiginthandler:
534 if oldsiginthandler:
534 signal.signal(signal.SIGINT, oldsiginthandler[0])
535 signal.signal(signal.SIGINT, oldsiginthandler[0])
535 if shouldbail:
536 if shouldbail:
536 raise KeyboardInterrupt
537 raise KeyboardInterrupt
537
538
538
539
539 if pycompat.iswindows:
540 if pycompat.iswindows:
540 # no fork on Windows, but we can create a detached process
541 # no fork on Windows, but we can create a detached process
541 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
542 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
542 # No stdlib constant exists for this value
543 # No stdlib constant exists for this value
543 DETACHED_PROCESS = 0x00000008
544 DETACHED_PROCESS = 0x00000008
544 # Following creation flags might create a console GUI window.
545 # Following creation flags might create a console GUI window.
545 # Using subprocess.CREATE_NEW_CONSOLE might helps.
546 # Using subprocess.CREATE_NEW_CONSOLE might helps.
546 # See https://phab.mercurial-scm.org/D1701 for discussion
547 # See https://phab.mercurial-scm.org/D1701 for discussion
547 _creationflags = (
548 _creationflags = (
548 DETACHED_PROCESS
549 DETACHED_PROCESS
549 | subprocess.CREATE_NEW_PROCESS_GROUP # pytype: disable=module-attr
550 | subprocess.CREATE_NEW_PROCESS_GROUP # pytype: disable=module-attr
550 )
551 )
551
552
552 def runbgcommand(
553 def runbgcommand(
553 script,
554 script,
554 env,
555 env,
555 shell=False,
556 shell=False,
556 stdout=None,
557 stdout=None,
557 stderr=None,
558 stderr=None,
558 ensurestart=True,
559 ensurestart=True,
559 record_wait=None,
560 record_wait=None,
560 ):
561 ):
561 '''Spawn a command without waiting for it to finish.'''
562 '''Spawn a command without waiting for it to finish.'''
562 # we can't use close_fds *and* redirect stdin. I'm not sure that we
563 # we can't use close_fds *and* redirect stdin. I'm not sure that we
563 # need to because the detached process has no console connection.
564 # need to because the detached process has no console connection.
564 p = subprocess.Popen(
565 p = subprocess.Popen(
565 tonativestr(script),
566 tonativestr(script),
566 shell=shell,
567 shell=shell,
567 env=tonativeenv(env),
568 env=tonativeenv(env),
568 close_fds=True,
569 close_fds=True,
569 creationflags=_creationflags,
570 creationflags=_creationflags,
570 stdout=stdout,
571 stdout=stdout,
571 stderr=stderr,
572 stderr=stderr,
572 )
573 )
573 if record_wait is not None:
574 if record_wait is not None:
574 record_wait(p.wait)
575 record_wait(p.wait)
575
576
576
577
577 else:
578 else:
578
579
579 def runbgcommand(
580 def runbgcommand(
580 cmd,
581 cmd,
581 env,
582 env,
582 shell=False,
583 shell=False,
583 stdout=None,
584 stdout=None,
584 stderr=None,
585 stderr=None,
585 ensurestart=True,
586 ensurestart=True,
586 record_wait=None,
587 record_wait=None,
587 ):
588 ):
588 '''Spawn a command without waiting for it to finish.
589 '''Spawn a command without waiting for it to finish.
589
590
590
591
591 When `record_wait` is not None, the spawned process will not be fully
592 When `record_wait` is not None, the spawned process will not be fully
592 detached and the `record_wait` argument will be called with a the
593 detached and the `record_wait` argument will be called with a the
593 `Subprocess.wait` function for the spawned process. This is mostly
594 `Subprocess.wait` function for the spawned process. This is mostly
594 useful for developers that need to make sure the spawned process
595 useful for developers that need to make sure the spawned process
595 finished before a certain point. (eg: writing test)'''
596 finished before a certain point. (eg: writing test)'''
596 if pycompat.isdarwin:
597 if pycompat.isdarwin:
597 # avoid crash in CoreFoundation in case another thread
598 # avoid crash in CoreFoundation in case another thread
598 # calls gui() while we're calling fork().
599 # calls gui() while we're calling fork().
599 gui()
600 gui()
600
601
601 # double-fork to completely detach from the parent process
602 # double-fork to completely detach from the parent process
602 # based on http://code.activestate.com/recipes/278731
603 # based on http://code.activestate.com/recipes/278731
603 if record_wait is None:
604 if record_wait is None:
604 pid = os.fork()
605 pid = os.fork()
605 if pid:
606 if pid:
606 if not ensurestart:
607 if not ensurestart:
608 # Even though we're not waiting on the child process,
609 # we still must call waitpid() on it at some point so
610 # it's not a zombie/defunct. This is especially relevant for
611 # chg since the parent process won't die anytime soon.
612 # We use a thread to make the overhead tiny.
613 def _do_wait():
614 os.waitpid(pid, 0)
615 threading.Thread(target=_do_wait, daemon=True).start()
607 return
616 return
608 # Parent process
617 # Parent process
609 (_pid, status) = os.waitpid(pid, 0)
618 (_pid, status) = os.waitpid(pid, 0)
610 if os.WIFEXITED(status):
619 if os.WIFEXITED(status):
611 returncode = os.WEXITSTATUS(status)
620 returncode = os.WEXITSTATUS(status)
612 else:
621 else:
613 returncode = -(os.WTERMSIG(status))
622 returncode = -(os.WTERMSIG(status))
614 if returncode != 0:
623 if returncode != 0:
615 # The child process's return code is 0 on success, an errno
624 # The child process's return code is 0 on success, an errno
616 # value on failure, or 255 if we don't have a valid errno
625 # value on failure, or 255 if we don't have a valid errno
617 # value.
626 # value.
618 #
627 #
619 # (It would be slightly nicer to return the full exception info
628 # (It would be slightly nicer to return the full exception info
620 # over a pipe as the subprocess module does. For now it
629 # over a pipe as the subprocess module does. For now it
621 # doesn't seem worth adding that complexity here, though.)
630 # doesn't seem worth adding that complexity here, though.)
622 if returncode == 255:
631 if returncode == 255:
623 returncode = errno.EINVAL
632 returncode = errno.EINVAL
624 raise OSError(
633 raise OSError(
625 returncode,
634 returncode,
626 b'error running %r: %s'
635 b'error running %r: %s'
627 % (cmd, os.strerror(returncode)),
636 % (cmd, os.strerror(returncode)),
628 )
637 )
629 return
638 return
630
639
631 returncode = 255
640 returncode = 255
632 try:
641 try:
633 if record_wait is None:
642 if record_wait is None:
634 # Start a new session
643 # Start a new session
635 os.setsid()
644 os.setsid()
636
645
637 stdin = open(os.devnull, b'r')
646 stdin = open(os.devnull, b'r')
638 if stdout is None:
647 if stdout is None:
639 stdout = open(os.devnull, b'w')
648 stdout = open(os.devnull, b'w')
640 if stderr is None:
649 if stderr is None:
641 stderr = open(os.devnull, b'w')
650 stderr = open(os.devnull, b'w')
642
651
643 # connect stdin to devnull to make sure the subprocess can't
652 # connect stdin to devnull to make sure the subprocess can't
644 # muck up that stream for mercurial.
653 # muck up that stream for mercurial.
645 p = subprocess.Popen(
654 p = subprocess.Popen(
646 cmd,
655 cmd,
647 shell=shell,
656 shell=shell,
648 env=env,
657 env=env,
649 close_fds=True,
658 close_fds=True,
650 stdin=stdin,
659 stdin=stdin,
651 stdout=stdout,
660 stdout=stdout,
652 stderr=stderr,
661 stderr=stderr,
653 )
662 )
654 if record_wait is not None:
663 if record_wait is not None:
655 record_wait(p.wait)
664 record_wait(p.wait)
656 returncode = 0
665 returncode = 0
657 except EnvironmentError as ex:
666 except EnvironmentError as ex:
658 returncode = ex.errno & 0xFF
667 returncode = ex.errno & 0xFF
659 if returncode == 0:
668 if returncode == 0:
660 # This shouldn't happen, but just in case make sure the
669 # This shouldn't happen, but just in case make sure the
661 # return code is never 0 here.
670 # return code is never 0 here.
662 returncode = 255
671 returncode = 255
663 except Exception:
672 except Exception:
664 returncode = 255
673 returncode = 255
665 finally:
674 finally:
666 # mission accomplished, this child needs to exit and not
675 # mission accomplished, this child needs to exit and not
667 # continue the hg process here.
676 # continue the hg process here.
668 if record_wait is None:
677 if record_wait is None:
669 os._exit(returncode)
678 os._exit(returncode)
General Comments 0
You need to be logged in to leave comments. Login now