root/releases/pkgcore/0.1/pkgcore/spawn.py @ ferringb%2540gentoo.org-20060929102606-c0fe345e389939c4

Revision ferringb%2540gentoo.org-20060929102606-c0fe345e389939c4, 14.8 KB (checked in by Brian Harring <ferringb@…>, 2 years ago)

merge from integration.

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