A couple of bug fixes, large cleanups, commenting and a bit of dead code removal.
authorJason Stubbs <jstubbs@gentoo.org>
Thu, 3 Nov 2005 14:34:08 +0000 (14:34 -0000)
committerJason Stubbs <jstubbs@gentoo.org>
Thu, 3 Nov 2005 14:34:08 +0000 (14:34 -0000)
svn path=/main/branches/2.0/; revision=2254

pym/portage_exec.py

index 8a92c7ae2c642117da92e2fb18e700e30c89a11d..c62d0a549a45ca1e76189043b3417cdec499b1ff 100644 (file)
 # $Id: /var/cvsroot/gentoo-src/portage/pym/portage_exec.py,v 1.13.2.4 2005/04/17 09:01:56 jstubbs Exp $
 
 
-import os,types,atexit,string,stat
-import signal
+import os, atexit, signal, sys
 import portage_data
-import portage_util
+
+from portage_const import BASH_BINARY, SANDBOX_BINARY
+
 
 try:
        import resource
-       max_fd_limit=resource.getrlimit(RLIMIT_NOFILE)
-except SystemExit, e:
-       raise
-except:
-       # hokay, no resource module.
-       max_fd_limit=256
+       max_fd_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[1]
+except ImportError:
+       max_fd_limit = 256
 
-spawned_pids = []
-def cleanup():
-       global spawned_pids
-       while spawned_pids:
-               pid = spawned_pids.pop()
-               try:
-                       os.kill(pid,SIGKILL)
-               except SystemExit, e:
-                       raise
-               except:
-                       pass
-atexit.register(cleanup)
 
-from portage_const import BASH_BINARY,SANDBOX_BINARY,SANDBOX_PIDS_FILE
+sandbox_capable = (os.path.isfile(SANDBOX_BINARY) and
+                   os.access(SANDBOX_BINARY, os.X_OK))
 
-sandbox_capable = (os.path.exists(SANDBOX_BINARY) and os.access(SANDBOX_BINARY, os.X_OK))
 
-def spawn_bash(mycommand,env={},debug=False,opt_name=None,**keywords):
-       args=[BASH_BINARY]
+def spawn_bash(mycommand, debug=False, opt_name=None, **keywords):
+       args = [BASH_BINARY]
        if not opt_name:
-               opt_name=mycommand.split()[0]
-       if not env.has_key("BASH_ENV"):
-               env["BASH_ENV"] = "/etc/spork/is/not/valid/profile.env"
+               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,env=env,opt_name=opt_name,**keywords)
+       return spawn(args, opt_name=opt_name, **keywords)
 
-def spawn_sandbox(mycommand,uid=None,opt_name=None,**keywords):
+
+def spawn_sandbox(mycommand, opt_name=None, **keywords):
        if not sandbox_capable:
-               return spawn_bash(mycommand,opt_name=opt_name,**keywords)
+               return spawn_bash(mycommand, opt_name=opt_name, **keywords)
        args=[SANDBOX_BINARY]
        if not opt_name:
-               opt_name=mycommand.split()[0]
+               opt_name = os.path.basename(mycommand.split()[0])
        args.append(mycommand)
