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