summaryrefslogtreecommitdiffstats
path: root/build_tools/larch8/liblarch/rootrun.py
diff options
context:
space:
mode:
Diffstat (limited to 'build_tools/larch8/liblarch/rootrun.py')
-rw-r--r--build_tools/larch8/liblarch/rootrun.py246
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)