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