summaryrefslogtreecommitdiffstats
path: root/build_tools/larch8/larch0/cli/backend.py
blob: 510491c3b2116df5cb3ce1a88d4478b2dc52d8ff (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
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
# backend.py - for the cli modules: handles processes and io
#
# (c) Copyright 2010 Michael Towers (larch42 at googlemail dot com)
#
# This file is part of the larch project.
#
#    larch 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.
#
#    larch 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 larch; if not, write to the Free Software Foundation, Inc.,
#    51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
#
#----------------------------------------------------------------------------
# 2010.11.09

# There was also the vague idea of a web interface, using a sort of state-
# based approach. Connecting to a running larch process would then require
# the ability to get the logging history, but presumably not the whole
# history on every ui update, which would need to be incremental.
# The logging function would need to be modified to accommodate this.

import os, sys, signal, atexit, __builtin__
import traceback, pwd
from subprocess import Popen, PIPE, STDOUT
import pexpect
try:
    import json as serialize
except:
    import simplejson as serialize
from config import *

def debug(text):
    sys.stderr.write("DEBUG: " + text.strip() + "\n")
    sys.stderr.flush()

sys.path.append(os.path.dirname(base_dir))
from liblarch.translation import i18n_module, lang
__builtin__._ = i18n_module(base_dir, 'larch')
__builtin__.lang = lang
# Run subprocesses without i18n in case the output is parsed.
os.environ["LANGUAGE"] = "C"


def init(app, options, app_quit=None):
    global _options, _quit_function, _log, _controlled, _dontask, _quiet
    _options = options
    _quit_function = app_quit
    _controlled = options.slave
    _dontask = options.force
    _log = None
    _quiet = False if _controlled else options.quiet

    atexit.register(sys_quit)
    if _controlled:
        _out('>-_$$_%d' % os.getpid())

    def sigint(num, frame):
        """A handler for SIGINT. Tidy up properly and quit.
        """
        errout("INTERRUPTED - killing subprocesses", 0)
        if _sub_process and _sub_process.pid:
            Popen(["pkill", "-g", str(_sub_process.pid)],
                    stdout=PIPE).communicate()
        errout("QUITTING", 2)
    signal.signal(signal.SIGINT, sigint)


    # Check no other instance of the script is running
    if os.path.isfile(LOCKFILE):
        app0 = readfile(LOCKFILE)
        if not query_yn(_(
                "larch (%s) seems to be running already."
                "\nIf you are absolutely sure this is not the case,"
                "\nyou may continue. Otherwise you should cancel."
                "\n\nShall I continue?") % app0):
            sys.exit(102)
    writefile(app, LOCKFILE)
    _log = open(LOGFILE + app, 'w')

    # For systems without /sbin and /usr/sbin in the normal PATH
    p = os.environ['PATH']
    ps = p.split(':')
    for px in ('/sbin', '/usr/sbin'):
        if px not in ps:
            p = px + ':' + p
    os.environ['PATH'] = p


def _out(text, force=False):
    """Send the string to standard output.
    How it is output depends on the '-s' command line option (whether the
    script is being run on the console or as a subprocess of another script).
    In the latter case the text will be slightly encoded - to avoid newline
    characters - and sent as a single unit.
    Otherwise output the lines as they are, but all lines except
    the first get a '--' prefix.
    """
    lines = text.encode('utf-8').splitlines()
    if _log and not text.startswith('>-'):
        # Don't log the progress report lines
        _log.write(lines[0] + '\n')
        for l in lines[1:]:
            _log.write('--' + l + '\n')

    if force or not _quiet:
        if _controlled:
            sys.stdout.write(serialize.dumps(text) + '\n')
        else:
            prefix = ''
            for line in lines:
                sys.stdout.write(prefix + line + '\n')
                prefix = '--'
        sys.stdout.flush()


def sys_quit():
    unmount()
    if _quit_function:
        _quit_function()
    if _errorcount:
        _out('!! ' + (_("The backend reported %d failed calls,"
                " you may want to investigate") % _errorcount))
    if _log:
        _log.close()
        os.remove(LOCKFILE)


def comment(text):
    _out('##' + text)


def query_yn(message):
    _out('?>' + message)
    if _dontask:
        result = True

    elif _controlled:
        result = (raw_input().strip() == '??YES')

    else:
        prompt = _("Yes:y|No:n")
        py, pn = prompt.split('|')
        respy = py.lower().split(':')
        respn = pn.lower().split(':')
        while True:
            resp = raw_input("   [ %s ]: " % prompt).strip().lower()
            if resp in respy:
                result = True
                break
            if resp in respn:
                result = False
                break

    _out('#>%s' % ('Yes' if result else 'No'))
    return result


def errout(message="ERROR", quit=1):
    _out('!>' + message, True)
    if quit:
        sys_quit()
        os._exit(quit)


def error0(message):
    errout(message, 0)
__builtin__.error0 = error0


# Catch all unhandled errors.
def errortrap(type, value, tb):
    etext = "".join(traceback.format_exception(type, value, tb))
    errout(_("Something went wrong:\n") + etext, 100)
sys.excepthook = errortrap


_sub_process = None
_errorcount = 0
def runcmd(cmd, filter=None):
    global _sub_process, _errorcount
    _out('>>' + cmd)
    _sub_process = pexpect.spawn(cmd)
    result = []
    line0 = ''
    # A normal end-of-line is '\r\n', so split on '\r' but don't
    # process a line until the next character is available.
    while True:
        try:
            line0 += _sub_process.read_nonblocking(size=256, timeout=None)
        except:
            break

        while True:
            lines = line0.split('\r', 1)
            if (len(lines) > 1) and lines[1]:
                line = lines[0]
                line0 = lines[1]
                nl = (line0[0] == '\n')
                if nl:
                    # Strip the '\n'
                    line0 = line0[1:]
                if filter:
                    nl, line = filter(line, nl)
                    if line == '/*/':
                        continue
                if nl:
                    line = line.rstrip()
                    _out('>_' + line)
                    result.append(line)
                else:
                    # Probably a progress line
                    if _controlled:
                        _out('>-' + line)
                    else:
                        sys.stdout.write(line + '\r')
                        sys.stdout.flush()

            else:
                break

    _sub_process.close()
    rc = _sub_process.exitstatus
    ok = (rc == 0)
    if not ok:
        _errorcount += 1
    _out(('>?%s' % repr(rc)) + ('' if ok else (' $$$ %s $$$' % cmd)))
    return (ok, result)


def script(cmd):
    s = runcmd("%s/%s" % (script_dir, cmd))
    if s[0]:
        return ""
    else:
        return "SCRIPT ERROR: (%s)\n" % cmd + "".join(s[1])


def chroot(ip, cmd, mnts=[], filter=None):
    if ip:
        for m in mnts:
            mdir = "%s/%s" % (ip, m)
            if not os.path.isdir(mdir):
                runcmd('mkdir -p %s' % mdir)
            mount("/" + m, mdir, "--bind")
        cmd = "chroot %s %s" % (ip, cmd)

    s = runcmd(cmd, filter)

    if ip:
        unmount(["%s/%s" % (ip, m) for m in mnts])

    if s[0]:
        if s[1]:
            return s[1]
        else:
            return True
    return False


_mounts = []
def mount(src, dst, opts=""):
    if runcmd("mount %s %s %s" % (opts, src, dst))[0]:
        _mounts.append(dst)
        return True
    return False


def unmount(dst=None):
    if dst == None:
        mnts = list(_mounts)
    elif type(dst) in (list, tuple):
        mnts = list(dst)
    else:
        mnts = [dst]

    r = True
    for m in mnts:
        if runcmd("umount %s" % m)[0]:
            _mounts.remove(m)
        else:
            r = False
    return r


def get_installation_dir():
    return os.path.realpath(_options.idir if _options.idir
            else INSTALLATION)


def get_profile():
    """Get the absolute path to the profile folder given its path in any
    acceptable form, including 'user:profile-name'
    """
    pd = (_options.profile if _options.profile
            else base_dir + '/profiles/default')
    p = pd.split(':')
    if len(p) == 1:
        pd = os.path.realpath(pd)
    else:
        try:
            pd = (pwd.getpwnam(p[0])[5] + PROFILE_DIR
                    + '/' + p[1])
        except:
            errout(_("Invalid profile: %s") % pd, quit=0)
            raise
    if not os.path.isfile(pd + '/addedpacks'):
        errout(_("Invalid profile folder: %s") % pd)
    return pd



#+++++++++++++++++++++++++++++++++++++++++
#Regular expression search strings for progress reports
import re
#lit: give []() a \-prefix
#grp: surround string in ()
#opt: surround string in []

def _lit(s):
    for c in r'[()]':
        s = s.replace(c, '\\' + c)
    return s

def _grp(s, x=''):
    return '(' + s + ')' + x

def _grp0(s, x=''):
    return '(?:' + s + ')' + x

def _opt(s, x=''):
    return '[' + s + ']' + x

re_psub = re.compile(r'\[[#-]+\]')
_re_pacman = re.compile( _grp0(_lit('(') +
                            _grp(_opt('^/', '+') + '/' + _opt('^)', '+')) +
                            _lit(')'), '?') +
                        _grp('.*?') +
                        _lit('[') + _grp(_opt('-#', '+')) + _lit(r']\s+') +
                        _grp(_opt('0-9', '+')) +
                        '%'
                        )

_re_mksquashfs = re.compile(_lit('[.*]') +
                            _grp('.* ' +
                                _grp(_opt('0-9', '+')) +
                                '%')
                            )

_re_mkisofs = re.compile(_opt(' 1') + _opt(' \d') + '\d\.\d\d%')

#-----------------------------------------
class pacman_filter_gen:
    """Return a function to detect and process the progress output of
    pacman.
    """
    def __init__(self):
        self.progress = ''

    def __call__(self, line, nl):
        ms = _re_pacman.match(line)
        if ms:
            p = ms.group(3)
            if (self.progress != p) or nl:
                self.progress = p
                xfromy = ms.group(1)
                if _controlled:
                    if not xfromy:
                        xfromy = ''
                    line = 'pacman:%s|%s|%s%%' % (xfromy, ms.group(2),
                            ms.group(4))
                elif ms.group(4) == '100':
                    line = re_psub.sub('[##########]', line)
                if nl:
                    sys.stdout.write(' '*80 + '\r')
            else:
                line = '/*/'
        return (nl, line)


class mksquashfs_filter_gen:
    """Return a function to detect and process the progress output of
    mksquashfs.
    """
    def __init__(self):
        self.progress = ''

    def __call__(self, line, nl):
        ms = _re_mksquashfs.match(line)
        if ms:
            percent = ms.group(2)
            if (self.progress != percent) or nl:
                self.progress = percent
                if _controlled:
                    line = 'mksquashfs:' + ms.group(1)
                else:
                    line = re.sub(r'=[-\\/|]', '= ', line)
            else:
                line = '/*/'
        return (nl, line)


class mkisofs_filter_gen:
    """Return a function to detect and process the progress output of
    mkisofs.
    """
    def __init__(self):
        self.running = None

    def __call__(self, line, nl):
        ms = _re_mkisofs.match(line)
        if ms:
            if _controlled:
                line = 'mkisofs:' + line
            self.running = line
            nl = False
        elif self.running:
            line = self.running + '\n' + line
            self.running = None
        return (nl, line)


def readdata(filename):
    return readfile(base_dir + '/data/' + filename)


def readfile(fpath):
    try:
        fh = open(fpath)
        text = fh.read()
        fh.close()
    except:
        errout(_("Couldn't read file: %s") % fpath)
        return None
    return text


def writefile(text, path):
    try:
        pd = os.path.dirname(path)
        if not os.path.isdir(pd):
            os.makedirs(pd)
        fh = None
        fh = open(path, 'w')
        fh.write(text)
        return True
    except:
        return False
    finally:
        if fh:
            fh.close()