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