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