~ubuntu-branches/ubuntu/quantal/enigmail/quantal-security

« back to all changes in this revision

Viewing changes to mozilla/build/pymake/pymake/process.py

  • Committer: Package Import Robot
  • Author(s): Chris Coulson
  • Date: 2013-09-13 16:02:15 UTC
  • mfrom: (0.12.16)
  • Revision ID: package-import@ubuntu.com-20130913160215-u3g8nmwa0pdwagwc
Tags: 2:1.5.2-0ubuntu0.12.10.1
* New upstream release v1.5.2 for Thunderbird 24

* Build enigmail using a stripped down Thunderbird 17 build system, as it's
  now quite difficult to build the way we were doing previously, with the
  latest Firefox build system
* Add debian/patches/no_libxpcom.patch - Don't link against libxpcom, as it
  doesn't exist anymore (but exists in the build system)
* Add debian/patches/use_sdk.patch - Use the SDK version of xpt.py and
  friends
* Drop debian/patches/ipc-pipe_rename.diff (not needed anymore)
* Drop debian/patches/makefile_depth.diff (not needed anymore)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
"""
 
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.
 
4
"""
 
5
 
 
6
#TODO: ship pyprocessing?
 
7
import multiprocessing
 
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
 
11
import command, util
 
12
if sys.platform=='win32':
 
13
    import win32process
 
14
 
 
15
_log = logging.getLogger('pymake.process')
 
16
 
 
17
_escapednewlines = re.compile(r'\\\n')
 
18
# Characters that most likely indicate a shell script and that native commands
 
19
# should reject
 
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'[\*\?]')
 
27
 
 
28
def clinetoargv(cline, blacklist_gray):
 
29
    """
 
30
    If this command line can safely skip the shell, return an argv array.
 
31
    @returns argv, badchar
 
32
    """
 
33
    str = _escapednewlines.sub('', cline)
 
34
    m = _blacklist.search(str)
 
35
    if m is not None:
 
36
        return None, m.group(0)
 
37
    if blacklist_gray:
 
38
        m = _graylist.search(str)
 
39
        if m is not None:
 
40
            return None, m.group(0)
 
41
 
 
42
    args = shlex.split(str, comments=True)
 
43
 
 
44
    if len(args) and args[0].find('=') != -1:
 
45
        return None, '='
 
46
 
 
47
    return args, None
 
48
 
 
49
def doglobbing(args, cwd):
 
50
    """
 
51
    Perform any needed globbing on the argument list passed in
 
52
    """
 
53
    globbedargs = []
 
54
    for arg in args:
 
55
        if _needsglob.search(arg):
 
56
            globbedargs.extend(glob.glob(os.path.join(cwd, arg)))
 
57
        else:
 
58
            globbedargs.append(arg)
 
59
 
 
60
    return globbedargs
 
61
 
 
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')
 
69
 
 
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()
 
73
 
 
74
    shellreason = None
 
75
    if msys and cline.startswith('/'):
 
76
        shellreason = "command starts with /"
 
77
    else:
 
78
        argv, badchar = clinetoargv(cline, blacklist_gray=True)
 
79
        if argv is None:
 
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],)
 
83
        else:
 
84
            argv = doglobbing(argv, cwd)
 
85
 
 
86
    if shellreason is not None:
 
87
        _log.debug("%s: using shell: %s: '%s'", loc, shellreason, cline)
 
88
        if msys:
 
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,
 
93
                     justprint=justprint)
 
94
        return
 
95
 
 
96
    if not len(argv):
 
97
        cb(res=0)
 
98
        return
 
99
 
 
100
    if argv[0] == command.makepypath:
 
101
        command.main(argv[1:], env, cwd, cb)
 
102
        return
 
103
 
 
104
    if argv[0:2] == [sys.executable.replace('\\', '/'),
 
105
                     command.makepypath.replace('\\', '/')]:
 
106
        command.main(argv[2:], env, cwd, cb)
 
107
        return
 
108
 
 
109
    if argv[0].find('/') != -1:
 
110
        executable = util.normaljoin(cwd, argv[0])
 
111
    else:
 
112
        executable = None
 
