Merge the portage.spawn() implementation with pty logging from trunk:
authorZac Medico <zmedico@gentoo.org>
Wed, 25 Jul 2007 11:16:27 +0000 (11:16 -0000)
committerZac Medico <zmedico@gentoo.org>
Wed, 25 Jul 2007 11:16:27 +0000 (11:16 -0000)
- Sending output through the pty device makes logging compatible with
  sesandbox and the default selinux security policy (see bug #162404).
- Having a pty device for stdout makes $PAGER behave nicely for things like
  check_license() from eutils.eclass.

svn path=/main/branches/2.1.2/; revision=7395

pym/output.py
pym/portage.py

index 6ac6da3fea4ffb198bd2a4758a31d5bf532c66e9..08e4d84011f9c2dab8e79013bc570f2b0c899728 100644 (file)
@@ -332,6 +332,15 @@ def get_term_size():
                                pass
        return -1, -1
 
+def set_term_size(lines, columns, fd):
+       """
+       Set the number of lines and columns for the tty that is connected to fd.
+       For portability, this simply calls `stty rows $lines columns $columns`.
+       """
+       from portage_exec import spawn
+       cmd = ["stty", "rows", str(lines), "columns", str(columns)]
+       spawn(cmd, env=os.environ, fd_pipes={0:fd})
+
 class EOutput:
        """
        Performs fancy terminal formatting for status and informational messages.
index a7ada84e426f38d16f1c76f4735c4b03734eef16..2c2e83278b3b4a0b80075f02432ddb67b7d191da 100644 (file)
@@ -2314,7 +2314,7 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
        1. The return code of the spawned process.
        """
 
-       if type(mysettings) == types.DictType:
+       if isinstance(mysettings, dict):
                env=mysettings
                keywords["opt_name"]="[ %s ]" % "portage"
        else:
@@ -2326,10 +2326,12 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
        # from shells and from binaries that belong to portage (the number of entry
        # points is minimized).  The "tee" binary is not among the allowed entry
        # points, so it is spawned outside of the sesandbox domain and reads from a
-       # pipe between two domains.
+       # pseudo-terminal that connects two domains.
        logfile = keywords.get("logfile")
        mypids = []
-       pw = None
+       master_fd = None
+       slave_fd = None
+       fd_pipes_orig = None
        if logfile:
                del keywords["logfile"]
                fd_pipes = keywords.get("fd_pipes")
@@ -2337,12 +2339,18 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
                        fd_pipes = {0:0, 1:1, 2:2}
                elif 1 not in fd_pipes or 2 not in fd_pipes:
                        raise ValueError(fd_pipes)
-               pr, pw = os.pipe()
-               mypids.extend(portage_exec.spawn(('tee', '-i', '-a', logfile),
-                        returnpid=True, fd_pipes={0:pr, 1:fd_pipes[1], 2:fd_pipes[2]}))
-               os.close(pr)
-               fd_pipes[1] = pw
-               fd_pipes[2] = pw
+               from pty import openpty
+               master_fd, slave_fd = openpty()
+               fd_pipes.setdefault(0, sys.stdin.fileno())
+               fd_pipes_orig = fd_pipes.copy()
+               stdin_fd = fd_pipes[0]
+               if os.isatty(stdin_fd):
+                       from output import get_term_size, set_term_size
+                       rows, columns = get_term_size()
+                       set_term_size(rows, columns, slave_fd)
+               fd_pipes[0] = fd_pipes_orig[0]
+               fd_pipes[1] = slave_fd
+               fd_pipes[2] = slave_fd
                keywords["fd_pipes"] = fd_pipes
 
        features = mysettings.features
@@ -2351,13 +2359,14 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
        # permissions in the merge phase.
        fakeroot = fakeroot and uid != 0 and portage_exec.fakeroot_capable
        if droppriv and not uid and portage_gid and portage_uid:
-               keywords.update({"uid":portage_uid,"gid":portage_gid,"groups":userpriv_groups,"umask":002})
-
+               keywords.update({"uid":portage_uid,"gid":portage_gid,
+                       "groups":userpriv_groups,"umask":002})
        if not free:
                free=((droppriv and "usersandbox" not in features) or \
-                       (not droppriv and "sandbox" not in features and "usersandbox" not in features))
+                       (not droppriv and "sandbox" not in features and \
+                       "usersandbox" not in features))
 
