comparison mercurial/utils/procutil.py @ 37118:5be286db5fb5

procutil: move process/executable management functions to new module std* files, pipe helpers, and findexe()s are moved as well since they are likely to be used with sub processes.
author Yuya Nishihara <yuya@tcha.org>
date Sat, 24 Mar 2018 13:38:04 +0900
parents mercurial/util.py@e7b517809ebc
children 0216232f21ab
comparison
equal deleted inserted replaced
37117:e7b517809ebc 37118:5be286db5fb5
1 # procutil.py - utility for managing processes and executable environment
2 #
3 # Copyright 2005 K. Thananchayan <thananck@yahoo.com>
4 # Copyright 2005-2007 Matt Mackall <mpm@selenic.com>
5 # Copyright 2006 Vadim Gelfer <vadim.gelfer@gmail.com>
6 #
7 # This software may be used and distributed according to the terms of the
8 # GNU General Public License version 2 or any later version.
9
10 from __future__ import absolute_import
11
12 import imp
13 import io
14 import os
15 import signal
16 import subprocess
17 import sys
18 import tempfile
19 import time
20
21 from ..i18n import _
22
23 from .. import (
24 encoding,
25 error,
26 policy,
27 pycompat,
28 )
29
30 osutil = policy.importmod(r'osutil')
31
32 stderr = pycompat.stderr
33 stdin = pycompat.stdin
34 stdout = pycompat.stdout
35
36 def isatty(fp):
37 try:
38 return fp.isatty()
39 except AttributeError:
40 return False
41
42 # glibc determines buffering on first write to stdout - if we replace a TTY
43 # destined stdout with a pipe destined stdout (e.g. pager), we want line
44 # buffering
45 if isatty(stdout):
46 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
47
48 if pycompat.iswindows:
49 from .. import windows as platform
50 stdout = platform.winstdout(stdout)
51 else:
52 from .. import posix as platform
53
54 explainexit = platform.explainexit
55 findexe = platform.findexe
56 _gethgcmd = platform.gethgcmd
57 getuser = platform.getuser
58 getpid = os.getpid
59 hidewindow = platform.hidewindow
60 popen = platform.popen
61 quotecommand = platform.quotecommand
62 readpipe = platform.readpipe
63 setbinary = platform.setbinary
64 setsignalhandler = platform.setsignalhandler
65 shellquote = platform.shellquote
66 shellsplit = platform.shellsplit
67 spawndetached = platform.spawndetached
68 sshargs = platform.sshargs
69 testpid = platform.testpid
70
71 try:
72 setprocname = osutil.setprocname
73 except AttributeError:
74 pass
75 try:
76 unblocksignal = osutil.unblocksignal
77 except AttributeError:
78 pass
79
80 closefds = pycompat.isposix
81
82 def popen2(cmd, env=None, newlines=False):
83 # Setting bufsize to -1 lets the system decide the buffer size.
84 # The default for bufsize is 0, meaning unbuffered. This leads to
85 # poor performance on Mac OS X: http://bugs.python.org/issue4194
86 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
87 close_fds=closefds,
88 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
89 universal_newlines=newlines,
90 env=env)
91 return p.stdin, p.stdout
92
93 def popen3(cmd, env=None, newlines=False):
94 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
95 return stdin, stdout, stderr
96
97 def popen4(cmd, env=None, newlines=False, bufsize=-1):
98 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
99 close_fds=closefds,
100 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
101 stderr=subprocess.PIPE,
102 universal_newlines=newlines,
103 env=env)
104 return p.stdin, p.stdout, p.stderr, p
105
106 def pipefilter(s, cmd):
107 '''filter string S through command CMD, returning its output'''
108 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
109 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
110 pout, perr = p.communicate(s)
111 return pout
112
113 def tempfilter(s, cmd):
114 '''filter string S through a pair of temporary files with CMD.
115 CMD is used as a template to create the real command to be run,
116 with the strings INFILE and OUTFILE replaced by the real names of
117 the temporary files generated.'''
118 inname, outname = None, None
119 try:
120 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
121 fp = os.fdopen(infd, r'wb')
122 fp.write(s)
123 fp.close()
124 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
125 os.close(outfd)
126 cmd = cmd.replace('INFILE', inname)
127 cmd = cmd.replace('OUTFILE', outname)
128 code = os.system(cmd)
129 if pycompat.sysplatform == 'OpenVMS' and code & 1:
130 code = 0
131 if code:
132 raise error.Abort(_("command '%s' failed: %s") %
133 (cmd, explainexit(code)))
134 with open(outname, 'rb') as fp:
135 return fp.read()
136 finally:
137 try:
138 if inname:
139 os.unlink(inname)
140 except OSError:
141 pass
142 try:
143 if outname:
144 os.unlink(outname)
145 except OSError:
146 pass
147
148 _filtertable = {
149 'tempfile:': tempfilter,
150 'pipe:': pipefilter,
151 }
152
153 def filter(s, cmd):
154 "filter a string through a command that transforms its input to its output"
155 for name, fn in _filtertable.iteritems():
156 if cmd.startswith(name):
157 return fn(s, cmd[len(name):].lstrip())
158 return pipefilter(s, cmd)
159
160 def mainfrozen():
161 """return True if we are a frozen executable.
162
163 The code supports py2exe (most common, Windows only) and tools/freeze
164 (portable, not much used).
165 """
166 return (pycompat.safehasattr(sys, "frozen") or # new py2exe
167 pycompat.safehasattr(sys, "importers") or # old py2exe
168 imp.is_frozen(u"__main__")) # tools/freeze
169
170 _hgexecutable = None
171
172 def hgexecutable():
173 """return location of the 'hg' executable.
174
175 Defaults to $HG or 'hg' in the search path.
176 """
177 if _hgexecutable is None:
178 hg = encoding.environ.get('HG')
179 mainmod = sys.modules[r'__main__']
180 if hg:
181 _sethgexecutable(hg)
182 elif mainfrozen():
183 if getattr(sys, 'frozen', None) == 'macosx_app':
184 # Env variable set by py2app
185 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
186 else:
187 _sethgexecutable(pycompat.sysexecutable)
188 elif (os.path.basename(
189 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
190 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
191 else:
192 exe = findexe('hg') or os.path.basename(sys.argv[0])
193 _sethgexecutable(exe)
194 return _hgexecutable
195
196 def _sethgexecutable(path):
197 """set location of the 'hg' executable"""
198 global _hgexecutable
199 _hgexecutable = path
200
201 def _testfileno(f, stdf):
202 fileno = getattr(f, 'fileno', None)
203 try:
204 return fileno and fileno() == stdf.fileno()
205 except io.UnsupportedOperation:
206 return False # fileno() raised UnsupportedOperation
207
208 def isstdin(f):
209 return _testfileno(f, sys.__stdin__)
210
211 def isstdout(f):
212 return _testfileno(f, sys.__stdout__)
213
214 def shellenviron(environ=None):
215 """return environ with optional override, useful for shelling out"""
216 def py2shell(val):
217 'convert python object into string that is useful to shell'
218 if val is None or val is False:
219 return '0'
220 if val is True:
221 return '1'
222 return pycompat.bytestr(val)
223 env = dict(encoding.environ)
224 if environ:
225 env.update((k, py2shell(v)) for k, v in environ.iteritems())
226 env['HG'] = hgexecutable()
227 return env
228
229 def system(cmd, environ=None, cwd=None, out=None):
230 '''enhanced shell command execution.
231 run with environment maybe modified, maybe in different dir.
232
233 if out is specified, it is assumed to be a file-like object that has a
234 write() method. stdout and stderr will be redirected to out.'''
235 try:
236 stdout.flush()
237 except Exception:
238 pass
239 cmd = quotecommand(cmd)
240 env = shellenviron(environ)
241 if out is None or isstdout(out):
242 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
243 env=env, cwd=cwd)
244 else:
245 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
246 env=env, cwd=cwd, stdout=subprocess.PIPE,
247 stderr=subprocess.STDOUT)
248 for line in iter(proc.stdout.readline, ''):
249 out.write(line)
250 proc.wait()
251 rc = proc.returncode
252 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
253 rc = 0
254 return rc
255
256 def gui():
257 '''Are we running in a GUI?'''
258 if pycompat.isdarwin:
259 if 'SSH_CONNECTION' in encoding.environ:
260 # handle SSH access to a box where the user is logged in
261 return False
262 elif getattr(osutil, 'isgui', None):
263 # check if a CoreGraphics session is available
264 return osutil.isgui()
265 else:
266 # pure build; use a safe default
267 return True
268 else:
269 return pycompat.iswindows or encoding.environ.get("DISPLAY")
270
271 def hgcmd():
272 """Return the command used to execute current hg
273
274 This is different from hgexecutable() because on Windows we want
275 to avoid things opening new shell windows like batch files, so we
276 get either the python call or current executable.
277 """
278 if mainfrozen():
279 if getattr(sys, 'frozen', None) == 'macosx_app':
280 # Env variable set by py2app
281 return [encoding.environ['EXECUTABLEPATH']]
282 else:
283 return [pycompat.sysexecutable]
284 return _gethgcmd()
285
286 def rundetached(args, condfn):
287 """Execute the argument list in a detached process.
288
289 condfn is a callable which is called repeatedly and should return
290 True once the child process is known to have started successfully.
291 At this point, the child process PID is returned. If the child
292 process fails to start or finishes before condfn() evaluates to
293 True, return -1.
294 """
295 # Windows case is easier because the child process is either
296 # successfully starting and validating the condition or exiting
297 # on failure. We just poll on its PID. On Unix, if the child
298 # process fails to start, it will be left in a zombie state until
299 # the parent wait on it, which we cannot do since we expect a long
300 # running process on success. Instead we listen for SIGCHLD telling
301 # us our child process terminated.
302 terminated = set()
303 def handler(signum, frame):
304 terminated.add(os.wait())
305 prevhandler = None
306 SIGCHLD = getattr(signal, 'SIGCHLD', None)
307 if SIGCHLD is not None:
308 prevhandler = signal.signal(SIGCHLD, handler)
309 try:
310 pid = spawndetached(args)
311 while not condfn():
312 if ((pid in terminated or not testpid(pid))
313 and not condfn()):
314 return -1
315 time.sleep(0.1)
316 return pid
317 finally:
318 if prevhandler is not None:
319 signal.signal(signal.SIGCHLD, prevhandler)