113
 
 
114
    context.call(argv, executable=executable, shell=False, env=env, cwd=cwd, cb=cb,
 
115
                 echo=echo, justprint=justprint)
 
116
 
 
117
def call_native(module, method, argv, env, cwd, loc, cb, context, echo, justprint=False,
 
118
                pycommandpath=None):
 
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)
 
122
 
 
123
def statustoresult(status):
 
124
    """
 
125
    Convert the status returned from waitpid into a prettier numeric result.
 
126
    """
 
127
    sig = status & 0xFF
 
128
    if sig:
 
129
        return -sig
 
130
 
 
131
    return status >>8
 
132
 
 
133
class Job(object):
 
134
    """
 
135
    A single job to be executed on the process pool.
 
136
    """
 
137
    done = False # set to true when the job completes
 
138
 
 
139
    def __init__(self):
 
140
        self.exitcode = -127
 
141
 
 
142
    def notify(self, condition, result):
 
143
        condition.acquire()
 
144
        self.done = True
 
145
        self.exitcode = result
 
146
        condition.notify()
 
147
        condition.release()
 
148
 
 
149
    def get_callback(self, condition):
 
150
        return lambda result: self.notify(condition, result)
 
151
 
 
152
class PopenJob(Job):
 
153
    """
 
154
    A job that executes a command using subprocess.Popen.
 
155
    """
 
156
    def __init__(self, argv, executable, shell, env, cwd):
 
157
        Job.__init__(self)
 
158
        self.argv = argv
 
159
        self.executable = executable
 
160
        self.shell = shell
 
161
        self.env = env
 
162
        self.cwd = cwd
 
163
        self.parentpid = os.getpid()
 
164
 
 
165
    def run(self):
 
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']
 
174
        try:
 
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)
 
178
            return p.wait()
 
179
        except OSError, e:
 
180
            print >>sys.stderr, e
 
181
            return -127
 
182
        finally:
 
183
            os.environ['PATH'] = oldpath
 
184
 
 
185
class PythonException(Exception):
 
186
    def __init__(self, message, exitcode):
 
187
        Exception.__init__(self)
 
188
        self.message = message
 
189
        self.exitcode = exitcode
 
190
 
 
191
    def __str__(self):
 
192
        return self.message
 
193
 
 
194
def load_module_recursive(module, path):
 
195
    """
 
196
    Emulate the behavior of __import__, but allow
 
197
    passing a custom path to search for modules.
 
198
    """
 
199
    bits = module.split('.')
 
200
    for i, bit in enumerate(bits):
 
201
        dotname = '.'.join(bits[:i+1])
 
202
        try:
 
203
          f, path, desc = imp.find_module(bit, path)
 
204
          m = imp.load_module(dotname, f, path, desc)
 
205
          if f is None:
 
206
              path = m.__path__
 
207
        except ImportError:
 
208
            return
 
209
 
 
210
class PythonJob(Job):
 
211
    """
 
212
    A job that calls a Python method.
 
213
    """
 
214
    def __init__(self, module, method, argv, env, cwd, pycommandpath=None):
 
215
        self.module = module
 
216
        self.method = method
 
217
        self.argv = argv
 
218
        self.env = env
 
219
        self.cwd = cwd
 
220
        self.pycommandpath = pycommandpath or []
 
221
        self.parentpid = os.getpid()
 
222
 
 
223
    def run(self):
 
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)
 
228
        try:
 
229
            os.chdir(self.cwd)
 
230
            os.environ.clear()
 
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
 
237
                return -127                
 
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)
 
241
                return -127
 
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)
 
248
 
 
249
        except PythonException, e:
 
250
            print >>sys.stderr, e
 
251
            return e.exitcode
 
252
        except:
 
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
 
256
            else:
 
257
                print >>sys.stderr, e
 
258
                print >>sys.stderr, traceback.print_exc()
 
259
                return (e.code if isinstance(e.code, int) else 1)
 
260
        finally:
 
261
            os.environ.clear()
 
262
            os.environ.update(oldenv)
 
263
        return 0
 
264
 
 
265
def job_runner(job):
 
266
    """
 
267
    Run a job. Called in a Process pool.
 
268
    """
 
269
    return job.run()
 
270
 
 
271
class ParallelContext(object):
 
