# 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.07.14 # 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 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) 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: _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, default=False): _out('?>' + message) result = default if _dontask: result = True elif not _quiet: if _controlled: result = (raw_input().strip() == '??YES') else: # The character after '_' is the response key # The default will be capitalized automatically prompt = _("_yes|_no").split('|') promptkey = [word[word.index('_') + 1] for word in prompt] if default: py = prompt[0].upper() pn = prompt[1] else: py = prompt[0] pn = prompt[1].upper() resp = raw_input(" [ %s / %s ]: " % (py, pn)).strip() if resp: testkey = promptkey[1] if default else promptkey[0] resp == resp.lower() if resp == prompt[0]: result = True elif resp == prompt[1]: result = False elif testkey in resp: result = not default _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: 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: mount("/" + m, "%s/%s" % (ip, m), "--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_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 if _controlled: xfromy = ms.group(1) if not xfromy: xfromy = '' return 'pacman:%s|%s|%s' % (xfromy, ms.group(2), ms.group(4)) if nl: sys.stdout.write(' '*80 + '\r') return line.rsplit(None, 1)[0] else: return '/*/' return 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: return 'mksquashfs:' + ms.group(1) return re.sub(r'=[-\\/|]', '= ', line) else: return '/*/' return line class mkisofs_filter_gen: """Return a function to detect and process the progress output of mkisofs. """ def __call__(self, line, nl): ms = _re_mkisofs.match(line) if ms: if _controlled: return 'mkisofs:' + line sys.stdout.write(line + '\r') sys.stdout.flush() return '/*/' return 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()