-       if not uid:
-               uid=os.getuid()
-       try:
-               os.chown(SANDBOX_PIDS_FILE,uid,portage_data.portage_gid)
-               os.chmod(SANDBOX_PIDS_FILE,0664)
-       except SystemExit, e:
-               raise
-       except:
-               pass
-       return spawn(args,uid=uid,opt_name=opt_name,**keywords)
-
-# base spawn function
-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):
-       if type(mycommand)==types.StringType:
-               mycommand=mycommand.split()
-       myc = mycommand[0]
-       if not os.path.isabs(myc) or not os.access(myc, os.X_OK):
-               if not path_lookup:
-                       return None
-               myc = find_binary(myc)
-               if myc == None:
-                       return None
-               
-       mypid=[]
-       if logfile:
-               pr,pw=os.pipe()
-               mypid.extend(spawn(('tee','-i','-a',logfile),returnpid=True,fd_pipes={0:pr,1:1,2:2}))
-               retval=os.waitpid(mypid[-1],os.WNOHANG)[1]
-               if retval != 0:
-                       # he's dead jim.
-                       if (retval & 0xff)==0:
-                               return (retval >> 8) # exit code
-                       else:
-                               return ((retval & 0xff) << 8) # signal
-               if not fd_pipes:
-                       fd_pipes={}
-                       fd_pipes[0] = 0
-               fd_pipes[1]=pw
-               fd_pipes[2]=pw
-               
-       if not opt_name:
-               opt_name = mycommand[0]
-       myargs=[opt_name]
-       myargs.extend(mycommand[1:])
-       mypid.append(os.fork())
-       if mypid[-1] == 0:
-               # this may look ugly, but basically it moves file descriptors around to ensure no
-               # handles that are needed are accidentally closed during the final dup2 calls.
-               trg_fd=[]
-               if type(fd_pipes)==types.DictType:
-                       src_fd=[]
-                       k=fd_pipes.keys()
-                       k.sort()
-                       for x in k:
-                               trg_fd.append(x)
-                               src_fd.append(fd_pipes[x])
-                       for x in range(0,len(trg_fd)):
-                               if trg_fd[x] == src_fd[x]:
-                                       continue
-                               if trg_fd[x] in src_fd[x+1:]:
-                                       new=os.dup2(trg_fd[x],max(src_fd) + 1)
-                                       os.close(trg_fd[x])
-                                       try:
-                                               while True: 
-                                                       src_fd[s.index(trg_fd[x])]=new
-                                       except SystemExit, e:
-                                               raise
-                                       except:
-                                               pass
-                       for x in range(0,len(trg_fd)):
-                               if trg_fd[x] != src_fd[x]:
-                                       os.dup2(src_fd[x], trg_fd[x])
-               else:
-                       trg_fd=[0,1,2]
-               for x in range(0,max_fd_limit):
-                       if x not in trg_fd:
-                               try: 
-                                       os.close(x)
-                               except SystemExit, e:
-                                       raise
-                               except:
-                                       pass
-               # note this order must be preserved- can't change gid/groups if you change uid first.
-               if gid:
-                       os.setgid(gid)
-               if groups:
-                       os.setgroups(groups)
-               if uid:
-                       os.setuid(uid)
-               if umask:
-                       os.umask(umask)
+       return spawn(args, opt_name=opt_name, **keywords)
+
+
+# We need to make sure that any processes spawned are killed off when
+# we exit. spawn() takes care of adding and removing pids to this list
+# as it creates and cleans up processes.
+spawned_pids = []
+def cleanup():
+       while spawned_pids:
+               pid = spawned_pids.pop()
                try:
-                       # XXX: We would do this to stop ebuild.sh from getting any
-                       # XXX: output, and consequently, we'd get to handle the sigINT.
-                       #os.close(sys.stdin.fileno())
-                       pass
-               except SystemExit, e:
-                       raise
-               except:
+                       if os.waitpid(pid, os.WNOHANG) == (0, 0):
+                               os.kill(pid, signal.SIGTERM)
+                               os.waitpid(pid, 0)
+               except OSError:
+                       # This pid has been cleaned up outside
+                       # of spawn().
                        pass
+atexit.register(cleanup)
 
+
+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):
+
+       # mycommand is either a str or a list
+       if isinstance(mycommand, str):
+               mycommand = mycommand.split()
+
+       # 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 (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:
+                       return -1
+
+       # If we haven't been told what file descriptors to use
+       # default to propogating our stdin, stdout and stderr.
+       if fd_pipes is None:
+               fd_pipes = {0:0, 1:1, 2:2}
+
+       # 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
+
+       pid = os.fork()
+
+       if not pid:
                try:
-                       #print "execing", myc, myargs
-                       os.execve(myc,myargs,env)
-               except SystemExit, e:
-                       raise
+                       _exec(binary, mycommand, opt_name, fd_pipes,
+                             env, gid, groups, uid, umask)
                except Exception, e:
