2 Wrappers around subprocess functionality that simulate an actual shell.
6 from wizard.shell import *
16 from wizard import util
18 wizard_bin = sys.argv[0]
20 This is the path to the wizard executable as specified
21 by the caller; it lets us recursively invoke wizard.
25 """Detects whether or not an argument list invokes a Python program."""
26 return args[0] == "python" or args[0] == "wizard"
28 def drop_priviledges(dir, log_file):
30 Checks if we are running as root. If we are, attempt to drop
31 priviledges to the user who owns ``dir``, by re-calling
32 itself using sudo with exec, such that the new process subsumes our
33 current one. If ``log_file`` is passed, the file is chown'ed
34 to the user we are dropping priviledges to, so the subprocess
39 uid = util.get_dir_uid(dir)
43 for k,v in os.environ.items():
44 if k.startswith('WIZARD_') or k == "SSH_GSSAPI_NAME":
45 args.append("%s=%s" % (k,v))
47 logging.debug("Dropping priviledges")
48 if log_file: os.chown(log_file, uid, -1)
49 os.execlp('sudo', 'sudo', '-u', '#' + str(uid), *args)
53 An advanced shell that performs logging. If ``dry`` is ``True``,
54 no commands are actually run.
56 def __init__(self, dry = False):
58 def call(self, *args, **kwargs):
60 Performs a system call. The actual executable and options should
61 be passed as arguments to this function. It will magically
62 ensure that 'wizard' as a command works. Several keyword arguments
65 :param python: explicitly marks the subprocess as Python or not Python
66 for improved error reporting. By default, we use
67 :func:`is_python` to autodetect this.
68 :param input: input to feed the subprocess on standard input.
69 :param interactive: whether or not directly hook up all pipes
70 to the controlling terminal, to allow interaction with subprocess.
71 :param strip: if ``True``, instead of returning a tuple,
72 return the string stdout output of the command with trailing newlines
73 removed. This emulates the behavior of backticks and ``$()`` in Bash.
74 Prefer to use :meth:`eval` instead (you should only need to explicitly
75 specify this if you are using another wrapper around this function).
76 :param log: if True, we log the call as INFO, if False, we log the call
77 as DEBUG, otherwise, we detect based on ``strip``.
80 :param stdin: a file-type object that will be written to or read from as a pipe.
81 :returns: a tuple of strings ``(stdout, stderr)``, or a string ``stdout``
82 if ``strip`` is specified.
85 >>> sh.call("echo", "Foobar")
87 >>> sh.call("cat", input='Foobar')
91 kwargs.setdefault("interactive", False)
92 kwargs.setdefault("strip", False)
93 kwargs.setdefault("python", None)
94 kwargs.setdefault("log", None)
95 kwargs.setdefault("stdout", subprocess.PIPE)
96 kwargs.setdefault("stdin", subprocess.PIPE)
97 kwargs.setdefault("stderr", subprocess.PIPE)
98 msg = "Running `" + ' '.join(args) + "`"
99 if kwargs["strip"] and not kwargs["log"] is True or kwargs["log"] is False:
107 if kwargs["python"] is None and is_python(args):
108 kwargs["python"] = True
109 if args[0] == "wizard":
112 kwargs.setdefault("input", None)
113 if kwargs["interactive"]:
118 stdout=kwargs["stdout"]
119 stdin=kwargs["stdin"]
120 stderr=kwargs["stderr"]
121 # XXX: There is a possible problem here where we can fill up
122 # the kernel buffer if we have 64KB of data. This shouldn't
123 # be a problem, and the fix for such case would be to write to
124 # temporary files instead of a pipe.
125 # Another possible way of fixing this is converting from a
126 # waitpid() pump to a select() pump, creating a pipe to
127 # ourself, and then setting up a
128 # SIGCHILD handler to write a single byte to the pipe to get
129 # us out of select() when a subprocess exits.
130 proc = subprocess.Popen(args, stdout=stdout, stderr=stderr, stdin=stdin)
131 if self._async(proc, args, **kwargs):
133 stdout, stderr = proc.communicate(kwargs["input"])
134 # can occur if we were doing interactive communication; i.e.
135 # we didn't pass in PIPE.
140 if not kwargs["interactive"]:
142 self._log(None, stderr)
144 self._log(stdout, stderr)
146 if kwargs["python"]: eclass = PythonCallError
147 else: eclass = CallError
148 raise eclass(proc.returncode, args, stdout, stderr)
150 return str(stdout).rstrip("\n")
151 return (stdout, stderr)
152 def _log(self, stdout, stderr):
153 """Logs the standard output and standard input from a command."""
155 logging.debug("STDOUT:\n" + stdout)
157 logging.debug("STDERR:\n" + stderr)
160 def _async(self, *args, **kwargs):
162 def callAsUser(self, *args, **kwargs):
164 Performs a system call as a different user. This is only possible
165 if you are running as root. Keyword arguments
166 are the same as :meth:`call` with the following additions:
168 :param user: name of the user to run command as.
169 :param uid: uid of the user to run command as.
173 The resulting system call internally uses :command:`sudo`,
174 and as such environment variables will get scrubbed. We
175 manually preserve :envvar:`SSH_GSSAPI_NAME`.
177 user = kwargs.pop("user", None)
178 uid = kwargs.pop("uid", None)
179 kwargs.setdefault("python", is_python(args))
180 if not user and not uid: return self.call(*args, **kwargs)
181 if os.getenv("SSH_GSSAPI_NAME"):
182 # This might be generalized as "preserve some environment"
184 args.insert(0, "SSH_GSSAPI_NAME=" + os.getenv("SSH_GSSAPI_NAME"))
185 if uid: return self.call("sudo", "-u", "#" + str(uid), *args, **kwargs)
186 if user: return self.call("sudo", "-u", user, *args, **kwargs)
187 def safeCall(self, *args, **kwargs):
189 Checks if the owner of the current working directory is the same
190 as the current user, and if it isn't, attempts to sudo to be
191 that user. The intended use case is for calling Git commands
192 when running as root, but this method should be used when
193 interfacing with any moderately complex program that depends
194 on working directory context. Keyword arguments are the
195 same as :meth:`call`.
198 return self.call(*args, **kwargs)
199 uid = os.stat(os.getcwd()).st_uid
200 # consider also checking ruid?
201 if uid != os.geteuid():
203 return self.callAsUser(*args, **kwargs)
205 return self.call(*args, **kwargs)
206 def eval(self, *args, **kwargs):
208 Evaluates a command and returns its output, with trailing newlines
209 stripped (like backticks in Bash). This is a convenience method for
210 calling :meth:`call` with ``strip``.
213 >>> sh.eval("echo", "Foobar")
216 kwargs["strip"] = True
217 return self.call(*args, **kwargs)
219 class ParallelShell(Shell):
221 Modifies the semantics of :class:`Shell` so that
222 commands are queued here, and executed in parallel using waitpid
223 with ``max`` subprocesses, and result in callback execution
226 .. method:: call(*args, **kwargs)
228 Enqueues a system call for parallel processing. If there are
229 no openings in the queue, this will block. Keyword arguments
230 are the same as :meth:`Shell.call` with the following additions:
232 :param on_success: Callback function for success (zero exit status).
233 The callback function should accept two arguments,
234 ``stdout`` and ``stderr``.
235 :param on_error: Callback function for failure (nonzero exit status).
236 The callback function should accept one argument, the
237 exception that would have been thrown by the synchronous
239 :return: The :class:`subprocess.Proc` object that was opened.
241 .. method:: callAsUser(*args, **kwargs)
243 Enqueues a system call under a different user for parallel
244 processing. Keyword arguments are the same as
245 :meth:`Shell.callAsUser` with the additions of keyword
246 arguments from :meth:`call`.
248 .. method:: safeCall(*args, **kwargs)
250 Enqueues a "safe" call for parallel processing. Keyword
251 arguments are the same as :meth:`Shell.safeCall` with the
252 additions of keyword arguments from :meth:`call`.
254 .. method:: eval(*args, **kwargs)
256 No difference from :meth:`call`. Consider having a
257 non-parallel shell if the program you are shelling out
261 def __init__(self, dry = False, max = 10):
262 super(ParallelShell, self).__init__(dry=dry)
264 self.max = max # maximum of commands to run in parallel
266 def make(no_parallelize, max):
267 """Convenience method oriented towards command modules."""
269 return DummyParallelShell()
271 return ParallelShell(max=max)
272 def _async(self, proc, args, python, on_success, on_error, **kwargs):
274 Gets handed a :class:`subprocess.Proc` object from our deferred
275 execution. See :meth:`Shell.call` source code for details.
277 self.running[proc.pid] = (proc, args, python, on_success, on_error)
278 return True # so that the parent function returns
281 Blocking call that waits for an open subprocess slot. This is
282 automatically called by :meth:`Shell.call`.
284 # XXX: This API sucks; the actuall call/callAsUser call should
285 # probably block automatically (unless I have a good reason not to)
286 # bail out immediately on initial ramp up
287 if len(self.running) < self.max: return
288 # now, wait for open pids.
290 self.reap(*os.waitpid(-1, 0))
292 if e.errno == errno.ECHILD: return
295 """Waits for all of our subprocesses to terminate."""
298 self.reap(*os.waitpid(-1, 0))
300 if e.errno == errno.ECHILD: return
302 def reap(self, pid, status):
303 """Reaps a process."""
304 # ooh, zombie process. reap it
305 proc, args, python, on_success, on_error = self.running.pop(pid)
306 # XXX: this is slightly dangerous; should actually use
308 stdout = proc.stdout.read()
309 stderr = proc.stderr.read()
310 self._log(stdout, stderr)
312 if python: eclass = PythonCallError
313 else: eclass = CallError
314 on_error(eclass(proc.returncode, args, stdout, stderr))
316 on_success(stdout, stderr)
318 # Setup a convenience global instance
321 callAsUser = shell.callAsUser
322 safeCall = shell.safeCall
325 class DummyParallelShell(ParallelShell):
326 """Same API as :class:`ParallelShell`, but doesn't actually
327 parallelize (i.e. all calls to :meth:`wait` block.)"""
328 def __init__(self, dry = False):
329 super(DummyParallelShell, self).__init__(dry=dry, max=1)
331 class Error(wizard.Error):
332 """Base exception for this module"""
335 class CallError(Error):
336 """Indicates that a subprocess call returned a nonzero exit status."""
337 #: The exit code of the failed subprocess.
339 #: List of the program and arguments that failed.
341 #: The stdout of the program.
343 #: The stderr of the program.
345 def __init__(self, code, args, stdout, stderr):
351 compact = self.stderr.rstrip().split("\n")[-1]
352 return "%s (exited with %d)\n%s" % (compact, self.code, self.stderr)
354 class PythonCallError(CallError):
356 Indicates that a Python subprocess call had an uncaught exception.
357 This exception also contains the attributes of :class:`CallError`.
359 #: Name of the uncaught exception.
361 def __init__(self, code, args, stdout, stderr):
362 if stderr: self.name = util.get_exception_name(stderr)
363 CallError.__init__(self, code, args, stdout, stderr)
366 return "PythonCallError [%s]\n%s" % (self.name, self.stderr)
368 return "PythonCallError\n%s" % self.stderr