##// END OF EJS Templates
procutil: make stream detection in make_line_buffered more correct and strict...
Manuel Jacob -
r50273:094a5fa3 6.2 stable
parent child Browse files
Show More
@@ -1,770 +1,790
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 Olivia Mackall <olivia@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
11 11 import contextlib
12 12 import errno
13 13 import io
14 14 import os
15 15 import signal
16 16 import subprocess
17 17 import sys
18 18 import threading
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 if pycompat.iswindows:
40 40 from .. import windows as platform
41 41 else:
42 42 from .. import posix as platform
43 43
44 44
45 45 def isatty(fp):
46 46 try:
47 47 return fp.isatty()
48 48 except AttributeError:
49 49 return False
50 50
51 51
52 52 class BadFile(io.RawIOBase):
53 53 """Dummy file object to simulate closed stdio behavior"""
54 54
55 55 def readinto(self, b):
56 56 raise IOError(errno.EBADF, 'Bad file descriptor')
57 57
58 58 def write(self, b):
59 59 raise IOError(errno.EBADF, 'Bad file descriptor')
60 60
61 61
62 62 class LineBufferedWrapper:
63 63 def __init__(self, orig):
64 64 self.orig = orig
65 65
66 66 def __getattr__(self, attr):
67 67 return getattr(self.orig, attr)
68 68
69 69 def write(self, s):
70 70 orig = self.orig
71 71 res = orig.write(s)
72 72 if s.endswith(b'\n'):
73 73 orig.flush()
74 74 return res
75 75
76 76
77 77 # pytype: disable=attribute-error
78 78 io.BufferedIOBase.register(LineBufferedWrapper)
79 79 # pytype: enable=attribute-error
80 80
81 81
82 82 def make_line_buffered(stream):
83 if not isinstance(stream, io.BufferedIOBase):
84 # On Python 3, buffered streams can be expected to subclass
85 # BufferedIOBase. This is definitively the case for the streams
86 # initialized by the interpreter. For unbuffered streams, we don't need
87 # to emulate line buffering.
83 # First, check if we need to wrap the stream.
84 check_stream = stream
85 while True:
86 if isinstance(check_stream, WriteAllWrapper):
87 check_stream = check_stream.orig
88 elif pycompat.iswindows and isinstance(
89 check_stream,
90 # pytype: disable=module-attr
91 platform.winstdout
92 # pytype: enable=module-attr
93 ):
94 check_stream = check_stream.fp
95 else:
96 break
97 if isinstance(check_stream, io.RawIOBase):
98 # The stream is unbuffered, we don't need to emulate line buffering.
88 99 return stream
100 elif isinstance(check_stream, io.BufferedIOBase):
101 # The stream supports some kind of buffering. We can't assume that
102 # lines are flushed. Fall back to wrapping the stream.
103 pass
104 else:
105 raise NotImplementedError(
106 "can't determine whether stream is buffered or not"
107 )
108
89 109 if isinstance(stream, LineBufferedWrapper):
90 110 return stream
91 111 return LineBufferedWrapper(stream)
92 112
93 113
94 114 def unwrap_line_buffered(stream):
95 115 if isinstance(stream, LineBufferedWrapper):
96 116 assert not isinstance(stream.orig, LineBufferedWrapper)
97 117 return stream.orig
98 118 return stream
99 119
100 120
101 121 class WriteAllWrapper:
102 122 def __init__(self, orig):
103 123 self.orig = orig
104 124
105 125 def __getattr__(self, attr):
106 126 return getattr(self.orig, attr)
107 127
108 128 def write(self, s):
109 129 write1 = self.orig.write
110 130 m = memoryview(s)
111 131 total_to_write = len(s)
112 132 total_written = 0
113 133 while total_written < total_to_write:
114 134 c = write1(m[total_written:])
115 135 if c:
116 136 total_written += c
117 137 return total_written
118 138
119 139
120 140 # pytype: disable=attribute-error
121 141 io.IOBase.register(WriteAllWrapper)
122 142 # pytype: enable=attribute-error
123 143
124 144
125 145 def _make_write_all(stream):
126 146 if isinstance(stream, WriteAllWrapper):
127 147 return stream
128 148 if isinstance(stream, io.BufferedIOBase):
129 149 # The io.BufferedIOBase.write() contract guarantees that all data is
130 150 # written.
131 151 return stream
132 152 # In general, the write() method of streams is free to write only part of
133 153 # the data.
134 154 return WriteAllWrapper(stream)
135 155
136 156
137 157 # Python 3 implements its own I/O streams. Unlike stdio of C library,
138 158 # sys.stdin/stdout/stderr may be None if underlying fd is closed.
139 159
140 160 # TODO: .buffer might not exist if std streams were replaced; we'll need
141 161 # a silly wrapper to make a bytes stream backed by a unicode one.
142 162
143 163 if sys.stdin is None:
144 164 stdin = BadFile()
145 165 else:
146 166 stdin = sys.stdin.buffer
147 167 if sys.stdout is None:
148 168 stdout = BadFile()
149 169 else:
150 170 stdout = _make_write_all(sys.stdout.buffer)
151 171 if sys.stderr is None:
152 172 stderr = BadFile()
153 173 else:
154 174 stderr = _make_write_all(sys.stderr.buffer)
155 175
156 176 if pycompat.iswindows:
157 177 # Work around Windows bugs.
158 178 stdout = platform.winstdout(stdout) # pytype: disable=module-attr
159 179 stderr = platform.winstdout(stderr) # pytype: disable=module-attr
160 180 if isatty(stdout):
161 181 # The standard library doesn't offer line-buffered binary streams.
162 182 stdout = make_line_buffered(stdout)
163 183
164 184 findexe = platform.findexe
165 185 _gethgcmd = platform.gethgcmd
166 186 getuser = platform.getuser
167 187 getpid = os.getpid
168 188 hidewindow = platform.hidewindow
169 189 readpipe = platform.readpipe
170 190 setbinary = platform.setbinary
171 191 setsignalhandler = platform.setsignalhandler
172 192 shellquote = platform.shellquote
173 193 shellsplit = platform.shellsplit
174 194 spawndetached = platform.spawndetached
175 195 sshargs = platform.sshargs
176 196 testpid = platform.testpid
177 197
178 198 try:
179 199 setprocname = osutil.setprocname
180 200 except AttributeError:
181 201 pass
182 202 try:
183 203 unblocksignal = osutil.unblocksignal
184 204 except AttributeError:
185 205 pass
186 206
187 207 closefds = pycompat.isposix
188 208
189 209
190 210 def explainexit(code):
191 211 """return a message describing a subprocess status
192 212 (codes from kill are negative - not os.system/wait encoding)"""
193 213 if code >= 0:
194 214 return _(b"exited with status %d") % code
195 215 return _(b"killed by signal %d") % -code
196 216
197 217
198 218 class _pfile:
199 219 """File-like wrapper for a stream opened by subprocess.Popen()"""
200 220
201 221 def __init__(self, proc, fp):
202 222 self._proc = proc
203 223 self._fp = fp
204 224
205 225 def close(self):
206 226 # unlike os.popen(), this returns an integer in subprocess coding
207 227 self._fp.close()
208 228 return self._proc.wait()
209 229
210 230 def __iter__(self):
211 231 return iter(self._fp)
212 232
213 233 def __getattr__(self, attr):
214 234 return getattr(self._fp, attr)
215 235
216 236 def __enter__(self):
217 237 return self
218 238
219 239 def __exit__(self, exc_type, exc_value, exc_tb):
220 240 self.close()
221 241
222 242
223 243 def popen(cmd, mode=b'rb', bufsize=-1):
224 244 if mode == b'rb':
225 245 return _popenreader(cmd, bufsize)
226 246 elif mode == b'wb':
227 247 return _popenwriter(cmd, bufsize)
228 248 raise error.ProgrammingError(b'unsupported mode: %r' % mode)
229 249
230 250
231 251 def _popenreader(cmd, bufsize):
232 252 p = subprocess.Popen(
233 253 tonativestr(cmd),
234 254 shell=True,
235 255 bufsize=bufsize,
236 256 close_fds=closefds,
237 257 stdout=subprocess.PIPE,
238 258 )
239 259 return _pfile(p, p.stdout)
240 260
241 261
242 262 def _popenwriter(cmd, bufsize):
243 263 p = subprocess.Popen(
244 264 tonativestr(cmd),
245 265 shell=True,
246 266 bufsize=bufsize,
247 267 close_fds=closefds,
248 268 stdin=subprocess.PIPE,
249 269 )
250 270 return _pfile(p, p.stdin)
251 271
252 272
253 273 def popen2(cmd, env=None):
254 274 # Setting bufsize to -1 lets the system decide the buffer size.
255 275 # The default for bufsize is 0, meaning unbuffered. This leads to
256 276 # poor performance on Mac OS X: http://bugs.python.org/issue4194
257 277 p = subprocess.Popen(
258 278 tonativestr(cmd),
259 279 shell=True,
260 280 bufsize=-1,
261 281 close_fds=closefds,
262 282 stdin=subprocess.PIPE,
263 283 stdout=subprocess.PIPE,
264 284 env=tonativeenv(env),
265 285 )
266 286 return p.stdin, p.stdout
267 287
268 288
269 289 def popen3(cmd, env=None):
270 290 stdin, stdout, stderr, p = popen4(cmd, env)
271 291 return stdin, stdout, stderr
272 292
273 293
274 294 def popen4(cmd, env=None, bufsize=-1):
275 295 p = subprocess.Popen(
276 296 tonativestr(cmd),
277 297 shell=True,
278 298 bufsize=bufsize,
279 299 close_fds=closefds,
280 300 stdin=subprocess.PIPE,
281 301 stdout=subprocess.PIPE,
282 302 stderr=subprocess.PIPE,
283 303 env=tonativeenv(env),
284 304 )
285 305 return p.stdin, p.stdout, p.stderr, p
286 306
287 307
288 308 def pipefilter(s, cmd):
289 309 '''filter string S through command CMD, returning its output'''
290 310 p = subprocess.Popen(
291 311 tonativestr(cmd),
292 312 shell=True,
293 313 close_fds=closefds,
294 314 stdin=subprocess.PIPE,
295 315 stdout=subprocess.PIPE,
296 316 )
297 317 pout, perr = p.communicate(s)
298 318 return pout
299 319
300 320
301 321 def tempfilter(s, cmd):
302 322 """filter string S through a pair of temporary files with CMD.
303 323 CMD is used as a template to create the real command to be run,
304 324 with the strings INFILE and OUTFILE replaced by the real names of
305 325 the temporary files generated."""
306 326 inname, outname = None, None
307 327 try:
308 328 infd, inname = pycompat.mkstemp(prefix=b'hg-filter-in-')
309 329 fp = os.fdopen(infd, 'wb')
310 330 fp.write(s)
311 331 fp.close()
312 332 outfd, outname = pycompat.mkstemp(prefix=b'hg-filter-out-')
313 333 os.close(outfd)
314 334 cmd = cmd.replace(b'INFILE', inname)
315 335 cmd = cmd.replace(b'OUTFILE', outname)
316 336 code = system(cmd)
317 337 if pycompat.sysplatform == b'OpenVMS' and code & 1:
318 338 code = 0
319 339 if code:
320 340 raise error.Abort(
321 341 _(b"command '%s' failed: %s") % (cmd, explainexit(code))
322 342 )
323 343 with open(outname, b'rb') as fp:
324 344 return fp.read()
325 345 finally:
326 346 try:
327 347 if inname:
328 348 os.unlink(inname)
329 349 except OSError:
330 350 pass
331 351 try:
332 352 if outname:
333 353 os.unlink(outname)
334 354 except OSError:
335 355 pass
336 356
337 357
338 358 _filtertable = {
339 359 b'tempfile:': tempfilter,
340 360 b'pipe:': pipefilter,
341 361 }
342 362
343 363
344 364 def filter(s, cmd):
345 365 """filter a string through a command that transforms its input to its
346 366 output"""
347 367 for name, fn in _filtertable.items():
348 368 if cmd.startswith(name):
349 369 return fn(s, cmd[len(name) :].lstrip())
350 370 return pipefilter(s, cmd)
351 371
352 372
353 373 _hgexecutable = None
354 374
355 375
356 376 def hgexecutable():
357 377 """return location of the 'hg' executable.
358 378
359 379 Defaults to $HG or 'hg' in the search path.
360 380 """
361 381 if _hgexecutable is None:
362 382 hg = encoding.environ.get(b'HG')
363 383 mainmod = sys.modules['__main__']
364 384 if hg:
365 385 _sethgexecutable(hg)
366 386 elif resourceutil.mainfrozen():
367 387 if getattr(sys, 'frozen', None) == 'macosx_app':
368 388 # Env variable set by py2app
369 389 _sethgexecutable(encoding.environ[b'EXECUTABLEPATH'])
370 390 else:
371 391 _sethgexecutable(pycompat.sysexecutable)
372 392 elif (
373 393 not pycompat.iswindows
374 394 and os.path.basename(getattr(mainmod, '__file__', '')) == 'hg'
375 395 ):
376 396 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
377 397 else:
378 398 _sethgexecutable(
379 399 findexe(b'hg') or os.path.basename(pycompat.sysargv[0])
380 400 )
381 401 return _hgexecutable
382 402
383 403
384 404 def _sethgexecutable(path):
385 405 """set location of the 'hg' executable"""
386 406 global _hgexecutable
387 407 _hgexecutable = path
388 408
389 409
390 410 def _testfileno(f, stdf):
391 411 fileno = getattr(f, 'fileno', None)
392 412 try:
393 413 return fileno and fileno() == stdf.fileno()
394 414 except io.UnsupportedOperation:
395 415 return False # fileno() raised UnsupportedOperation
396 416
397 417
398 418 def isstdin(f):
399 419 return _testfileno(f, sys.__stdin__)
400 420
401 421
402 422 def isstdout(f):
403 423 return _testfileno(f, sys.__stdout__)
404 424
405 425
406 426 def protectstdio(uin, uout):
407 427 """Duplicate streams and redirect original if (uin, uout) are stdio
408 428
409 429 If uin is stdin, it's redirected to /dev/null. If uout is stdout, it's
410 430 redirected to stderr so the output is still readable.
411 431
412 432 Returns (fin, fout) which point to the original (uin, uout) fds, but
413 433 may be copy of (uin, uout). The returned streams can be considered
414 434 "owned" in that print(), exec(), etc. never reach to them.
415 435 """
416 436 uout.flush()
417 437 fin, fout = uin, uout
418 438 if _testfileno(uin, stdin):
419 439 newfd = os.dup(uin.fileno())
420 440 nullfd = os.open(os.devnull, os.O_RDONLY)
421 441 os.dup2(nullfd, uin.fileno())
422 442 os.close(nullfd)
423 443 fin = os.fdopen(newfd, 'rb')
424 444 if _testfileno(uout, stdout):
425 445 newfd = os.dup(uout.fileno())
426 446 os.dup2(stderr.fileno(), uout.fileno())
427 447 fout = os.fdopen(newfd, 'wb')
428 448 return fin, fout
429 449
430 450
431 451 def restorestdio(uin, uout, fin, fout):
432 452 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
433 453 uout.flush()
434 454 for f, uif in [(fin, uin), (fout, uout)]:
435 455 if f is not uif:
436 456 os.dup2(f.fileno(), uif.fileno())
437 457 f.close()
438 458
439 459
440 460 def shellenviron(environ=None):
441 461 """return environ with optional override, useful for shelling out"""
442 462
443 463 def py2shell(val):
444 464 """convert python object into string that is useful to shell"""
445 465 if val is None or val is False:
446 466 return b'0'
447 467 if val is True:
448 468 return b'1'
449 469 return pycompat.bytestr(val)
450 470
451 471 env = dict(encoding.environ)
452 472 if environ:
453 473 env.update((k, py2shell(v)) for k, v in environ.items())
454 474 env[b'HG'] = hgexecutable()
455 475 return env
456 476
457 477
458 478 if pycompat.iswindows:
459 479
460 480 def shelltonative(cmd, env):
461 481 return platform.shelltocmdexe( # pytype: disable=module-attr
462 482 cmd, shellenviron(env)
463 483 )
464 484
465 485 tonativestr = encoding.strfromlocal
466 486 else:
467 487
468 488 def shelltonative(cmd, env):
469 489 return cmd
470 490
471 491 tonativestr = pycompat.identity
472 492
473 493
474 494 def tonativeenv(env):
475 495 """convert the environment from bytes to strings suitable for Popen(), etc."""
476 496 return pycompat.rapply(tonativestr, env)
477 497
478 498
479 499 def system(cmd, environ=None, cwd=None, out=None):
480 500 """enhanced shell command execution.
481 501 run with environment maybe modified, maybe in different dir.
482 502
483 503 if out is specified, it is assumed to be a file-like object that has a
484 504 write() method. stdout and stderr will be redirected to out."""
485 505 try:
486 506 stdout.flush()
487 507 except Exception:
488 508 pass
489 509 env = shellenviron(environ)
490 510 if out is None or isstdout(out):
491 511 rc = subprocess.call(
492 512 tonativestr(cmd),
493 513 shell=True,
494 514 close_fds=closefds,
495 515 env=tonativeenv(env),
496 516 cwd=pycompat.rapply(tonativestr, cwd),
497 517 )
498 518 else:
499 519 proc = subprocess.Popen(
500 520 tonativestr(cmd),
501 521 shell=True,
502 522 close_fds=closefds,
503 523 env=tonativeenv(env),
504 524 cwd=pycompat.rapply(tonativestr, cwd),
505 525 stdout=subprocess.PIPE,
506 526 stderr=subprocess.STDOUT,
507 527 )
508 528 for line in iter(proc.stdout.readline, b''):
509 529 out.write(line)
510 530 proc.wait()
511 531 rc = proc.returncode
512 532 if pycompat.sysplatform == b'OpenVMS' and rc & 1:
513 533 rc = 0
514 534 return rc
515 535
516 536
517 537 _is_gui = None
518 538
519 539
520 540 def _gui():
521 541 '''Are we running in a GUI?'''
522 542 if pycompat.isdarwin:
523 543 if b'SSH_CONNECTION' in encoding.environ:
524 544 # handle SSH access to a box where the user is logged in
525 545 return False
526 546 elif getattr(osutil, 'isgui', None):
527 547 # check if a CoreGraphics session is available
528 548 return osutil.isgui()
529 549 else:
530 550 # pure build; use a safe default
531 551 return True
532 552 else:
533 553 return (
534 554 pycompat.iswindows
535 555 or encoding.environ.get(b"DISPLAY")
536 556 or encoding.environ.get(b"WAYLAND_DISPLAY")
537 557 )
538 558
539 559
540 560 def gui():
541 561 global _is_gui
542 562 if _is_gui is None:
543 563 _is_gui = _gui()
544 564 return _is_gui
545 565
546 566
547 567 def hgcmd():
548 568 """Return the command used to execute current hg
549 569
550 570 This is different from hgexecutable() because on Windows we want
551 571 to avoid things opening new shell windows like batch files, so we
552 572 get either the python call or current executable.
553 573 """
554 574 if resourceutil.mainfrozen():
555 575 if getattr(sys, 'frozen', None) == 'macosx_app':
556 576 # Env variable set by py2app
557 577 return [encoding.environ[b'EXECUTABLEPATH']]
558 578 else:
559 579 return [pycompat.sysexecutable]
560 580 return _gethgcmd()
561 581
562 582
563 583 def rundetached(args, condfn):
564 584 """Execute the argument list in a detached process.
565 585
566 586 condfn is a callable which is called repeatedly and should return
567 587 True once the child process is known to have started successfully.
568 588 At this point, the child process PID is returned. If the child
569 589 process fails to start or finishes before condfn() evaluates to
570 590 True, return -1.
571 591 """
572 592 # Windows case is easier because the child process is either
573 593 # successfully starting and validating the condition or exiting
574 594 # on failure. We just poll on its PID. On Unix, if the child
575 595 # process fails to start, it will be left in a zombie state until
576 596 # the parent wait on it, which we cannot do since we expect a long
577 597 # running process on success. Instead we listen for SIGCHLD telling
578 598 # us our child process terminated.
579 599 terminated = set()
580 600
581 601 def handler(signum, frame):
582 602 terminated.add(os.wait())
583 603
584 604 prevhandler = None
585 605 SIGCHLD = getattr(signal, 'SIGCHLD', None)
586 606 if SIGCHLD is not None:
587 607 prevhandler = signal.signal(SIGCHLD, handler)
588 608 try:
589 609 pid = spawndetached(args)
590 610 while not condfn():
591 611 if (pid in terminated or not testpid(pid)) and not condfn():
592 612 return -1
593 613 time.sleep(0.1)
594 614 return pid
595 615 finally:
596 616 if prevhandler is not None:
597 617 signal.signal(signal.SIGCHLD, prevhandler)
598 618
599 619
600 620 @contextlib.contextmanager
601 621 def uninterruptible(warn):
602 622 """Inhibit SIGINT handling on a region of code.
603 623
604 624 Note that if this is called in a non-main thread, it turns into a no-op.
605 625
606 626 Args:
607 627 warn: A callable which takes no arguments, and returns True if the
608 628 previous signal handling should be restored.
609 629 """
610 630
611 631 oldsiginthandler = [signal.getsignal(signal.SIGINT)]
612 632 shouldbail = []
613 633
614 634 def disabledsiginthandler(*args):
615 635 if warn():
616 636 signal.signal(signal.SIGINT, oldsiginthandler[0])
617 637 del oldsiginthandler[0]
618 638 shouldbail.append(True)
619 639
620 640 try:
621 641 try:
622 642 signal.signal(signal.SIGINT, disabledsiginthandler)
623 643 except ValueError:
624 644 # wrong thread, oh well, we tried
625 645 del oldsiginthandler[0]
626 646 yield
627 647 finally:
628 648 if oldsiginthandler:
629 649 signal.signal(signal.SIGINT, oldsiginthandler[0])
630 650 if shouldbail:
631 651 raise KeyboardInterrupt
632 652
633 653
634 654 if pycompat.iswindows:
635 655 # no fork on Windows, but we can create a detached process
636 656 # https://msdn.microsoft.com/en-us/library/windows/desktop/ms684863.aspx
637 657 # No stdlib constant exists for this value
638 658 DETACHED_PROCESS = 0x00000008
639 659 # Following creation flags might create a console GUI window.
640 660 # Using subprocess.CREATE_NEW_CONSOLE might helps.
641 661 # See https://phab.mercurial-scm.org/D1701 for discussion
642 662 _creationflags = (
643 663 DETACHED_PROCESS
644 664 | subprocess.CREATE_NEW_PROCESS_GROUP # pytype: disable=module-attr
645 665 )
646 666
647 667 def runbgcommand(
648 668 script,
649 669 env,
650 670 shell=False,
651 671 stdout=None,
652 672 stderr=None,
653 673 ensurestart=True,
654 674 record_wait=None,
655 675 stdin_bytes=None,
656 676 ):
657 677 '''Spawn a command without waiting for it to finish.'''
658 678 # we can't use close_fds *and* redirect stdin. I'm not sure that we
659 679 # need to because the detached process has no console connection.
660 680
661 681 try:
662 682 stdin = None
663 683 if stdin_bytes is not None:
664 684 stdin = pycompat.unnamedtempfile()
665 685 stdin.write(stdin_bytes)
666 686 stdin.flush()
667 687 stdin.seek(0)
668 688
669 689 p = subprocess.Popen(
670 690 pycompat.rapply(tonativestr, script),
671 691 shell=shell,
672 692 env=tonativeenv(env),
673 693 close_fds=True,
674 694 creationflags=_creationflags,
675 695 stdin=stdin,
676 696 stdout=stdout,
677 697 stderr=stderr,
678 698 )
679 699 if record_wait is not None:
680 700 record_wait(p.wait)
681 701 finally:
682 702 if stdin is not None:
683 703 stdin.close()
684 704
685 705
686 706 else:
687 707
688 708 def runbgcommand(
689 709 cmd,
690 710 env,
691 711 shell=False,
692 712 stdout=None,
693 713 stderr=None,
694 714 ensurestart=True,
695 715 record_wait=None,
696 716 stdin_bytes=None,
697 717 ):
698 718 """Spawn a command without waiting for it to finish.
699 719
700 720
701 721 When `record_wait` is not None, the spawned process will not be fully
702 722 detached and the `record_wait` argument will be called with a the
703 723 `Subprocess.wait` function for the spawned process. This is mostly
704 724 useful for developers that need to make sure the spawned process
705 725 finished before a certain point. (eg: writing test)"""
706 726 if pycompat.isdarwin:
707 727 # avoid crash in CoreFoundation in case another thread
708 728 # calls gui() while we're calling fork().
709 729 gui()
710 730
711 731 if shell:
712 732 script = cmd
713 733 else:
714 734 if isinstance(cmd, bytes):
715 735 cmd = [cmd]
716 736 script = b' '.join(shellquote(x) for x in cmd)
717 737 if record_wait is None:
718 738 # double-fork to completely detach from the parent process
719 739 script = b'( %s ) &' % script
720 740 start_new_session = True
721 741 else:
722 742 start_new_session = False
723 743 ensurestart = True
724 744
725 745 stdin = None
726 746
727 747 try:
728 748 if stdin_bytes is None:
729 749 stdin = subprocess.DEVNULL
730 750 else:
731 751 stdin = pycompat.unnamedtempfile()
732 752 stdin.write(stdin_bytes)
733 753 stdin.flush()
734 754 stdin.seek(0)
735 755 if stdout is None:
736 756 stdout = subprocess.DEVNULL
737 757 if stderr is None:
738 758 stderr = subprocess.DEVNULL
739 759
740 760 p = subprocess.Popen(
741 761 script,
742 762 shell=True,
743 763 env=env,
744 764 close_fds=True,
745 765 stdin=stdin,
746 766 stdout=stdout,
747 767 stderr=stderr,
748 768 start_new_session=start_new_session,
749 769 )
750 770 except Exception:
751 771 if record_wait is not None:
752 772 record_wait(255)
753 773 raise
754 774 finally:
755 775 if stdin_bytes is not None and stdin is not None:
756 776 assert not isinstance(stdin, int)
757 777 stdin.close()
758 778 if not ensurestart:
759 779 # Even though we're not waiting on the child process,
760 780 # we still must call waitpid() on it at some point so
761 781 # it's not a zombie/defunct. This is especially relevant for
762 782 # chg since the parent process won't die anytime soon.
763 783 # We use a thread to make the overhead tiny.
764 784 t = threading.Thread(target=lambda: p.wait)
765 785 t.daemon = True
766 786 t.start()
767 787 else:
768 788 returncode = p.wait
769 789 if record_wait is not None:
770 790 record_wait(returncode)
General Comments 0
You need to be logged in to leave comments. Login now