root/releases/pkgcore/0.1.2/pkgcore/spawn.py @ ferringb%2540gentoo.org-20061001002928-e3ad5eb445bb7f3a

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

add chdir resetting option for spawn, use it for sandbox (sandbox sucks) when ran from a non existant dir

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          chdir=None, 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, chdir)
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    chdir):
254    """internal function to handle exec'ing the child process.
255
256    If it succeeds this function does not return. It might raise an
257    exception, and since this runs after fork calling code needs to
258    make sure this is caught and os._exit is called if it does (or
259    atexit handlers run twice).
260    """
261
262    # If the process we're creating hasn't been given a name
263    # assign it the name of the executable.
264    if not opt_name:
265        opt_name = os.path.basename(binary)
266
267    # Set up the command's argument list.
268    myargs = [opt_name]
269    myargs.extend(mycommand[1:])
270
271    # Set up the command's pipes.
272    my_fds = {}
273    # To protect from cases where direct assignment could
274    # clobber needed fds ({1:2, 2:1}) we first dupe the fds
275    # into unused fds.
276    for fd in fd_pipes:
277        my_fds[fd] = os.dup(fd_pipes[fd])
278    # Then assign them to what they should be.
279    for fd in my_fds:
280        os.dup2(my_fds[fd], fd)
281    # Then close _all_ fds that haven't been explictly
282    # requested to be kept open.
283    for fd in get_open_fds():
284        if fd not in my_fds:
285            try:
286                os.close(fd)
287            except OSError:
288                pass
289
290    if chdir is not None:
291        os.chdir(chdir)
292
293    # Set requested process permissions.
294    if gid:
295        os.setgid(gid)
296    if groups:
297        os.setgroups(groups)
298    if uid:
299        os.setuid(uid)
300    if umask:
301        os.umask(umask)
302
303    # And switch to the new process.
304    os.execve(binary, myargs, env)
305
306
307def find_binary(binary, paths=None):
308    """look through the PATH environment, finding the binary to execute"""
309
310    if os.path.isabs(binary):
311        if not (os.path.isfile(binary) and os.access(binary, os.X_OK)):
312            raise CommandNotFound(binary)
313        return binary
314
315    if paths is None:
316        paths = os.environ.get("PATH", "").split(":")
317   
318    for path in paths:
319        filename = "%s/%s" % (path, binary)
320        if os.access(filename, os.X_OK) and os.path.isfile(filename):
321            return filename
322
323    raise CommandNotFound(binary)
324
325def spawn_fakeroot(mycommand, save_file, env=None, opt_name=None,
326                   returnpid=False, **keywords):
327    """spawn a process via fakeroot
328
329    refer to the fakeroot manpage for specifics of using fakeroot
330    """
331    if env is None:
332        env = {}
333    else:
334        env = ProtectedDict(env)
335
336    if opt_name is None:
337        opt_name = "fakeroot %s" % mycommand
338
339    args = [
340        FAKED_PATH,
341        "--unknown-is-real", "--foreground", "--save-file", save_file]
342
343    rd_fd, wr_fd = os.pipe()
344    daemon_fd_pipes = {1:wr_fd, 2:wr_fd}
345    if os.path.exists(save_file):
346        args.append("--load")
347        daemon_fd_pipes[0] = os.open(save_file, os.O_RDONLY)
348    else:
349        daemon_fd_pipes[0] = os.open("/dev/null", os.O_RDONLY)
350
351    pids = None
352    pids = spawn(args, fd_pipes=daemon_fd_pipes, returnpid=True)
353    try:
354        try:
355            rd_f = os.fdopen(rd_fd)
356            line = rd_f.readline()
357            rd_f.close()
358            rd_fd = None
359        except:
360            cleanup_pids(pids)
361            raise
362    finally:
363        for x in (rd_fd, wr_fd, daemon_fd_pipes[0]):
364            if x is not None:
365                try:
366                    os.close(x)
367                except OSError:
368                    pass
369
370    line = line.strip()
371
372    try:
373        fakekey, fakepid = map(int, line.split(":"))
374    except ValueError:
375        raise ExecutionFailure("output from faked was unparsable- %s" % line)
376
377    # by now we have our very own daemonized faked.  yay.
378    env["FAKEROOTKEY"] = str(fakekey)
379    env["LD_PRELOAD"] = ":".join(
380        [LIBFAKEROOT_PATH] + env.get("LD_PRELOAD", "").split(":"))
381
382    try:
383        ret = spawn(
384            mycommand, opt_name=opt_name, env=env, returnpid=returnpid,
385            **keywords)
386        if returnpid:
387            return ret + [fakepid] + pids
388        return ret
389    finally:
390        if not returnpid:
391            cleanup_pids([fakepid] + pids)
392
393def spawn_get_output(
394    mycommand, spawn_type=spawn, raw_exit_code=False, collect_fds=(1,),
395    fd_pipes=None, split_lines=True, **keywords):
396
397    """Call spawn, collecting the output to fd's specified in collect_fds list.
398
399    @param spawn_type: the passed in function to call-
400       typically spawn_bash, spawn, spawn_sandbox, or spawn_fakeroot.
401    """
402
403    pr, pw = None, None
404    if fd_pipes is None:
405        fd_pipes = {0:0}
406    else:
407        fd_pipes = ProtectedDict(fd_pipes)
408    try:
409        pr, pw = os.pipe()
410        for x in collect_fds:
411            fd_pipes[x] = pw
412        keywords["returnpid"] = True
413        mypid = spawn_type(mycommand, fd_pipes=fd_pipes, **keywords)
414        os.close(pw)
415        pw = None
416
417        if not isinstance(mypid, (list, tuple)):
418            raise ExecutionFailure()
419
420        fd = os.fdopen(pr, "r")
421        try:
422            if not split_lines:
423                mydata = fd.read()
424            else:
425                mydata = fd.readlines()
426        finally:
427            fd.close()
428            pw = None
429
430        retval = os.waitpid(mypid[0], 0)[1]
431        cleanup_pids(mypid)
432        if raw_exit_code:
433            return [retval, mydata]
434        return [process_exit_code(retval), mydata]
435
436    finally:
437        if pr is not None:
438            try:
439                os.close(pr)
440            except OSError:
441                pass
442        if pw is not None:
443            try:
444                os.close(pw)
445            except OSError:
446                pass
447
448def process_exit_code(retval):
449    """Process a waitpid returned exit code.
450
451    @return: The exit code if it exit'd, the signal if it died from signalling.
452    """
453    # If it got a signal, return the signal that was sent.
454    if retval & 0xff:
455        return (retval & 0xff) << 8
456
457    # Otherwise, return its exit code.
458    return retval >> 8
459
460
461class ExecutionFailure(Exception):
462    def __init__(self, msg):
463        Exception.__init__(self, msg)
464        self.msg = msg
465    def __str__(self):
466        return "Execution Failure: %s" % self.msg
467
468class CommandNotFound(ExecutionFailure):
469    def __init__(self, command):
470        ExecutionFailure.__init__(
471            self, "CommandNotFound Exception: Couldn't find '%s'" % (command,))
472        self.command = command
473
474# JIT'd capabilities
475
476def _determine_fakeroot_usable():
477    if not (os.path.exists(FAKED_PATH) and os.path.exists(LIBFAKEROOT_PATH)):
478        return False
479    try:
480        r, s = spawn_get_output(["fakeroot", "--version"],
481            fd_pipes={2:1, 1:1})
482        return (r == 0) and (len(s) == 1) and ("version 1." in s[0])
483    except ExecutionFailure:
484        return False
485
486def _determine_sandbox_usable():
487    return os.path.isfile(SANDBOX_BINARY) and \
488        os.access(SANDBOX_BINARY, os.X_OK)
489
490def _determine_userpriv_usable():
491    return os.getuid() == 0
492
493sandbox_capable = DelayedInstantiation(bool, _determine_sandbox_usable)
494userpriv_capable = DelayedInstantiation(bool, _determine_userpriv_usable)
495fakeroot_capable = DelayedInstantiation(bool, _determine_fakeroot_usable)
Note: See TracBrowser for help on using the browser.