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