-       if free:
+       if free or "SANDBOX_ACTIVE" in os.environ:
                keywords["opt_name"] += " bash"
                spawn_func = portage_exec.spawn_bash
        elif fakeroot:
@@ -2370,7 +2379,8 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
 
        if sesandbox:
                con = selinux.getcontext()
-               con = con.replace(mysettings["PORTAGE_T"], mysettings["PORTAGE_SANDBOX_T"])
+               con = con.replace(mysettings["PORTAGE_T"],
+                       mysettings["PORTAGE_SANDBOX_T"])
                selinux.setexec(con)
 
        returnpid = keywords.get("returnpid")
@@ -2378,29 +2388,61 @@ def spawn(mystring, mysettings, debug=0, free=0, droppriv=0, sesandbox=0, fakero
        try:
                mypids.extend(spawn_func(mystring, env=env, **keywords))
        finally:
-               if pw:
-                       os.close(pw)
+               if slave_fd:
+                       os.close(slave_fd)
                if sesandbox:
                        selinux.setexec(None)
 
        if returnpid:
                return mypids
 
-       while mypids:
-               pid = mypids.pop(0)
-               retval = os.waitpid(pid, 0)[1]
-               portage_exec.spawned_pids.remove(pid)
-               if retval != os.EX_OK:
-                       for pid in mypids:
-                               if os.waitpid(pid, os.WNOHANG) == (0,0):
-                                       import signal
-                                       os.kill(pid, signal.SIGTERM)
-                                       os.waitpid(pid, 0)
-                               portage_exec.spawned_pids.remove(pid)
-                       if retval & 0xff:
-                               return (retval & 0xff) << 8
-                       return retval >> 8
-       return os.EX_OK
+       if logfile:
+               log_file = open(logfile, 'a')
+               stdout_file = os.fdopen(os.dup(fd_pipes_orig[1]), 'w')
+               master_file = os.fdopen(master_fd, 'w+')
+               iwtd = [master_file]
+               owtd = []
+               ewtd = []
+               import array, fcntl, select
+               fd_flags = {}
+               for f in iwtd:
+                       fd_flags[f] = fcntl.fcntl(f.fileno(), fcntl.F_GETFL)
+               buffsize = 65536
+               eof = False
+               while not eof:
+                       events = select.select(iwtd, owtd, ewtd)
+                       for f in events[0]:
+                               # Use non-blocking mode to prevent read
+                               # calls from blocking indefinitely.
+                               fcntl.fcntl(f.fileno(), fcntl.F_SETFL,
+                                       fd_flags[f] | os.O_NONBLOCK)
+                               buf = array.array('B')
+                               try:
+                                       buf.fromfile(f, buffsize)
+                               except EOFError:
+                                       pass
+                               fcntl.fcntl(f.fileno(), fcntl.F_SETFL, fd_flags[f])
+                               if not buf:
+                                       eof = True
+                                       break
+                               # Use blocking mode for writes since we'd rather block than
+                               # trigger a EWOULDBLOCK error.
+                               if f is master_file:
+                                       buf.tofile(stdout_file)
+                                       stdout_file.flush()
+                                       buf.tofile(log_file)
+                                       log_file.flush()
+               log_file.close()
+               stdout_file.close()
+               master_file.close()
+       pid = mypids[-1]
+       retval = os.waitpid(pid, 0)[1]
+       portage_exec.spawned_pids.remove(pid)
+       if retval != os.EX_OK:
+               if retval & 0xff:
+                       return (retval & 0xff) << 8
+               return retval >> 8
+       return retval
 
 def fetch(myuris, mysettings, listonly=0, fetchonly=0, locks_in_subdir=".locks",use_locks=1, try_mirrors=1):
        "fetch files.  Will use digest file if available."