core/larch/cli/backend.py

460 lines
13 KiB
Python

# 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()