summaryrefslogtreecommitdiffstats
path: root/build_tools/larch8/liblarch/rootrun.py
blob: 240f2cdbe8fd79258bb44014ea865e2b808a4238 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
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)