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

« back to all changes in this revision

Viewing changes to 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