2
Skipping shell invocations is good, when possible. This wrapper around subprocess does dirty work of
3
parsing command lines into argv and making sure that no shell magic is being used.
6
#TODO: ship pyprocessing?
8
import subprocess, shlex, re, logging, sys, traceback, os, imp, glob
9
# XXXkhuey Work around http://bugs.python.org/issue1731717
10
subprocess._cleanup = lambda: None
12
if sys.platform=='win32':
15
_log = logging.getLogger('pymake.process')
17
_escapednewlines = re.compile(r'\\\n')
18
# Characters that most likely indicate a shell script and that native commands
20
_blacklist = re.compile(r'[$><;\[~`|&]' +
21
r'|\${|(?:^|\s){(?:$|\s)') # Blacklist ${foo} and { commands }
22
# Characters that probably indicate a shell script, but that native commands
23
# shouldn't just reject
24
_graylist = re.compile(r'[()]')
25
# Characters that indicate we need to glob
26
_needsglob = re.compile(r'[\*\?]')
28
def clinetoargv(cline, blacklist_gray):
30
If this command line can safely skip the shell, return an argv array.
31
@returns argv, badchar
33
str = _escapednewlines.sub('', cline)
34
m = _blacklist.search(str)
36
return None, m.group(0)
38
m = _graylist.search(str)
40
return None, m.group(0)
42
args = shlex.split(str, comments=True)
44
if len(args) and args[0].find('=') != -1:
49
def doglobbing(args, cwd):
51
Perform any needed globbing on the argument list passed in
55
if _needsglob.search(arg):
56
globbedargs.extend(glob.glob(os.path.join(cwd, arg)))
58
globbedargs.append(arg)
62
shellwords = (':', '.', 'break', 'cd', 'continue', 'exec', 'exit', 'export',
63
'getopts', 'hash', 'pwd', 'readonly', 'return', 'shift',
64
'test', 'times', 'trap', 'umask', 'unset', 'alias',
65
'set', 'bind', 'builtin', 'caller', 'command', 'declare',
66
'echo', 'enable', 'help', 'let', 'local', 'logout',
67
'printf', 'read', 'shopt', 'source', 'type', 'typeset',
68
'ulimit', 'unalias', 'set')
70
def call(cline, env, cwd, loc, cb, context, echo, justprint=False):
71
#TODO: call this once up-front somewhere and save the result?
72
shell, msys = util.checkmsyscompat()
75
if msys and cline.startswith('/'):
76
shellreason = "command starts with /"
78
argv, badchar = clinetoargv(cline, blacklist_gray=True)
80
shellreason = "command contains shell-special character '%s'" % (badchar,)
81
elif len(argv) and argv[0] in shellwords:
82
shellreason = "command starts with shell primitive '%s'" % (argv[0],)
84
argv = doglobbing(argv, cwd)
86
if shellreason is not None:
87
_log.debug("%s: using shell: %s: '%s'", loc, shellreason, cline)
89
if len(cline) > 3 and cline[1] == ':' and cline[2] == '/':
90
cline = '/' + cline[0] + cline[2:]
91
cline = [shell, "-c", cline]
92
context.call(cline, shell=not msys, env=env, cwd=cwd, cb=cb, echo=echo,
100
if argv[0] == command.makepypath:
101
command.main(argv[1:], env, cwd, cb)
104
if argv[0:2] == [sys.executable.replace('\\', '/'),
105
command.makepypath.replace('\\', '/')]:
106
command.main(argv[2:], env, cwd, cb)
109
if argv[0].find('/') != -1:
110
executable = util.normaljoin(cwd, argv[0])
114
context.call(argv, executable=executable, shell=False, env=env, cwd=cwd, cb=cb,
115
echo=echo, justprint=justprint)
117
def call_native(module, method, argv, env, cwd, loc, cb, context, echo, justprint=False,
119
argv = doglobbing(argv, cwd)
120
context.call_native(module, method, argv, env=env, cwd=cwd, cb=cb,
121
echo=echo, justprint=justprint, pycommandpath=pycommandpath)
123
def statustoresult(status):
125
Convert the status returned from waitpid into a prettier numeric result.
135
A single job to be executed on the process pool.
137
done = False # set to true when the job completes
142
def notify(self, condition, result):
145
self.exitcode = result
149
def get_callback(self, condition):
150
return lambda result: self.notify(condition, result)
154
A job that executes a command using subprocess.Popen.
156
def __init__(self, argv, executable, shell, env, cwd):
159
self.executable = executable
163
self.parentpid = os.getpid()
166
assert os.getpid() != self.parentpid
167
# subprocess.Popen doesn't use the PATH set in the env argument for
168
# finding the executable on some platforms (but strangely it does on
169
# others!), so set os.environ['PATH'] explicitly. This is parallel-
170
# safe because pymake uses separate processes for parallelism, and
171
# each process is serial. See http://bugs.python.org/issue8557 for a
172
# general overview of "subprocess PATH semantics and portability".
173
oldpath = os.environ['PATH']
175
if self.env is not None and self.env.has_key('PATH'):
176
os.environ['PATH'] = self.env['PATH']
177
p = subprocess.Popen(self.argv, executable=self.executable, shell=self.shell, env=self.env, cwd=self.cwd)
180
print >>sys.stderr, e
183
os.environ['PATH'] = oldpath
185
class PythonException(Exception):
186
def __init__(self, message, exitcode):
187
Exception.__init__(self)
188
self.message = message
189
self.exitcode = exitcode
194
def load_module_recursive(module, path):
196
Emulate the behavior of __import__, but allow
197
passing a custom path to search for modules.
199
bits = module.split('.')
200
for i, bit in enumerate(bits):
201
dotname = '.'.join(bits[:i+1])
203
f, path, desc = imp.find_module(bit, path)
204
m = imp.load_module(dotname, f, path, desc)
210
class PythonJob(Job):
212
A job that calls a Python method.
214
def __init__(self, module, method, argv, env, cwd, pycommandpath=None):
220
self.pycommandpath = pycommandpath or []
221
self.parentpid = os.getpid()
224
assert os.getpid() != self.parentpid
225
# os.environ is a magic dictionary. Setting it to something else
226
# doesn't affect the environment of subprocesses, so use clear/update
227
oldenv = dict(os.environ)
231
os.environ.update(self.env)
232
if self.module not in sys.modules:
233
load_module_recursive(self.module,
234
sys.path + self.pycommandpath)
235
if self.module not in sys.modules:
236
print >>sys.stderr, "No module named '%s'" % self.module
238
m = sys.modules[self.module]
239
if self.method not in m.__dict__:
240
print >>sys.stderr, "No method named '%s' in module %s" % (self.method, self.module)
242
rv = m.__dict__[self.method](self.argv)
243
if rv != 0 and rv is not None:
244
print >>sys.stderr, (
245
"Native command '%s %s' returned value '%s'" %
246
(self.module, self.method, rv))
247
return (rv if isinstance(rv, int) else 1)
249
except PythonException, e:
250
print >>sys.stderr, e
253
e = sys.exc_info()[1]
254
if isinstance(e, SystemExit) and (e.code == 0 or e.code is None):
255
pass # sys.exit(0) is not a failure
257
print >>sys.stderr, e
258
print >>sys.stderr, traceback.print_exc()
259
return (e.code if isinstance(e.code, int) else 1)
262
os.environ.update(oldenv)
267
Run a job. Called in a Process pool.
271
class ParallelContext(object):
273
Manages the parallel execution of processes.
277
_condition = multiprocessing.Condition()
279
def __init__(self, jcount):
283
self.processpool = multiprocessing.Pool(processes=jcount)
284
self.pending = [] # list of (cb, args, kwargs)
285
self.running = [] # list of (subprocess, cb)
287
self._allcontexts.add(self)
290
assert len(self.pending) == 0 and len(self.running) == 0, "pending: %i running: %i" % (len(self.pending), len(self.running))
291
self.processpool.close()
292
self.processpool.join()
293
self._allcontexts.remove(self)
296
while len(self.pending) and len(self.running) < self.jcount:
297
cb, args, kwargs = self.pending.pop(0)
300
def defer(self, cb, *args, **kwargs):
301
assert self.jcount > 1 or not len(self.pending), "Serial execution error defering %r %r %r: currently pending %r" % (cb, args, kwargs, self.pending)
302
self.pending.append((cb, args, kwargs))
304
def _docall_generic(self, pool, job, cb, echo, justprint):
307
processcb = job.get_callback(ParallelContext._condition)
311
pool.apply_async(job_runner, args=(job,), callback=processcb)
312
self.running.append((job, cb))
314
def call(self, argv, shell, env, cwd, cb, echo, justprint=False, executable=None):
316
Asynchronously call the process
319
job = PopenJob(argv, executable=executable, shell=shell, env=env, cwd=cwd)
320
self.defer(self._docall_generic, self.processpool, job, cb, echo, justprint)
322
def call_native(self, module, method, argv, env, cwd, cb,
323
echo, justprint=False, pycommandpath=None):
325
Asynchronously call the native function
328
job = PythonJob(module, method, argv, env, cwd, pycommandpath)
329
self.defer(self._docall_generic, self.processpool, job, cb, echo, justprint)
332
def _waitany(condition):
335
for c in ParallelContext._allcontexts:
336
for i in xrange(0, len(c.running)):
337
if c.running[i][0].done:
338
jobs.append(c.running[i])
344
# We must acquire the lock, and then check to see if any jobs have
345
# finished. If we don't check after acquiring the lock it's possible
346
# that all outstanding jobs will have completed before we wait and we'll
347
# wait for notifications that have already occurred.
362
Spin the 'event loop', and never return.
366
clist = list(ParallelContext._allcontexts)
370
dowait = util.any((len(c.running) for c in ParallelContext._allcontexts))
372
# Wait on local jobs first for perf
373
for job, cb in ParallelContext._waitany(ParallelContext._condition):
376
assert any(len(c.pending) for c in ParallelContext._allcontexts)
378
def makedeferrable(usercb, **userkwargs):
379
def cb(*args, **kwargs):
380
kwargs.update(userkwargs)
381
return usercb(*args, **kwargs)
385
_serialContext = None
386
_parallelContext = None
388
def getcontext(jcount):
389
global _serialContext, _parallelContext
391
if _serialContext is None:
392
_serialContext = ParallelContext(1)
393
return _serialContext
395
if _parallelContext is None:
396
_parallelContext = ParallelContext(jcount)
397
return _parallelContext