Mercurial > hg
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) |