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