diff options
Diffstat (limited to 'build_tools/larch8/liblarch/rootrun.py')
-rw-r--r-- | build_tools/larch8/liblarch/rootrun.py | 246 |
1 files changed, 246 insertions, 0 deletions
diff --git a/build_tools/larch8/liblarch/rootrun.py b/build_tools/larch8/liblarch/rootrun.py new file mode 100644 index 0000000..240f2cd --- /dev/null +++ b/build_tools/larch8/liblarch/rootrun.py @@ -0,0 +1,246 @@ +#!/usr/bin/env python2 +# +# rootrun.py -- Running commands as root, using pexpect with su or sudo +# +# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com) +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA +# +#------------------------------------------------------------------- +# 2010.08.08 + + +import os, threading, pexpect +from subprocess import Popen, PIPE, STDOUT +from collections import deque + +from liblarch_conf import sudo, sudoprompt + +from translation import i18n_liblarch +_ = i18n_liblarch() + + +class FnChain: + """Implements a simple dynamic function chain. + The supplied function call is executed in a new thread. Further + (pending) function calls can be added to the fifo list, self.fnlist, + which is a queue for execution when the current function returns. + When the list is empty, the chain is complete. + """ + def __init__(self, fn, *args): + self.fnlist = deque() + self.step(fn, *args) + self.pending = {} + self.flagcount = 0 + threading.Thread(target=self._fnloop, args=()).start() + + def _fnloop(self): + while True: + try: + fn, args = self.fnlist.popleft() + except IndexError: + return + fn(*args) + + def step(self, fn, *args): + self.fnlist.append((fn, args)) + + def callback(self, cb, continuation, *args): + if continuation: + self.flagcount += 1 + myflag = self.flagcount + if cb: + self.pending[myflag] = continuation + else: + myflag = None + if cb: + cb(myflag, *args) + # When the callback completes it needs to call the + # callback_done method, passing the flag and the results, + # unless the flag is None. + elif continuation: + self.step(continuation, *args) + + def callback_done(self, flag, *args): + self.step(self.pending[flag], *args) + del(self.pending[flag]) + + + +class _RootCommand: + """This class allows shell commands to be run as root by non-root users. + This is achieved using pexpect to negotiate with either su or sudo + (selectable in the configuration file). + The run method allows interaction with the running process on a + line-by-line basis, if desired, using callbacks. + The call method is a non-interactive wrapper to allow simple running + of shell scripts as root, returning completion code and output text. + """ + def __init__(self, callback_pw): + """Initialize the instance + """ + self.password = None + self.callback_pw = callback_pw + self.fnchain = None + + + def cb_done(self, flag, *args): + """Called by a callback to pass its result back. + """ + if flag: + self.fnchain.callback_done(flag, *args) + + + def run(self, cmd, end_callback, line_callback=None, cwd=None): + self.cmd = cmd + self.line_callback = line_callback + self.end_callback = end_callback + self.cwd = cwd + if self.fnchain: + debug("Previous FnChain instance not terminated") + self.fnchain = FnChain(self._run_start) + + def _run_start(self, message=None): + if sudo: + self.process = pexpect.spawn('sudo -p "%s" bash -c "echo ___ && %s"' + % (sudoprompt, self.cmd), cwd=self.cwd, timeout=None) + self.password_prompt = sudoprompt + if not message: + message = _("Please enter (sudo) password:") + + else: # use su + self.process = pexpect.spawn('su -c "echo ___ && %s"' % self.cmd, + cwd=self.cwd, timeout=None) + self.password_prompt = ':' + if not message: + message = _("Please enter root password:") + + if self.process.expect([self.password_prompt, '\r\n']) == 0: + if self.password: + self.fnchain.step(self._run_sendpw) + else: + self.fnchain.callback(self.callback_pw, self._run_pw, message) + + else: + self.fnchain.step(self._run_readloop) + + def _run_pw(self, ok, pw): + if ok: + self.password = pw + self.fnchain.step(self._run_sendpw) + else: + self.process.close(force=True) + # Avoid calling the end callback before self.fnchain is reset (threads!) + _fnchain = self.fnchain + self.fnchain = None + _fnchain.callback(self.end_callback, None, False, _("Operation cancelled")) + + def _run_sendpw(self): + self.process.sendline(self.password) + while True: + line1 = self.process.readline().strip() + if line1: + break + if line1 == '___': + self.fnchain.step(self._run_readloop) + else: + self.process.close(force=True) + self.password = None + self.fnchain.step(self._run_start, _("Incorrect password, try again:")) + + def _run_readloop(self): + """The read loop collects output from the subprocess line by line + and passes it on via the line callback. When the process has finished + the end callback is passed the complete output text. + """ + lines = "" + while True: + line = self.process.readline() + if not line: # Process finished + break + line = line.rstrip() + lines += line + '\n' + self.fnchain.callback(self.line_callback, None, line) + + # From the pexpect docs: + # If you wish to get the exit status of the child you must call the + # close() method. The exit or signal status of the child will be stored + # in self.exitstatus or self.signalstatus. If the child exited normally + # then exitstatus will store the exit return code and signalstatus will + # be None. If the child was terminated abnormally with a signal then + # signalstatus will store the signal value and exitstatus will be None. + self.process.close() + # Avoid calling the end callback before self.fnchain is reset (threads!) + _fnchain = self.fnchain + self.fnchain = None + _fnchain.callback(self.end_callback, None, self.process.exitstatus == 0, lines) + + + def interrupt(self): + # Need to start a second pexpect to interrupt as root + if sudo: + pexpect.run('sudo -p "%s" kill -2 %d"' % (self.password_prompt, self.pid), + events={self.password_prompt: self.password+'\n'}) + else: + pexpect.run('su -c "kill -2 %d"' % self.pid, + events={':': self.password+'\n'}) + +# def kill(self): +# self.process.close(force=True) + + def setpid(self, pid): + """For kill to work as desired the pid to which the signal should + be sent must first be set. + """ + self.pid = pid + + + def send(self, cmd): + self.process.sendline(cmd) + + + def call(self, cmd, cwd=None): + self.event = threading.Event() + self.run(cmd, self._end_callback0) + self.event.wait() + return self.retv + + + def _end_callback0(self, ok, text): + self.retv = (ok, text) + self.event.set() + + + +_rc = None +def init_rootrun(callback_pw): + global _rc + if not _rc: + _rc = _RootCommand(callback_pw) + return _rc + + +def runasroot(cmd, cwd=None, pwget=None): + """Wrap RootCommand within a function, which waits for completion and + returns the result as (ok (bool), output-text). + In addition to the command to be executed it is also necessary to pass + the callback for fetching the password, unless this has been supplied + to a previous call, or is known to be unnecessary (unlikely, but possible). + The cwd parameter allows the current working directory to be changed + for the call. + """ + if not _rc: + init_rootrun(pwget) + return _rc.call(cmd, cwd) |