root/releases/pkgcore/0.2.2/pkgcore/spawn.py @ jokey%2540gentoo.org-20070116031851-j7hqxnbelinxoa3z

Revision jokey%2540gentoo.org-20070116031851-j7hqxnbelinxoa3z, 16.0 KB (checked in by Markus Ullmann <jokey@…>, 2 years ago)

whitespace fixes

Line 
1# Copyright: 2005-2006 Jason Stubbs <jstubbs@gmail.com>
2# Copyright: 2004-2006 Brian Harring <ferringb@gmail.com>
3# Copyright: 2004-2005 Gentoo Foundation
4# License: GPL2
5
6
7"""
8subprocess related functionality
9"""
10
11__all__ = [
12    "cleanup_pids", "spawn", "spawn_sandbox", "spawn_bash", "spawn_fakeroot",
13    "spawn_get_output", "find_binary"]
14
15import os, atexit, signal, sys
16
17from pkgcore.util.osutils import listdir
18from pkgcore.util.mappings import ProtectedDict
19
20from pkgcore.const import (
21    BASH_BINARY, SANDBOX_BINARY, FAKED_PATH, LIBFAKEROOT_PATH)
22
23try:
24    import resource
25    max_fd_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
26except ImportError:
27    max_fd_limit = 256
28
29def slow_get_open_fds():
30    return xrange(max_fd_limit)
31if os.path.isdir("/proc/%i/fd" % os.getpid()):
32    def get_open_fds():
33        try:
34            return map(int, listdir("/proc/%i/fd" % os.getpid()))
35        except (OSError, IOError):
36            return slow_get_open_fds()
37        except ValueError, v:
38            import warnings
39            warnings.warn(
40                "extremely odd, got a value error '%s' while scanning "
41                "/proc/%i/fd; OS allowing string names in fd?" %
42                (v, os.getpid()))
43            return slow_get_open_fds()
44else:
45    get_open_fds = slow_get_open_fds
46
47
48def spawn_bash(mycommand, debug=False, opt_name=None, **keywords):
49    """spawn the command via bash -c"""
50
51    args = [BASH_BINARY]
52    if not opt_name:
53        opt_name = os.path.basename(mycommand.split()[0])
54    if debug:
55        # Print commands and their arguments as they are executed.
56        args.append("-x")
57    args.append("-c")
58    args.append(mycommand)
59    return spawn(args, opt_name=opt_name, **keywords)
60
61def spawn_sandbox(mycommand, opt_name=None, **keywords):
62    """spawn the command under sandboxed"""
63
64    if not is_sandbox_capable():
65        return spawn_bash(mycommand, opt_name=opt_name, **keywords)
66    args = [SANDBOX_BINARY]
67    if not opt_name:
68        opt_name = os.path.basename(mycommand.split()[0])
69    args.append(mycommand)
70    return spawn(args, opt_name=opt_name, **keywords)
71
72_exithandlers = []
73def atexit_register(func, *args, **kargs):
74    """Wrapper around atexit.register that is needed in order to track
75    what is registered.  For example, when portage restarts itself via
76    os.execv, the atexit module does not work so we have to do it
77    manually by calling the run_exitfuncs() function in this module."""
78    _exithandlers.append((func, args, kargs))
79
80def run_exitfuncs():
81    """This should behave identically to the routine performed by
82    the atexit module at exit time.  It's only necessary to call this
83    function when atexit will not work (because of os.execv, for
84    example)."""
85
86    # This function is a copy of the private atexit._run_exitfuncs()
87    # from the python 2.4.2 sources.  The only difference from the
88    # original function is in the output to stderr.
89    exc_info = None
90    while _exithandlers:
91        func, targs, kargs = _exithandlers.pop()
92        try:
93            func(*targs, **kargs)
94        except SystemExit:
95            exc_info = sys.exc_info()
96        except:
97            exc_info = sys.exc_info()
98
99    if exc_info is not None:
100        raise exc_info[0], exc_info[1], exc_info[2]
101
102atexit.register(run_exitfuncs)
103
104# We need to make sure that any processes spawned are killed off when
105# we exit. spawn() takes care of adding and removing pids to this list
106# as it creates and cleans up processes.
107spawned_pids = []
108def cleanup_pids(pids=None):
109    """reap list of pids if specified, else all children"""
110
111    if pids is None:
112        pids = spawned_pids
113    elif pids is not spawned_pids:
114        pids = list(pids)
115
116    while pids:
117        pid = pids.pop()
118        try:
119            if os.waitpid(pid, os.WNOHANG) == (0, 0):
120                os.kill(pid, signal.SIGTERM)
121                os.waitpid(pid, 0)
122        except OSError:
123            # This pid has been cleaned up outside
124            # of spawn().
125            pass
126
127        if spawned_pids is not pids:
128            try:
129                spawned_pids.remove(pid)
130            except ValueError:
131                pass
132
133def spawn(mycommand, env=None, opt_name=None, fd_pipes=None, returnpid=False,
134          uid=None, gid=None, groups=None, umask=None, logfile=None,
135          chdir=None, path_lookup=True):
136
137    """wrapper around execve
138
139    @type  mycommand: list or string
140    @type  env: mapping with string keys and values
141    @param opt_name: controls what the process is named
142        (what it would show up as under top for example)
143    @type  fd_pipes: mapping from existing fd to fd (inside the new process)
144    @param fd_pipes: controls what fd's are left open in the spawned process-
145    @param returnpid: controls whether spawn waits for the process to finish,
146        or returns the pid.
147    """
148    if env is None:
149        env = {}
150    # mycommand is either a str or a list
151    if isinstance(mycommand, str):
152        mycommand = mycommand.split()
153
154    # If an absolute path to an executable file isn't given
155    # search for it unless we've been told not to.
156    binary = mycommand[0]
157    if not path_lookup:
158        if find_binary(binary) != binary:
159            raise CommandNotFound(binary)
160    else:
161        binary = find_binary(binary)
162
163    # If we haven't been told what file descriptors to use
164    # default to propogating our stdin, stdout and stderr.
165    if fd_pipes is None:
166        fd_pipes = {0:0, 1:1, 2:2}
167
168    # mypids will hold the pids of all processes created.
169    mypids = []
170
171    if logfile:
172        # Using a log file requires that stdout and stderr
173        # are assigned to the process we're running.
174        if 1 not in fd_pipes or 2 not in fd_pipes:
175            raise ValueError(fd_pipes)
176
177        # Create a pipe
178        (pr, pw) = os.pipe()
179
180        # Create a tee process, giving it our stdout and stderr
181        # as well as the read end of the pipe.
182        mypids.extend(spawn(('tee', '-i', '-a', logfile), returnpid=True,
183            fd_pipes={0:pr, 1:fd_pipes[1], 2:fd_pipes[2]}))
184
185        # We don't need the read end of the pipe, so close it.
186        os.close(pr)
187
188        # Assign the write end of the pipe to our stdout and stderr.
189        fd_pipes[1] = pw
190        fd_pipes[2] = pw
191
192
193    pid = os.fork()
194
195    if not pid:
196        # 'Catch "Exception"'
197        # pylint: disable-msg=W0703
198        try:
199            _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups,
200                  uid, umask, chdir)
201        except Exception, e:
202            # We need to catch _any_ exception so that it doesn't
203            # propogate out of this function and cause exiting
204            # with anything other than os._exit()
205            sys.stderr.write("%s:\n   %s\n" % (e, " ".join(mycommand)))
206            os._exit(1)
207
208    # Add the pid to our local and the global pid lists.
209    mypids.append(pid)
210    spawned_pids.append(pid)
211
212    # If we started a tee process the write side of the pipe is no
213    # longer needed, so close it.
214    if logfile:
215        os.close(pw)
216
217    # If the caller wants to handle cleaning up the processes, we tell
218    # it about all processes that were created.
219    if returnpid:
220        return mypids
221
222    try:
223        # Otherwise we clean them up.
224        while mypids:
225
226            # Pull the last reader in the pipe chain. If all processes
227            # in the pipe are well behaved, it will die when the process
228            # it is reading from dies.
229            pid = mypids.pop(0)
230
231            # and wait for it.
232            retval = os.waitpid(pid, 0)[1]
233
234            # When it's done, we can remove it from the
235            # global pid list as well.
236            spawned_pids.remove(pid)
237
238            if retval:
239                # If it failed, kill off anything else that
240                # isn't dead yet.
241                for pid in mypids:
242                    if os.waitpid(pid, os.WNOHANG) == (0, 0):
243                        os.kill(pid, signal.SIGTERM)
244                        os.waitpid(pid, 0)
245                    spawned_pids.remove(pid)
246
247                return process_exit_code(retval)
248    finally:
249        cleanup_pids(mypids)
250
251    # Everything succeeded
252    return 0
253
254def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
255    chdir):
256    """internal function to handle exec'ing the child process.
257
258    If it succeeds this function does not return. It might raise an
259    exception, and since this runs after fork calling code needs to
260    make sure this is caught and os._exit is called if it does (or
261    atexit handlers run twice).
262    """
263
264    # If the process we're creating hasn't been given a name
265    # assign it the name of the executable.
266    if not opt_name:
267        opt_name = os.path.basename(binary)
268
269    # Set up the command's argument list.
270    myargs = [opt_name]
271    myargs.extend(mycommand[1:])
272
273    # Set up the command's pipes.
274    my_fds = {}
275    # To protect from cases where direct assignment could
276    # clobber needed fds ({1:2, 2:1}) we first dupe the fds
277    # into unused fds.
278
279    protected_fds = set(fd_pipes.itervalues())
280
281    for trg_fd, src_fd in fd_pipes.iteritems():
282        # if it's not the same we care
283        if trg_fd != src_fd:
284            if trg_fd not in protected_fds:
285                # if nothing we care about is there... do it now.
286                # we're not updating protected_fds here due to the fact
287                # dup will not overwrite existing fds, and that the target is
288                # not stated as a src at this point.
289                os.dup2(src_fd, trg_fd)
290            else:
291                x = os.dup(src_fd)
292                protected_fds.add(x)
293                my_fds[trg_fd] = x
294
295    # reassign whats required now.
296    for trg_fd, src_fd in my_fds.iteritems():
297        os.dup2(src_fd, trg_fd)
298
299    # Then close _all_ fds that haven't been explictly
300    # requested to be kept open.
301    for fd in get_open_fds():
302        # if it's not a target fd, close it.
303        if fd not in fd_pipes:
304            try:
305                os.close(fd)
306            except OSError:
307                pass
308
309    if chdir is not None:
310        os.chdir(chdir)
311
312    # Set requested process permissions.
313    if gid:
314        os.setgid(gid)
315    if groups:
316        os.setgroups(groups)
317    if uid:
318        os.setuid(uid)
319    if umask:
320        os.umask(umask)
321
322    # And switch to the new process.
323    os.execve(binary, myargs, env)
324
325
326def find_binary(binary, paths=None):
327    """look through the PATH environment, finding the binary to execute"""
328
329    if os.path.isabs(binary):
330        if not (os.path.isfile(binary) and os.access(binary, os.X_OK)):
331            raise CommandNotFound(binary)
332        return binary
333
334    if paths is None:
335        paths = os.environ.get("PATH", "").split(":")
336
337    for path in paths:
338        filename = "%s/%s" % (path, binary)
339        if os.access(filename, os.X_OK) and os.path.isfile(filename):
340            return filename
341
342    raise CommandNotFound(binary)
343
344def spawn_fakeroot(mycommand, save_file, env=None, opt_name=None,
345                   returnpid=False, **keywords):
346    """spawn a process via fakeroot
347
348    refer to the fakeroot manpage for specifics of using fakeroot
349    """
350    if env is None:
351        env = {}
352    else:
353        env = ProtectedDict(env)
354
355    if opt_name is None:
356        opt_name = "fakeroot %s" % mycommand
357
358    args = [
359        FAKED_PATH,
360        "--unknown-is-real", "--foreground", "--save-file", save_file]
361
362    rd_fd, wr_fd = os.pipe()
363    daemon_fd_pipes = {1:wr_fd, 2:wr_fd}
364    if os.path.exists(save_file):
365        args.append("--load")
366        daemon_fd_pipes[0] = os.open(save_file, os.O_RDONLY)
367    else:
368        daemon_fd_pipes[0] = os.open("/dev/null", os.O_RDONLY)
369
370    pids = None
371    pids = spawn(args, fd_pipes=daemon_fd_pipes, returnpid=True)
372    try:
373        try:
374            rd_f = os.fdopen(rd_fd)
375            line = rd_f.readline()
376            rd_f.close()
377            rd_fd = None
378        except:
379            cleanup_pids(pids)
380            raise
381    finally:
382        for x in (rd_fd, wr_fd, daemon_fd_pipes[0]):
383            if x is not None:
384                try:
385                    os.close(x)
386                except OSError:
387                    pass
388
389    line = line.strip()
390
391    try:
392        fakekey, fakepid = map(int, line.split(":"))
393    except ValueError:
394        raise ExecutionFailure("output from faked was unparsable- %s" % line)
395
396    # by now we have our very own daemonized faked.  yay.
397    env["FAKEROOTKEY"] = str(fakekey)
398    env["LD_PRELOAD"] = ":".join(
399        [LIBFAKEROOT_PATH] + env.get("LD_PRELOAD", "").split(":"))
400
401    try:
402        ret = spawn(
403            mycommand, opt_name=opt_name, env=env, returnpid=returnpid,
404            **keywords)
405        if returnpid:
406            return ret + [fakepid] + pids
407        return ret
408    finally:
409        if not returnpid:
410            cleanup_pids([fakepid] + pids)
411
412def spawn_get_output(
413    mycommand, spawn_type=spawn, raw_exit_code=False, collect_fds=(1,),
414    fd_pipes=None, split_lines=True, **keywords):
415
416    """Call spawn, collecting the output to fd's specified in collect_fds list.
417
418    @param spawn_type: the passed in function to call-
419       typically spawn_bash, spawn, spawn_sandbox, or spawn_fakeroot.
420       defaults to spawn
421    """
422
423    pr, pw = None, None
424    if fd_pipes is None:
425        fd_pipes = {0:0}
426    else:
427        fd_pipes = ProtectedDict(fd_pipes)
428    try:
429        pr, pw = os.pipe()
430        for x in collect_fds:
431            fd_pipes[x] = pw
432        keywords["returnpid"] = True
433        mypid = spawn_type(mycommand, fd_pipes=fd_pipes, **keywords)
434        os.close(pw)
435        pw = None
436
437        if not isinstance(mypid, (list, tuple)):
438            raise ExecutionFailure()
439
440        fd = os.fdopen(pr, "r")
441        try:
442            if not split_lines:
443                mydata = fd.read()
444            else:
445                mydata = fd.readlines()
446        finally:
447            fd.close()
448            pw = None
449
450        retval = os.waitpid(mypid[0], 0)[1]
451        cleanup_pids(mypid)
452        if raw_exit_code:
453            return [retval, mydata]
454        return [process_exit_code(retval), mydata]
455
456    finally:
457        if pr is not None:
458            try:
459                os.close(pr)
460            except OSError:
461                pass
462        if pw is not None:
463            try:
464                os.close(pw)
465            except OSError:
466                pass
467
468def process_exit_code(retval):
469    """Process a waitpid returned exit code.
470
471    @return: The exit code if it exit'd, the signal if it died from signalling.
472    """
473    # If it got a signal, return the signal that was sent.
474    if retval & 0xff:
475        return (retval & 0xff) << 8
476
477    # Otherwise, return its exit code.
478    return retval >> 8
479
480
481class ExecutionFailure(Exception):
482    def __init__(self, msg):
483        Exception.__init__(self, msg)
484        self.msg = msg
485    def __str__(self):
486        return "Execution Failure: %s" % self.msg
487
488class CommandNotFound(ExecutionFailure):
489    def __init__(self, command):
490        ExecutionFailure.__init__(
491            self, "CommandNotFound Exception: Couldn't find '%s'" % (command,))
492        self.command = command
493
494# cached capabilities
495
496def is_fakeroot_capable(force=False):
497    if not force:
498        try:
499            return is_fakeroot_capable.cached_result
500        except AttributeError:
501            pass
502    if not (os.path.exists(FAKED_PATH) and os.path.exists(LIBFAKEROOT_PATH)):
503        res = False
504    else:
505        try:
506            r, s = spawn_get_output(["fakeroot", "--version"],
507                fd_pipes={2:1, 1:1})
508            res = (r == 0) and (len(s) == 1) and ("version 1." in s[0])
509        except ExecutionFailure:
510            res = False
511    is_fakeroot_capable.cached_result = res
512    return res
513
514def is_sandbox_capable(force=False):
515    if not force:
516        try:
517            return is_sandbox_capable.cached_result
518        except AttributeError:
519            pass
520    res = os.path.isfile(SANDBOX_BINARY) and os.access(SANDBOX_BINARY, os.X_OK)
521    is_sandbox_capable.cached_result = res
522    return res
523
524def is_userpriv_capable(force=False):
525    if not force:
526        try:
527            return is_userpriv_capable.cached_result
528        except AttributeError:
529            pass
530    res = is_userpriv_capable.cached_result = (os.getuid() == 0)
531    return res
Note: See TracBrowser for help on using the browser.