-                       raise str(e)+":\n   "+myc+" "+string.join(myargs)
-               # If the execve fails, we need to report it, and exit
-               # *carefully* --- report error here
-               os._exit(1)
-               sys.exit(1)
-               return # should never get reached
+                       # We need to catch _any_ exception so that it doesn't
+                       # propogate out of this function and cause exiting
+                       # with anything other than os._exit()
+                       sys.stderr.write("%s:\n   %s\n" % (e, " ".join(mycommand)))
+                       os._exit(1)
 
+       # Add the pid to our local and the global pid lists.
+       mypids.append(pid)
+       spawned_pids.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(pr)
                os.close(pw)
-       
+
+       # If the caller wants to handle cleaning up the processes, we tell
+       # it about all processes that were created.
        if returnpid:
-               global spawned_pids
-               spawned_pids.append(mypid[-1])
-               return mypid
-       while len(mypid):
-               retval=os.waitpid(mypid[-1],0)[1]
-               if retval != 0:
-                       for x in mypid[0:-1]:
-                               try:
-                                       os.kill(x,signal.SIGTERM)
-                                       if os.waitpid(x,os.WNOHANG)[1] == 0:
-                                               # feisty bugger, still alive.
-                                               os.kill(x,signal.SIGKILL)
-                                       os.waitpid(x,0)
-                               except OSError, oe:
-                                       if oe.errno not in (10,3):
-                                               raise oe
-                       
-                       # at this point we've killed all other kid pids generated via this call.
-                       # return now.
-                       
-                       if (retval & 0xff)==0:
-                               return (retval >> 8) # return exit code
-                       else:
-                               return ((retval & 0xff) << 8) # interrupted by signal
-               else:
-                       mypid.pop(-1)
+               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]
+
+               # When it's done, we can remove it from the
+               # global pid list as well.
+               spawned_pids.remove(pid)
+
+               if retval:
+                       # If it failed, kill off anything else that
+                       # isn't dead yet.
+                       for pid in mypids:
+                               if os.waitpid(pid, os.WNOHANG) == (0,0):
+                                       os.kill(pid, signal.SIGTERM)
+                                       os.waitpid(pid, 0)
+                               spawned_pids.remove(pid)
+
+                       # 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 find_binary(myc):
-       p=os.getenv("PATH")
-       if p == None:
-               return None
-       for x in p.split(":"):
-               # if it exists, and is executable
-               if os.access("%s/%s" % (x,myc), os.X_OK):
-                       return "%s/%s" % (x,myc)
 
-       return None
+def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask):
 
+       # If the process we're creating hasn't been given a name
+       # assign it the name of the executable.
+       if not opt_name:
+               opt_name = os.path.basename(binary)
 
+       # Set up the command's argument list.
+       myargs = [opt_name]
+       myargs.extend(mycommand[1:])
+
+       # Set up the command's pipes.
+       my_fds = {}
+       # To protect from cases where direct assignment could
+       # clobber needed fds ({1:2, 2:1}) we first dupe the fds
+       # into unused fds.
+       for fd in fd_pipes:
+               my_fds[fd] = os.dup(fd_pipes[fd])
+       # Then assign them to what they should be.
+       for fd in my_fds:
+               os.dup2(my_fds[fd], fd)
+       # Then close _all_ fds that haven't been explictly
+       # requested to be kept open.
+       for fd in range(max_fd_limit):
+               if fd not in my_fds:
+                       try:
+                               os.close(fd)
+                       except OSError:
+                               pass
+
+       # Set requested process permissions.
+       if gid:
+               os.setgid(gid)
+       if groups:
+               os.setgroups(groups)
+       if uid:
+               os.setuid(uid)
+       if umask:
+               os.umask(umask)
+
+       # And switch to the new process.
+       os.execve(binary, myargs, env)
+
+
+def find_binary(binary):
+       for path in os.getenv("PATH", "").split(":"):
+               filename = "%s/%s" % (path, binary)
+               if os.access(filename, os.X_OK) and os.path.isfile(filename):
+                       return filename
+       return None