| # portage.py -- core Portage functionality |
| # Copyright 1998-2014 Gentoo Foundation |
| # Distributed under the terms of the GNU General Public License v2 |
| |
| |
| import atexit |
| import errno |
| import fcntl |
| import platform |
| import signal |
| import socket |
| import struct |
| import sys |
| import traceback |
| import os as _os |
| |
| from portage import os |
| from portage import _encodings |
| from portage import _unicode_encode |
| import portage |
| portage.proxy.lazyimport.lazyimport(globals(), |
| 'portage.util:dump_traceback,writemsg', |
| ) |
| |
| from portage.const import BASH_BINARY, SANDBOX_BINARY, FAKEROOT_BINARY |
| from portage.exception import CommandNotFound |
| from portage.util._ctypes import find_library, LoadLibrary, ctypes |
| |
| try: |
| import resource |
| max_fd_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0] |
| except ImportError: |
| max_fd_limit = 256 |
| |
| if sys.hexversion >= 0x3000000: |
| # pylint: disable=W0622 |
| basestring = str |
| |
| # Support PEP 446 for Python >=3.4 |
| try: |
| _set_inheritable = _os.set_inheritable |
| except AttributeError: |
| _set_inheritable = None |
| |
| try: |
| _FD_CLOEXEC = fcntl.FD_CLOEXEC |
| except AttributeError: |
| _FD_CLOEXEC = None |
| |
| # Prefer /proc/self/fd if available (/dev/fd |
| # doesn't work on solaris, see bug #474536). |
| for _fd_dir in ("/proc/self/fd", "/dev/fd"): |
| if os.path.isdir(_fd_dir): |
| break |
| else: |
| _fd_dir = None |
| |
| # /dev/fd does not work on FreeBSD, see bug #478446 |
| if platform.system() in ('FreeBSD',) and _fd_dir == '/dev/fd': |
| _fd_dir = None |
| |
| if _fd_dir is not None: |
| def get_open_fds(): |
| return (int(fd) for fd in os.listdir(_fd_dir) if fd.isdigit()) |
| |
| if platform.python_implementation() == 'PyPy': |
| # EAGAIN observed with PyPy 1.8. |
| _get_open_fds = get_open_fds |
| def get_open_fds(): |
| try: |
| return _get_open_fds() |
| except OSError as e: |
| if e.errno != errno.EAGAIN: |
| raise |
| return range(max_fd_limit) |
| |
| elif os.path.isdir("/proc/%s/fd" % os.getpid()): |
| # In order for this function to work in forked subprocesses, |
| # os.getpid() must be called from inside the function. |
| def get_open_fds(): |
| return (int(fd) for fd in os.listdir("/proc/%s/fd" % os.getpid()) |
| if fd.isdigit()) |
| |
| else: |
| def get_open_fds(): |
| return range(max_fd_limit) |
| |
| sandbox_capable = (os.path.isfile(SANDBOX_BINARY) and |
| os.access(SANDBOX_BINARY, os.X_OK)) |
| |
| fakeroot_capable = (os.path.isfile(FAKEROOT_BINARY) and |
| os.access(FAKEROOT_BINARY, os.X_OK)) |
| |
| def spawn_bash(mycommand, debug=False, opt_name=None, **keywords): |
| """ |
| Spawns a bash shell running a specific commands |
| |
| @param mycommand: The command for bash to run |
| @type mycommand: String |
| @param debug: Turn bash debugging on (set -x) |
| @type debug: Boolean |
| @param opt_name: Name of the spawned process (detaults to binary name) |
| @type opt_name: String |
| @param keywords: Extra Dictionary arguments to pass to spawn |
| @type keywords: Dictionary |
| """ |
| |
| args = [BASH_BINARY] |
| if not opt_name: |
| opt_name = os.path.basename(mycommand.split()[0]) |
| if debug: |
| # Print commands and their arguments as they are executed. |
| args.append("-x") |
| args.append("-c") |
| args.append(mycommand) |
| return spawn(args, opt_name=opt_name, **keywords) |
| |
| def spawn_sandbox(mycommand, opt_name=None, **keywords): |
| if not sandbox_capable: |
| return spawn_bash(mycommand, opt_name=opt_name, **keywords) |
| args = [SANDBOX_BINARY] |
| if not opt_name: |
| opt_name = os.path.basename(mycommand.split()[0]) |
| args.append(mycommand) |
| return spawn(args, opt_name=opt_name, **keywords) |
| |
| def spawn_fakeroot(mycommand, fakeroot_state=None, opt_name=None, **keywords): |
| args = [FAKEROOT_BINARY] |
| if not opt_name: |
| opt_name = os.path.basename(mycommand.split()[0]) |
| if fakeroot_state: |
| open(fakeroot_state, "a").close() |
| args.append("-s") |
| args.append(fakeroot_state) |
| args.append("-i") |
| args.append(fakeroot_state) |
| args.append("--") |
| args.append(BASH_BINARY) |
| args.append("-c") |
| args.append(mycommand) |
| return spawn(args, opt_name=opt_name, **keywords) |
| |
| _exithandlers = [] |
| def atexit_register(func, *args, **kargs): |
| """Wrapper around atexit.register that is needed in order to track |
| what is registered. For example, when portage restarts itself via |
| os.execv, the atexit module does not work so we have to do it |
| manually by calling the run_exitfuncs() function in this module.""" |
| _exithandlers.append((func, args, kargs)) |
| |
| def run_exitfuncs(): |
| """This should behave identically to the routine performed by |
| the atexit module at exit time. It's only necessary to call this |
| function when atexit will not work (because of os.execv, for |
| example).""" |
| |
| # This function is a copy of the private atexit._run_exitfuncs() |
| # from the python 2.4.2 sources. The only difference from the |
| # original function is in the output to stderr. |
| exc_info = None |
| while _exithandlers: |
| func, targs, kargs = _exithandlers.pop() |
| try: |
| func(*targs, **kargs) |
| except SystemExit: |
| exc_info = sys.exc_info() |
| except: # No idea what they called, so we need this broad except here. |
| dump_traceback("Error in portage.process.run_exitfuncs", noiselevel=0) |
| exc_info = sys.exc_info() |
| |
| if exc_info is not None: |
| if sys.hexversion >= 0x3000000: |
| raise exc_info[0](exc_info[1]).with_traceback(exc_info[2]) |
| else: |
| exec("raise exc_info[0], exc_info[1], exc_info[2]") |
| |
| atexit.register(run_exitfuncs) |
| |
| # It used to be necessary for API consumers to remove pids from spawned_pids, |
| # since otherwise it would accumulate a pids endlessly. Now, spawned_pids is |
| # just an empty dummy list, so for backward compatibility, ignore ValueError |
| # for removal on non-existent items. |
| class _dummy_list(list): |
| def remove(self, item): |
| # TODO: Trigger a DeprecationWarning here, after stable portage |
| # has dummy spawned_pids. |
| try: |
| list.remove(self, item) |
| except ValueError: |
| pass |
| |
| spawned_pids = _dummy_list() |
| |
| def cleanup(): |
| pass |
| |
| def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False, |
| uid=None, gid=None, groups=None, umask=None, logfile=None, |
| path_lookup=True, pre_exec=None, close_fds=True, unshare_net=False, |
| unshare_ipc=False, cgroup=None): |
| """ |
| Spawns a given command. |
| |
| @param mycommand: the command to execute |
| @type mycommand: String or List (Popen style list) |
| @param env: A dict of Key=Value pairs for env variables |
| @type env: Dictionary |
| @param opt_name: an optional name for the spawn'd process (defaults to the binary name) |
| @type opt_name: String |
| @param fd_pipes: A dict of mapping for pipes, { '0': stdin, '1': stdout } for example |
| (default is {0:stdin, 1:stdout, 2:stderr}) |
| @type fd_pipes: Dictionary |
| @param returnpid: Return the Process IDs for a successful spawn. |
| NOTE: This requires the caller clean up all the PIDs, otherwise spawn will clean them. |
| @type returnpid: Boolean |
| @param uid: User ID to spawn as; useful for dropping privilages |
| @type uid: Integer |
| @param gid: Group ID to spawn as; useful for dropping privilages |
| @type gid: Integer |
| @param groups: Group ID's to spawn in: useful for having the process run in multiple group contexts. |
| @type groups: List |
| @param umask: An integer representing the umask for the process (see man chmod for umask details) |
| @type umask: Integer |
| @param logfile: name of a file to use for logging purposes |
| @type logfile: String |
| @param path_lookup: If the binary is not fully specified then look for it in PATH |
| @type path_lookup: Boolean |
| @param pre_exec: A function to be called with no arguments just prior to the exec call. |
| @type pre_exec: callable |
| @param close_fds: If True, then close all file descriptors except those |
| referenced by fd_pipes (default is True). |
| @type close_fds: Boolean |
| @param unshare_net: If True, networking will be unshared from the spawned process |
| @type unshare_net: Boolean |
| @param unshare_ipc: If True, IPC will be unshared from the spawned process |
| @type unshare_ipc: Boolean |
| @param cgroup: CGroup path to bind the process to |
| @type cgroup: String |
| |
| logfile requires stdout and stderr to be assigned to this process (ie not pointed |
| somewhere else.) |
| |
| """ |
| |
| # mycommand is either a str or a list |
| if isinstance(mycommand, basestring): |
| mycommand = mycommand.split() |
| |
| if sys.hexversion < 0x3000000: |
| # Avoid a potential UnicodeEncodeError from os.execve(). |
| env_bytes = {} |
| for k, v in env.items(): |
| env_bytes[_unicode_encode(k, encoding=_encodings['content'])] = \ |
| _unicode_encode(v, encoding=_encodings['content']) |
| env = env_bytes |
| del env_bytes |
| |
| # If an absolute path to an executable file isn't given |
| # search for it unless we've been told not to. |
| binary = mycommand[0] |
| if binary not in (BASH_BINARY, SANDBOX_BINARY, FAKEROOT_BINARY) and \ |
| (not os.path.isabs(binary) or not os.path.isfile(binary) |
| or not os.access(binary, os.X_OK)): |
| binary = path_lookup and find_binary(binary) or None |
| if not binary: |
| raise CommandNotFound(mycommand[0]) |
| |
| # If we haven't been told what file descriptors to use |
| # default to propagating our stdin, stdout and stderr. |
| if fd_pipes is None: |
| fd_pipes = { |
| 0:portage._get_stdin().fileno(), |
| 1:sys.__stdout__.fileno(), |
| 2:sys.__stderr__.fileno(), |
| } |
| |
| # mypids will hold the pids of all processes created. |
| mypids = [] |
| |
| if logfile: |
| # Using a log file requires that stdout and stderr |
| # are assigned to the process we're running. |
| if 1 not in fd_pipes or 2 not in fd_pipes: |
| raise ValueError(fd_pipes) |
| |
| # Create a pipe |
| (pr, pw) = os.pipe() |
| |
| # Create a tee process, giving it our stdout and stderr |
| # as well as the read end of the pipe. |
| mypids.extend(spawn(('tee', '-i', '-a', logfile), |
| returnpid=True, fd_pipes={0:pr, |
| 1:fd_pipes[1], 2:fd_pipes[2]})) |
| |
| # We don't need the read end of the pipe, so close it. |
| os.close(pr) |
| |
| # Assign the write end of the pipe to our stdout and stderr. |
| fd_pipes[1] = pw |
| fd_pipes[2] = pw |
| |
| # This caches the libc library lookup in the current |
| # process, so that it's only done once rather than |
| # for each child process. |
| if unshare_net or unshare_ipc: |
| find_library("c") |
| |
| parent_pid = os.getpid() |
| pid = None |
| try: |
| pid = os.fork() |
| |
| if pid == 0: |
| try: |
| _exec(binary, mycommand, opt_name, fd_pipes, |
| env, gid, groups, uid, umask, pre_exec, close_fds, |
| unshare_net, unshare_ipc, cgroup) |
| except SystemExit: |
| raise |
| except Exception as e: |
| # We need to catch _any_ exception so that it doesn't |
| # propagate out of this function and cause exiting |
| # with anything other than os._exit() |
| writemsg("%s:\n %s\n" % (e, " ".join(mycommand)), |
| noiselevel=-1) |
| traceback.print_exc() |
| sys.stderr.flush() |
| |
| finally: |
| if pid == 0 or (pid is None and os.getpid() != parent_pid): |
| # Call os._exit() from a finally block in order |
| # to suppress any finally blocks from earlier |
| # in the call stack (see bug #345289). This |
| # finally block has to be setup before the fork |
| # in order to avoid a race condition. |
| os._exit(1) |
| |
| if not isinstance(pid, int): |
| raise AssertionError("fork returned non-integer: %s" % (repr(pid),)) |
| |
| # Add the pid to our local and the global pid lists. |
| mypids.append(pid) |
| |
| # If we started a tee process the write side of the pipe is no |
| # longer needed, so close it. |
| if logfile: |
| os.close(pw) |
| |
| # If the caller wants to handle cleaning up the processes, we tell |
| # it about all processes that were created. |
| if returnpid: |
| return mypids |
| |
| # Otherwise we clean them up. |
| while mypids: |
| |
| # Pull the last reader in the pipe chain. If all processes |
| # in the pipe are well behaved, it will die when the process |
| # it is reading from dies. |
| pid = mypids.pop(0) |
| |
| # and wait for it. |
| retval = os.waitpid(pid, 0)[1] |
| |
| if retval: |
| # If it failed, kill off anything else that |
| # isn't dead yet. |
| for pid in mypids: |
| # With waitpid and WNOHANG, only check the |
| # first element of the tuple since the second |
| # element may vary (bug #337465). |
| if os.waitpid(pid, os.WNOHANG)[0] == 0: |
| os.kill(pid, signal.SIGTERM) |
| os.waitpid(pid, 0) |
| |
| # If it got a signal, return the signal that was sent. |
| if (retval & 0xff): |
| return ((retval & 0xff) << 8) |
| |
| # Otherwise, return its exit code. |
| return (retval >> 8) |
| |
| # Everything succeeded |
| return 0 |
| |
| def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask, |
| pre_exec, close_fds, unshare_net, unshare_ipc, cgroup): |
| |
| """ |
| Execute a given binary with options |
| |
| @param binary: Name of program to execute |
| @type binary: String |
| @param mycommand: Options for program |
| @type mycommand: String |
| @param opt_name: Name of process (defaults to binary) |
| @type opt_name: String |
| @param fd_pipes: Mapping pipes to destination; { 0:0, 1:1, 2:2 } |
| @type fd_pipes: Dictionary |
| @param env: Key,Value mapping for Environmental Variables |
| @type env: Dictionary |
| @param gid: Group ID to run the process under |
| @type gid: Integer |
| @param groups: Groups the Process should be in. |
| @type groups: Integer |
| @param uid: User ID to run the process under |
| @type uid: Integer |
| @param umask: an int representing a unix umask (see man chmod for umask details) |
| @type umask: Integer |
| @param pre_exec: A function to be called with no arguments just prior to the exec call. |
| @type pre_exec: callable |
| @param unshare_net: If True, networking will be unshared from the spawned process |
| @type unshare_net: Boolean |
| @param unshare_ipc: If True, IPC will be unshared from the spawned process |
| @type unshare_ipc: Boolean |
| @param cgroup: CGroup path to bind the process to |
| @type cgroup: String |
| @rtype: None |
| @return: Never returns (calls os.execve) |
| """ |
| |
| # If the process we're creating hasn't been given a name |
| # assign it the name of the executable. |
| if not opt_name: |
| if binary is portage._python_interpreter: |
| # NOTE: PyPy 1.7 will die due to "libary path not found" if argv[0] |
| # does not contain the full path of the binary. |
| opt_name = binary |
| else: |
| opt_name = os.path.basename(binary) |
| |
| # Set up the command's argument list. |
| myargs = [opt_name] |
| myargs.extend(mycommand[1:]) |
| |
| # Avoid a potential UnicodeEncodeError from os.execve(). |
| myargs = [_unicode_encode(x, encoding=_encodings['fs'], |
| errors='strict') for x in myargs] |
| |
| # Use default signal handlers in order to avoid problems |
| # killing subprocesses as reported in bug #353239. |
| signal.signal(signal.SIGINT, signal.SIG_DFL) |
| signal.signal(signal.SIGTERM, signal.SIG_DFL) |
| |
| # Quiet killing of subprocesses by SIGPIPE (see bug #309001). |
| signal.signal(signal.SIGPIPE, signal.SIG_DFL) |
| |
| # Avoid issues triggered by inheritance of SIGQUIT handler from |
| # the parent process (see bug #289486). |
| signal.signal(signal.SIGQUIT, signal.SIG_DFL) |
| |
| _setup_pipes(fd_pipes, close_fds=close_fds, inheritable=True) |
| |
| # Add to cgroup |
| # it's better to do it from the child since we can guarantee |
| # it is done before we start forking children |
| if cgroup: |
| with open(os.path.join(cgroup, 'cgroup.procs'), 'a') as f: |
| f.write('%d\n' % os.getpid()) |
| |
| # Unshare (while still uid==0) |
| if unshare_net or unshare_ipc: |
| filename = find_library("c") |
| if filename is not None: |
| libc = LoadLibrary(filename) |
| if libc is not None: |
| CLONE_NEWIPC = 0x08000000 |
| CLONE_NEWNET = 0x40000000 |
| |
| flags = 0 |
| if unshare_net: |
| flags |= CLONE_NEWNET |
| if unshare_ipc: |
| flags |= CLONE_NEWIPC |
| |
| try: |
| if libc.unshare(flags) != 0: |
| writemsg("Unable to unshare: %s\n" % ( |
| errno.errorcode.get(ctypes.get_errno(), '?')), |
| noiselevel=-1) |
| else: |
| if unshare_net: |
| # 'up' the loopback |
| IFF_UP = 0x1 |
| ifreq = struct.pack('16sh', b'lo', IFF_UP) |
| SIOCSIFFLAGS = 0x8914 |
| |
| sock = socket.socket(socket.AF_INET, socket.SOCK_DGRAM, 0) |
| try: |
| fcntl.ioctl(sock, SIOCSIFFLAGS, ifreq) |
| except IOError as e: |
| writemsg("Unable to enable loopback interface: %s\n" % ( |
| errno.errorcode.get(e.errno, '?')), |
| noiselevel=-1) |
| sock.close() |
| except AttributeError: |
| # unshare() not supported by libc |
| pass |
| |
| # Set requested process permissions. |
| if gid: |
| # Cast proxies to int, in case it matters. |
| os.setgid(int(gid)) |
| if groups: |
| os.setgroups(groups) |
| if uid: |
| # Cast proxies to int, in case it matters. |
| os.setuid(int(uid)) |
| if umask: |
| os.umask(umask) |
| if pre_exec: |
| pre_exec() |
| |
| # And switch to the new process. |
| os.execve(binary, myargs, env) |
| |
| def _setup_pipes(fd_pipes, close_fds=True, inheritable=None): |
| """Setup pipes for a forked process. |
| |
| Even when close_fds is False, file descriptors referenced as |
| values in fd_pipes are automatically closed if they do not also |
| occur as keys in fd_pipes. It is assumed that the caller will |
| explicitly add them to the fd_pipes keys if they are intended |
| to remain open. This allows for convenient elimination of |
| unnecessary duplicate file descriptors. |
| |
| WARNING: When not followed by exec, the close_fds behavior |
| can trigger interference from destructors that close file |
| descriptors. This interference happens when the garbage |
| collector intermittently executes such destructors after their |
| corresponding file descriptors have been re-used, leading |
| to intermittent "[Errno 9] Bad file descriptor" exceptions in |
| forked processes. This problem has been observed with PyPy 1.8, |
| and also with CPython under some circumstances (as triggered |
| by xmpppy in bug #374335). In order to close a safe subset of |
| file descriptors, see portage.locks._close_fds(). |
| |
| NOTE: When not followed by exec, even when close_fds is False, |
| it's still possible for dup2() calls to cause interference in a |
| way that's similar to the way that close_fds interferes (since |
| dup2() has to close the target fd if it happens to be open). |
| It's possible to avoid such interference by using allocated |
| file descriptors as the keys in fd_pipes. For example: |
| |
| pr, pw = os.pipe() |
| fd_pipes[pw] = pw |
| |
| By using the allocated pw file descriptor as the key in fd_pipes, |
| it's not necessary for dup2() to close a file descriptor (it |
| actually does nothing in this case), which avoids possible |
| interference. |
| """ |
| |
| reverse_map = {} |
| # To protect from cases where direct assignment could |
| # clobber needed fds ({1:2, 2:1}) we create a reverse map |
| # in order to know when it's necessary to create temporary |
| # backup copies with os.dup(). |
| for newfd, oldfd in fd_pipes.items(): |
| newfds = reverse_map.get(oldfd) |
| if newfds is None: |
| newfds = [] |
| reverse_map[oldfd] = newfds |
| newfds.append(newfd) |
| |
| # Assign newfds via dup2(), making temporary backups when |
| # necessary, and closing oldfd if the caller has not |
| # explicitly requested for it to remain open by adding |
| # it to the keys of fd_pipes. |
| while reverse_map: |
| |
| oldfd, newfds = reverse_map.popitem() |
| old_fdflags = None |
| |
| for newfd in newfds: |
| if newfd in reverse_map: |
| # Make a temporary backup before re-assignment, assuming |
| # that backup_fd won't collide with a key in reverse_map |
| # (since all of the keys correspond to open file |
| # descriptors, and os.dup() only allocates a previously |
| # unused file discriptors). |
| backup_fd = os.dup(newfd) |
| reverse_map[backup_fd] = reverse_map.pop(newfd) |
| |
| if oldfd != newfd: |
| os.dup2(oldfd, newfd) |
| if _set_inheritable is not None: |
| # Don't do this unless _set_inheritable is available, |
| # since it's used below to ensure correct state, and |
| # otherwise /dev/null stdin fails to inherit (at least |
| # with Python versions from 3.1 to 3.3). |
| if old_fdflags is None: |
| old_fdflags = fcntl.fcntl(oldfd, fcntl.F_GETFD) |
| fcntl.fcntl(newfd, fcntl.F_SETFD, old_fdflags) |
| |
| if _set_inheritable is not None: |
| |
| inheritable_state = None |
| if not (old_fdflags is None or _FD_CLOEXEC is None): |
| inheritable_state = not bool(old_fdflags & _FD_CLOEXEC) |
| |
| if inheritable is not None: |
| if inheritable_state is not inheritable: |
| _set_inheritable(newfd, inheritable) |
| |
| elif newfd in (0, 1, 2): |
| if inheritable_state is not True: |
| _set_inheritable(newfd, True) |
| |
| if oldfd not in fd_pipes: |
| # If oldfd is not a key in fd_pipes, then it's safe |
| # to close now, since we've already made all of the |
| # requested duplicates. This also closes every |
| # backup_fd that may have been created on previous |
| # iterations of this loop. |
| os.close(oldfd) |
| |
| if close_fds: |
| # Then close _all_ fds that haven't been explicitly |
| # requested to be kept open. |
| for fd in get_open_fds(): |
| if fd not in fd_pipes: |
| try: |
| os.close(fd) |
| except OSError: |
| pass |
| |
| def find_binary(binary): |
| """ |
| Given a binary name, find the binary in PATH |
| |
| @param binary: Name of the binary to find |
| @type string |
| @rtype: None or string |
| @return: full path to binary or None if the binary could not be located. |
| """ |
| paths = os.environ.get("PATH", "") |
| if sys.hexversion >= 0x3000000 and isinstance(binary, bytes): |
| # return bytes when input is bytes |
| paths = paths.encode(sys.getfilesystemencoding(), 'surrogateescape') |
| paths = paths.split(b':') |
| else: |
| paths = paths.split(':') |
| |
| for path in paths: |
| filename = _os.path.join(path, binary) |
| if _os.access(filename, os.X_OK) and _os.path.isfile(filename): |
| return filename |
| return None |