Don't encode the env in py3k since it expects strings for the env that's
[portage.git] / pym / portage / process.py
1 # portage.py -- core Portage functionality
2 # Copyright 1998-2009 Gentoo Foundation
3 # Distributed under the terms of the GNU General Public License v2
4 # $Id$
5
6
7 import atexit
8 import signal
9 import sys
10 import traceback
11
12 from portage import os
13 from portage import _encodings
14 from portage import _unicode_decode
15 from portage import _unicode_encode
16 import portage
17 portage.proxy.lazyimport.lazyimport(globals(),
18         'portage.util:dump_traceback',
19 )
20
21 from portage.const import BASH_BINARY, SANDBOX_BINARY, FAKEROOT_BINARY
22 from portage.exception import CommandNotFound
23
24 try:
25         import resource
26         max_fd_limit = resource.getrlimit(resource.RLIMIT_NOFILE)[0]
27 except ImportError:
28         max_fd_limit = 256
29
30 if sys.hexversion >= 0x3000000:
31         basestring = str
32
33 if os.path.isdir("/proc/%i/fd" % os.getpid()):
34         def get_open_fds():
35                 return (int(fd) for fd in os.listdir("/proc/%i/fd" % os.getpid()) \
36                         if fd.isdigit())
37 else:
38         def get_open_fds():
39                 return range(max_fd_limit)
40
41 sandbox_capable = (os.path.isfile(SANDBOX_BINARY) and
42                    os.access(SANDBOX_BINARY, os.X_OK))
43
44 fakeroot_capable = (os.path.isfile(FAKEROOT_BINARY) and
45                     os.access(FAKEROOT_BINARY, os.X_OK))
46
47 def spawn_bash(mycommand, debug=False, opt_name=None, **keywords):
48         """
49         Spawns a bash shell running a specific commands
50         
51         @param mycommand: The command for bash to run
52         @type mycommand: String
53         @param debug: Turn bash debugging on (set -x)
54         @type debug: Boolean
55         @param opt_name: Name of the spawned process (detaults to binary name)
56         @type opt_name: String
57         @param keywords: Extra Dictionary arguments to pass to spawn
58         @type keywords: Dictionary
59         """
60
61         args = [BASH_BINARY]
62         if not opt_name:
63                 opt_name = os.path.basename(mycommand.split()[0])
64         if debug:
65                 # Print commands and their arguments as they are executed.
66                 args.append("-x")
67         args.append("-c")
68         args.append(mycommand)
69         return spawn(args, opt_name=opt_name, **keywords)
70
71 def spawn_sandbox(mycommand, opt_name=None, **keywords):
72         if not sandbox_capable:
73                 return spawn_bash(mycommand, opt_name=opt_name, **keywords)
74         args=[SANDBOX_BINARY]
75         if not opt_name:
76                 opt_name = os.path.basename(mycommand.split()[0])
77         args.append(mycommand)
78         return spawn(args, opt_name=opt_name, **keywords)
79
80 def spawn_fakeroot(mycommand, fakeroot_state=None, opt_name=None, **keywords):
81         args=[FAKEROOT_BINARY]
82         if not opt_name:
83                 opt_name = os.path.basename(mycommand.split()[0])
84         if fakeroot_state:
85                 open(fakeroot_state, "a").close()
86                 args.append("-s")
87                 args.append(fakeroot_state)
88                 args.append("-i")
89                 args.append(fakeroot_state)
90         args.append("--")
91         args.append(BASH_BINARY)
92         args.append("-c")
93         args.append(mycommand)
94         return spawn(args, opt_name=opt_name, **keywords)
95
96 _exithandlers = []
97 def atexit_register(func, *args, **kargs):
98         """Wrapper around atexit.register that is needed in order to track
99         what is registered.  For example, when portage restarts itself via
100         os.execv, the atexit module does not work so we have to do it
101         manually by calling the run_exitfuncs() function in this module."""
102         _exithandlers.append((func, args, kargs))
103
104 def run_exitfuncs():
105         """This should behave identically to the routine performed by
106         the atexit module at exit time.  It's only necessary to call this
107         function when atexit will not work (because of os.execv, for
108         example)."""
109
110         # This function is a copy of the private atexit._run_exitfuncs()
111         # from the python 2.4.2 sources.  The only difference from the
112         # original function is in the output to stderr.
113         exc_info = None
114         while _exithandlers:
115                 func, targs, kargs = _exithandlers.pop()
116                 try:
117                         func(*targs, **kargs)
118                 except SystemExit:
119                         exc_info = sys.exc_info()
120                 except: # No idea what they called, so we need this broad except here.
121                         dump_traceback("Error in portage.process.run_exitfuncs", noiselevel=0)
122                         exc_info = sys.exc_info()
123
124         if exc_info is not None:
125                 if sys.hexversion >= 0x3000000:
126                         raise exc_info[0](exc_info[1]).with_traceback(exc_info[2])
127                 else:
128                         exec("raise exc_info[0], exc_info[1], exc_info[2]")
129
130 atexit.register(run_exitfuncs)
131
132 # We need to make sure that any processes spawned are killed off when
133 # we exit. spawn() takes care of adding and removing pids to this list
134 # as it creates and cleans up processes.
135 spawned_pids = []
136 def cleanup():
137         while spawned_pids:
138                 pid = spawned_pids.pop()
139                 try:
140                         if os.waitpid(pid, os.WNOHANG) == (0, 0):
141                                 os.kill(pid, signal.SIGTERM)
142                                 os.waitpid(pid, 0)
143                 except OSError:
144                         # This pid has been cleaned up outside
145                         # of spawn().
146                         pass
147
148 atexit_register(cleanup)
149
150 def spawn(mycommand, env={}, opt_name=None, fd_pipes=None, returnpid=False,
151           uid=None, gid=None, groups=None, umask=None, logfile=None,
152           path_lookup=True, pre_exec=None):
153         """
154         Spawns a given command.
155         
156         @param mycommand: the command to execute
157         @type mycommand: String or List (Popen style list)
158         @param env: A dict of Key=Value pairs for env variables
159         @type env: Dictionary
160         @param opt_name: an optional name for the spawn'd process (defaults to the binary name)
161         @type opt_name: String
162         @param fd_pipes: A dict of mapping for pipes, { '0': stdin, '1': stdout } for example
163         @type fd_pipes: Dictionary
164         @param returnpid: Return the Process IDs for a successful spawn.
165         NOTE: This requires the caller clean up all the PIDs, otherwise spawn will clean them.
166         @type returnpid: Boolean
167         @param uid: User ID to spawn as; useful for dropping privilages
168         @type uid: Integer
169         @param gid: Group ID to spawn as; useful for dropping privilages
170         @type gid: Integer
171         @param groups: Group ID's to spawn in: useful for having the process run in multiple group contexts.
172         @type groups: List
173         @param umask: An integer representing the umask for the process (see man chmod for umask details)
174         @type umask: Integer
175         @param logfile: name of a file to use for logging purposes
176         @type logfile: String
177         @param path_lookup: If the binary is not fully specified then look for it in PATH
178         @type path_lookup: Boolean
179         @param pre_exec: A function to be called with no arguments just prior to the exec call.
180         @type pre_exec: callable
181         
182         logfile requires stdout and stderr to be assigned to this process (ie not pointed
183            somewhere else.)
184         
185         """
186
187         # mycommand is either a str or a list
188         if isinstance(mycommand, basestring):
189                 mycommand = mycommand.split()
190
191         if sys.hexversion < 0x3000000:
192                 # Avoid a potential UnicodeEncodeError from os.execve().
193                 env_bytes = {}
194                 for k, v in env.items():
195                         env_bytes[_unicode_encode(k, encoding=_encodings['content'])] = \
196                                 _unicode_encode(v, encoding=_encodings['content'])
197                 env = env_bytes
198                 del env_bytes
199
200         # If an absolute path to an executable file isn't given
201         # search for it unless we've been told not to.
202         binary = mycommand[0]
203         if binary not in (BASH_BINARY, SANDBOX_BINARY, FAKEROOT_BINARY) and \
204                 (not os.path.isabs(binary) or not os.path.isfile(binary)
205             or not os.access(binary, os.X_OK)):
206                 binary = path_lookup and find_binary(binary) or None
207                 if not binary:
208                         raise CommandNotFound(mycommand[0])
209
210         # If we haven't been told what file descriptors to use
211         # default to propogating our stdin, stdout and stderr.
212         if fd_pipes is None:
213                 fd_pipes = {
214                         0:sys.stdin.fileno(),
215                         1:sys.stdout.fileno(),
216                         2:sys.stderr.fileno(),
217                 }
218
219         # mypids will hold the pids of all processes created.
220         mypids = []
221
222         if logfile:
223                 # Using a log file requires that stdout and stderr
224                 # are assigned to the process we're running.
225                 if 1 not in fd_pipes or 2 not in fd_pipes:
226                         raise ValueError(fd_pipes)
227
228                 # Create a pipe
229                 (pr, pw) = os.pipe()
230
231                 # Create a tee process, giving it our stdout and stderr
232                 # as well as the read end of the pipe.
233                 mypids.extend(spawn(('tee', '-i', '-a', logfile),
234                               returnpid=True, fd_pipes={0:pr,
235                               1:fd_pipes[1], 2:fd_pipes[2]}))
236
237                 # We don't need the read end of the pipe, so close it.
238                 os.close(pr)
239
240                 # Assign the write end of the pipe to our stdout and stderr.
241                 fd_pipes[1] = pw
242                 fd_pipes[2] = pw
243
244         pid = os.fork()
245
246         if not pid:
247                 try:
248                         _exec(binary, mycommand, opt_name, fd_pipes,
249                               env, gid, groups, uid, umask, pre_exec)
250                 except Exception as e:
251                         # We need to catch _any_ exception so that it doesn't
252                         # propogate out of this function and cause exiting
253                         # with anything other than os._exit()
254                         sys.stderr.write("%s:\n   %s\n" % (e, " ".join(mycommand)))
255                         traceback.print_exc()
256                         sys.stderr.flush()
257                         os._exit(1)
258
259         # Add the pid to our local and the global pid lists.
260         mypids.append(pid)
261         spawned_pids.append(pid)
262
263         # If we started a tee process the write side of the pipe is no
264         # longer needed, so close it.
265         if logfile:
266                 os.close(pw)
267
268         # If the caller wants to handle cleaning up the processes, we tell
269         # it about all processes that were created.
270         if returnpid:
271                 return mypids
272
273         # Otherwise we clean them up.
274         while mypids:
275
276                 # Pull the last reader in the pipe chain. If all processes
277                 # in the pipe are well behaved, it will die when the process
278                 # it is reading from dies.
279                 pid = mypids.pop(0)
280
281                 # and wait for it.
282                 retval = os.waitpid(pid, 0)[1]
283
284                 # When it's done, we can remove it from the
285                 # global pid list as well.
286                 spawned_pids.remove(pid)
287
288                 if retval:
289                         # If it failed, kill off anything else that
290                         # isn't dead yet.
291                         for pid in mypids:
292                                 if os.waitpid(pid, os.WNOHANG) == (0,0):
293                                         os.kill(pid, signal.SIGTERM)
294                                         os.waitpid(pid, 0)
295                                 spawned_pids.remove(pid)
296
297                         # If it got a signal, return the signal that was sent.
298                         if (retval & 0xff):
299                                 return ((retval & 0xff) << 8)
300
301                         # Otherwise, return its exit code.
302                         return (retval >> 8)
303
304         # Everything succeeded
305         return 0
306
307 def _exec(binary, mycommand, opt_name, fd_pipes, env, gid, groups, uid, umask,
308         pre_exec):
309
310         """
311         Execute a given binary with options
312         
313         @param binary: Name of program to execute
314         @type binary: String
315         @param mycommand: Options for program
316         @type mycommand: String
317         @param opt_name: Name of process (defaults to binary)
318         @type opt_name: String
319         @param fd_pipes: Mapping pipes to destination; { 0:0, 1:1, 2:2 }
320         @type fd_pipes: Dictionary
321         @param env: Key,Value mapping for Environmental Variables
322         @type env: Dictionary
323         @param gid: Group ID to run the process under
324         @type gid: Integer
325         @param groups: Groups the Process should be in.
326         @type groups: Integer
327         @param uid: User ID to run the process under
328         @type uid: Integer
329         @param umask: an int representing a unix umask (see man chmod for umask details)
330         @type umask: Integer
331         @param pre_exec: A function to be called with no arguments just prior to the exec call.
332         @type pre_exec: callable
333         @rtype: None
334         @returns: Never returns (calls os.execve)
335         """
336         
337         # If the process we're creating hasn't been given a name
338         # assign it the name of the executable.
339         if not opt_name:
340                 opt_name = os.path.basename(binary)
341
342         # Set up the command's argument list.
343         myargs = [opt_name]
344         myargs.extend(mycommand[1:])
345
346         # Set up the command's pipes.
347         my_fds = {}
348         # To protect from cases where direct assignment could
349         # clobber needed fds ({1:2, 2:1}) we first dupe the fds
350         # into unused fds.
351         for fd in fd_pipes:
352                 my_fds[fd] = os.dup(fd_pipes[fd])
353         # Then assign them to what they should be.
354         for fd in my_fds:
355                 os.dup2(my_fds[fd], fd)
356         # Then close _all_ fds that haven't been explictly
357         # requested to be kept open.
358         for fd in get_open_fds():
359                 if fd not in my_fds:
360                         try:
361                                 os.close(fd)
362                         except OSError:
363                                 pass
364
365         # Set requested process permissions.
366         if gid:
367                 os.setgid(gid)
368         if groups:
369                 os.setgroups(groups)
370         if uid:
371                 os.setuid(uid)
372         if umask:
373                 os.umask(umask)
374         if pre_exec:
375                 pre_exec()
376
377         # And switch to the new process.
378         os.execve(binary, myargs, env)
379
380 def find_binary(binary):
381         """
382         Given a binary name, find the binary in PATH
383         
384         @param binary: Name of the binary to find
385         @type string
386         @rtype: None or string
387         @returns: full path to binary or None if the binary could not be located.
388         """
389         
390         for path in os.getenv("PATH", "").split(":"):
391                 filename = "%s/%s" % (path, binary)
392                 if os.access(filename, os.X_OK) and os.path.isfile(filename):
393                         return filename
394         return None