##// END OF EJS Templates
procutil: unroll uin/uout loop in protectstdio()...
Yuya Nishihara -
r37236:ac71cbad default
parent child Browse files
Show More
@@ -1,358 +1,359
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 imp
14 14 import io
15 15 import os
16 16 import signal
17 17 import subprocess
18 18 import sys
19 19 import tempfile
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
46 46 if isatty(stdout):
47 47 stdout = os.fdopen(stdout.fileno(), r'wb', 1)
48 48
49 49 if pycompat.iswindows:
50 50 from .. import windows as platform
51 51 stdout = platform.winstdout(stdout)
52 52 else:
53 53 from .. import posix as platform
54 54
55 55 explainexit = platform.explainexit
56 56 findexe = platform.findexe
57 57 _gethgcmd = platform.gethgcmd
58 58 getuser = platform.getuser
59 59 getpid = os.getpid
60 60 hidewindow = platform.hidewindow
61 61 popen = platform.popen
62 62 quotecommand = platform.quotecommand
63 63 readpipe = platform.readpipe
64 64 setbinary = platform.setbinary
65 65 setsignalhandler = platform.setsignalhandler
66 66 shellquote = platform.shellquote
67 67 shellsplit = platform.shellsplit
68 68 spawndetached = platform.spawndetached
69 69 sshargs = platform.sshargs
70 70 testpid = platform.testpid
71 71
72 72 try:
73 73 setprocname = osutil.setprocname
74 74 except AttributeError:
75 75 pass
76 76 try:
77 77 unblocksignal = osutil.unblocksignal
78 78 except AttributeError:
79 79 pass
80 80
81 81 closefds = pycompat.isposix
82 82
83 83 def popen2(cmd, env=None, newlines=False):
84 84 # Setting bufsize to -1 lets the system decide the buffer size.
85 85 # The default for bufsize is 0, meaning unbuffered. This leads to
86 86 # poor performance on Mac OS X: http://bugs.python.org/issue4194
87 87 p = subprocess.Popen(cmd, shell=True, bufsize=-1,
88 88 close_fds=closefds,
89 89 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
90 90 universal_newlines=newlines,
91 91 env=env)
92 92 return p.stdin, p.stdout
93 93
94 94 def popen3(cmd, env=None, newlines=False):
95 95 stdin, stdout, stderr, p = popen4(cmd, env, newlines)
96 96 return stdin, stdout, stderr
97 97
98 98 def popen4(cmd, env=None, newlines=False, bufsize=-1):
99 99 p = subprocess.Popen(cmd, shell=True, bufsize=bufsize,
100 100 close_fds=closefds,
101 101 stdin=subprocess.PIPE, stdout=subprocess.PIPE,
102 102 stderr=subprocess.PIPE,
103 103 universal_newlines=newlines,
104 104 env=env)
105 105 return p.stdin, p.stdout, p.stderr, p
106 106
107 107 def pipefilter(s, cmd):
108 108 '''filter string S through command CMD, returning its output'''
109 109 p = subprocess.Popen(cmd, shell=True, close_fds=closefds,
110 110 stdin=subprocess.PIPE, stdout=subprocess.PIPE)
111 111 pout, perr = p.communicate(s)
112 112 return pout
113 113
114 114 def tempfilter(s, cmd):
115 115 '''filter string S through a pair of temporary files with CMD.
116 116 CMD is used as a template to create the real command to be run,
117 117 with the strings INFILE and OUTFILE replaced by the real names of
118 118 the temporary files generated.'''
119 119 inname, outname = None, None
120 120 try:
121 121 infd, inname = tempfile.mkstemp(prefix='hg-filter-in-')
122 122 fp = os.fdopen(infd, r'wb')
123 123 fp.write(s)
124 124 fp.close()
125 125 outfd, outname = tempfile.mkstemp(prefix='hg-filter-out-')
126 126 os.close(outfd)
127 127 cmd = cmd.replace('INFILE', inname)
128 128 cmd = cmd.replace('OUTFILE', outname)
129 129 code = os.system(cmd)
130 130 if pycompat.sysplatform == 'OpenVMS' and code & 1:
131 131 code = 0
132 132 if code:
133 133 raise error.Abort(_("command '%s' failed: %s") %
134 134 (cmd, explainexit(code)))
135 135 with open(outname, 'rb') as fp:
136 136 return fp.read()
137 137 finally:
138 138 try:
139 139 if inname:
140 140 os.unlink(inname)
141 141 except OSError:
142 142 pass
143 143 try:
144 144 if outname:
145 145 os.unlink(outname)
146 146 except OSError:
147 147 pass
148 148
149 149 _filtertable = {
150 150 'tempfile:': tempfilter,
151 151 'pipe:': pipefilter,
152 152 }
153 153
154 154 def filter(s, cmd):
155 155 "filter a string through a command that transforms its input to its output"
156 156 for name, fn in _filtertable.iteritems():
157 157 if cmd.startswith(name):
158 158 return fn(s, cmd[len(name):].lstrip())
159 159 return pipefilter(s, cmd)
160 160
161 161 def mainfrozen():
162 162 """return True if we are a frozen executable.
163 163
164 164 The code supports py2exe (most common, Windows only) and tools/freeze
165 165 (portable, not much used).
166 166 """
167 167 return (pycompat.safehasattr(sys, "frozen") or # new py2exe
168 168 pycompat.safehasattr(sys, "importers") or # old py2exe
169 169 imp.is_frozen(u"__main__")) # tools/freeze
170 170
171 171 _hgexecutable = None
172 172
173 173 def hgexecutable():
174 174 """return location of the 'hg' executable.
175 175
176 176 Defaults to $HG or 'hg' in the search path.
177 177 """
178 178 if _hgexecutable is None:
179 179 hg = encoding.environ.get('HG')
180 180 mainmod = sys.modules[r'__main__']
181 181 if hg:
182 182 _sethgexecutable(hg)
183 183 elif mainfrozen():
184 184 if getattr(sys, 'frozen', None) == 'macosx_app':
185 185 # Env variable set by py2app
186 186 _sethgexecutable(encoding.environ['EXECUTABLEPATH'])
187 187 else:
188 188 _sethgexecutable(pycompat.sysexecutable)
189 189 elif (os.path.basename(
190 190 pycompat.fsencode(getattr(mainmod, '__file__', ''))) == 'hg'):
191 191 _sethgexecutable(pycompat.fsencode(mainmod.__file__))
192 192 else:
193 193 exe = findexe('hg') or os.path.basename(sys.argv[0])
194 194 _sethgexecutable(exe)
195 195 return _hgexecutable
196 196
197 197 def _sethgexecutable(path):
198 198 """set location of the 'hg' executable"""
199 199 global _hgexecutable
200 200 _hgexecutable = path
201 201
202 202 def _testfileno(f, stdf):
203 203 fileno = getattr(f, 'fileno', None)
204 204 try:
205 205 return fileno and fileno() == stdf.fileno()
206 206 except io.UnsupportedOperation:
207 207 return False # fileno() raised UnsupportedOperation
208 208
209 209 def isstdin(f):
210 210 return _testfileno(f, sys.__stdin__)
211 211
212 212 def isstdout(f):
213 213 return _testfileno(f, sys.__stdout__)
214 214
215 215 def protectstdio(uin, uout):
216 216 """Duplicate streams and redirect original to null if (uin, uout) are
217 217 stdio
218 218
219 219 Returns (fin, fout) which point to the original (uin, uout) fds, but
220 220 may be copy of (uin, uout). The returned streams can be considered
221 221 "owned" in that print(), exec(), etc. never reach to them.
222 222 """
223 223 uout.flush()
224 newfiles = []
225 224 nullfd = os.open(os.devnull, os.O_RDWR)
226 for f, sysf, mode in [(uin, stdin, r'rb'),
227 (uout, stdout, r'wb')]:
228 if f is sysf:
229 newfd = os.dup(f.fileno())
230 os.dup2(nullfd, f.fileno())
231 f = os.fdopen(newfd, mode)
232 newfiles.append(f)
225 fin, fout = uin, uout
226 if uin is stdin:
227 newfd = os.dup(uin.fileno())
228 os.dup2(nullfd, uin.fileno())
229 fin = os.fdopen(newfd, r'rb')
230 if uout is stdout:
231 newfd = os.dup(uout.fileno())
232 os.dup2(nullfd, uout.fileno())
233 fout = os.fdopen(newfd, r'wb')
233 234 os.close(nullfd)
234 return tuple(newfiles)
235 return fin, fout
235 236
236 237 def restorestdio(uin, uout, fin, fout):
237 238 """Restore (uin, uout) streams from possibly duplicated (fin, fout)"""
238 239 uout.flush()
239 240 for f, uif in [(fin, uin), (fout, uout)]:
240 241 if f is not uif:
241 242 os.dup2(f.fileno(), uif.fileno())
242 243 f.close()
243 244
244 245 @contextlib.contextmanager
245 246 def protectedstdio(uin, uout):
246 247 """Run code block with protected standard streams"""
247 248 fin, fout = protectstdio(uin, uout)
248 249 try:
249 250 yield fin, fout
250 251 finally:
251 252 restorestdio(uin, uout, fin, fout)
252 253
253 254 def shellenviron(environ=None):
254 255 """return environ with optional override, useful for shelling out"""
255 256 def py2shell(val):
256 257 'convert python object into string that is useful to shell'
257 258 if val is None or val is False:
258 259 return '0'
259 260 if val is True:
260 261 return '1'
261 262 return pycompat.bytestr(val)
262 263 env = dict(encoding.environ)
263 264 if environ:
264 265 env.update((k, py2shell(v)) for k, v in environ.iteritems())
265 266 env['HG'] = hgexecutable()
266 267 return env
267 268
268 269 def system(cmd, environ=None, cwd=None, out=None):
269 270 '''enhanced shell command execution.
270 271 run with environment maybe modified, maybe in different dir.
271 272
272 273 if out is specified, it is assumed to be a file-like object that has a
273 274 write() method. stdout and stderr will be redirected to out.'''
274 275 try:
275 276 stdout.flush()
276 277 except Exception:
277 278 pass
278 279 cmd = quotecommand(cmd)
279 280 env = shellenviron(environ)
280 281 if out is None or isstdout(out):
281 282 rc = subprocess.call(cmd, shell=True, close_fds=closefds,
282 283 env=env, cwd=cwd)
283 284 else:
284 285 proc = subprocess.Popen(cmd, shell=True, close_fds=closefds,
285 286 env=env, cwd=cwd, stdout=subprocess.PIPE,
286 287 stderr=subprocess.STDOUT)
287 288 for line in iter(proc.stdout.readline, ''):
288 289 out.write(line)
289 290 proc.wait()
290 291 rc = proc.returncode
291 292 if pycompat.sysplatform == 'OpenVMS' and rc & 1:
292 293 rc = 0
293 294 return rc
294 295
295 296 def gui():
296 297 '''Are we running in a GUI?'''
297 298 if pycompat.isdarwin:
298 299 if 'SSH_CONNECTION' in encoding.environ:
299 300 # handle SSH access to a box where the user is logged in
300 301 return False
301 302 elif getattr(osutil, 'isgui', None):
302 303 # check if a CoreGraphics session is available
303 304 return osutil.isgui()
304 305 else:
305 306 # pure build; use a safe default
306 307 return True
307 308 else:
308 309 return pycompat.iswindows or encoding.environ.get("DISPLAY")
309 310
310 311 def hgcmd():
311 312 """Return the command used to execute current hg
312 313
313 314 This is different from hgexecutable() because on Windows we want
314 315 to avoid things opening new shell windows like batch files, so we
315 316 get either the python call or current executable.
316 317 """
317 318 if mainfrozen():
318 319 if getattr(sys, 'frozen', None) == 'macosx_app':
319 320 # Env variable set by py2app
320 321 return [encoding.environ['EXECUTABLEPATH']]
321 322 else:
322 323 return [pycompat.sysexecutable]
323 324 return _gethgcmd()
324 325
325 326 def rundetached(args, condfn):
326 327 """Execute the argument list in a detached process.
327 328
328 329 condfn is a callable which is called repeatedly and should return
329 330 True once the child process is known to have started successfully.
330 331 At this point, the child process PID is returned. If the child
331 332 process fails to start or finishes before condfn() evaluates to
332 333 True, return -1.
333 334 """
334 335 # Windows case is easier because the child process is either
335 336 # successfully starting and validating the condition or exiting
336 337 # on failure. We just poll on its PID. On Unix, if the child
337 338 # process fails to start, it will be left in a zombie state until
338 339 # the parent wait on it, which we cannot do since we expect a long
339 340 # running process on success. Instead we listen for SIGCHLD telling
340 341 # us our child process terminated.
341 342 terminated = set()
342 343 def handler(signum, frame):
343 344 terminated.add(os.wait())
344 345 prevhandler = None
345 346 SIGCHLD = getattr(signal, 'SIGCHLD', None)
346 347 if SIGCHLD is not None:
347 348 prevhandler = signal.signal(SIGCHLD, handler)
348 349 try:
349 350 pid = spawndetached(args)
350 351 while not condfn():
351 352 if ((pid in terminated or not testpid(pid))
352 353 and not condfn()):
353 354 return -1
354 355 time.sleep(0.1)
355 356 return pid
356 357 finally:
357 358 if prevhandler is not None:
358 359 signal.signal(signal.SIGCHLD, prevhandler)
General Comments 0
You need to be logged in to leave comments. Login now