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