272
    """
 
273
    Manages the parallel execution of processes.
 
274
    """
 
275
 
 
276
    _allcontexts = set()
 
277
    _condition = multiprocessing.Condition()
 
278
 
 
279
    def __init__(self, jcount):
 
280
        self.jcount = jcount
 
281
        self.exit = False
 
282
 
 
283
        self.processpool = multiprocessing.Pool(processes=jcount)
 
284
        self.pending = [] # list of (cb, args, kwargs)
 
285
        self.running = [] # list of (subprocess, cb)
 
286
 
 
287
        self._allcontexts.add(self)
 
288
 
 
289
    def finish(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)
 
294
 
 
295
    def run(self):
 
296
        while len(self.pending) and len(self.running) < self.jcount:
 
297
            cb, args, kwargs = self.pending.pop(0)
 
298
            cb(*args, **kwargs)
 
299
 
 
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))
 
303
 
 
304
    def _docall_generic(self, pool, job, cb, echo, justprint):
 
305
        if echo is not None:
 
306
            print echo
 
307
        processcb = job.get_callback(ParallelContext._condition)
 
308
        if justprint:
 
309
            processcb(0)
 
310
        else:
 
311
            pool.apply_async(job_runner, args=(job,), callback=processcb)
 
312
        self.running.append((job, cb))
 
313
 
 
314
    def call(self, argv, shell, env, cwd, cb, echo, justprint=False, executable=None):
 
315
        """
 
316
        Asynchronously call the process
 
317
        """
 
318
 
 
319
        job = PopenJob(argv, executable=executable, shell=shell, env=env, cwd=cwd)
 
320
        self.defer(self._docall_generic, self.processpool, job, cb, echo, justprint)
 
321
 
 
322
    def call_native(self, module, method, argv, env, cwd, cb,
 
323
                    echo, justprint=False, pycommandpath=None):
 
324
        """
 
325
        Asynchronously call the native function
 
326
        """
 
327
 
 
328
        job = PythonJob(module, method, argv, env, cwd, pycommandpath)
 
329
        self.defer(self._docall_generic, self.processpool, job, cb, echo, justprint)
 
330
 
 
331
    @staticmethod
 
332
    def _waitany(condition):
 
333
        def _checkdone():
 
334
            jobs = []
 
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])
 
339
                for j in jobs:
 
340
                    if j in c.running:
 
341
                        c.running.remove(j)
 
342
            return jobs
 
343
 
 
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.
 
348
        condition.acquire()
 
349
        jobs = _checkdone()
 
350
 
 
351
        if jobs == []:
 
352
            condition.wait()
 
353
            jobs = _checkdone()
 
354
 
 
355
        condition.release()
 
356
 
 
357
        return jobs
 
358
        
 
359
    @staticmethod
 
360
    def spin():
 
361
        """
 
362
        Spin the 'event loop', and never return.
 
363
        """
 
364
 
 
365
        while True:
 
366
            clist = list(ParallelContext._allcontexts)
 
367
            for c in clist:
 
368
                c.run()
 
369
 
 
370
            dowait = util.any((len(c.running) for c in ParallelContext._allcontexts))
 
371
            if dowait:
 
372
                # Wait on local jobs first for perf
 
373
                for job, cb in ParallelContext._waitany(ParallelContext._condition):
 
374
                    cb(job.exitcode)
 
375
            else:
 
376
                assert any(len(c.pending) for c in ParallelContext._allcontexts)
 
377
 
 
378
def makedeferrable(usercb, **userkwargs):
 
379
    def cb(*args, **kwargs):
 
380
        kwargs.update(userkwargs)
 
381
        return usercb(*args, **kwargs)
 
382
 
 
383
    return cb
 
384
 
 
385
_serialContext = None
 
386
_parallelContext = None
 
387
 
 
388
def getcontext(jcount):
 
389
    global _serialContext, _parallelContext
 
390
    if jcount == 1:
 
391
        if _serialContext is None:
 
392
            _serialContext = ParallelContext(1)
 
393
        return _serialContext
 
394
    else:
 
395
        if _parallelContext is None:
 
396
            _parallelContext = ParallelContext(jcount)
 
397
        return _parallelContext
 
398