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