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