core/larch/larch.py
2010-05-21 22:16:47 +00:00

673 lines
20 KiB
Python
Executable File

#!/usr/bin/env python
#
# larch.py
#
# (c) Copyright 2009-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.02.12
"""
This is the main module of the larch program. It needs to run most of the
larch-building commands as root, but it is generally better to run it as
a non-privileged user - it can ask for the root password when it needs it.
One advantage of running as a normal user is that all the larch
configuration files are stored by default in the user's home directory,
and it would be inconvenient to have these only accessible as root.
The graphical user interface runs as a separate process and the
communication is via pipes to the subprocess's stdio channels.
Data is passed as json objects.
The command-line user interface works in a very different way, though it
still uses signals to some extent to control execution. It does not start
a separate process, it just handles the commands passed on the command line
sequentially.
The main work of the program (the steering of the live system
construction) is handled by the imported modules and run in a separate
thread.
"""
import os, sys, traceback, re, pwd, signal, time
import __builtin__
__builtin__.base_dir = os.path.dirname(os.path.realpath(__file__))
sys.path.append("%s/modules" % base_dir)
script_dir = "%s/buildscripts" % base_dir
from subprocess import Popen, call, PIPE, STDOUT
import threading, traceback
from Queue import Queue
import pexpect
import base
from installation import Installation
from projectpage import ProjectPage
from installpage import InstallPage
from buildpage import BuildPage
from mediumpage import MediumPage
from tweakpage import TweakPage
doc_home = [
"gui_project_settings.html",
"gui_installation.html",
"gui_larchify.html",
"gui_medium.html",
"gui_tweaks.html"
]
def errout(text, quitc=0):
sys.stderr.write(text)
sys.stderr.flush()
if quitc:
exit(quitc)
__builtin__.errout = errout
def debug(text):
errout("DEBUG: " + text.strip() + "\n")
__builtin__.debug = debug
import gettext
gettext.install('larch', base_dir+'/i18n', unicode=1)
__builtin__.lang = (os.environ.get("LANGUAGE") or os.environ.get("LC_ALL")
or os.environ.get("LC_MESSAGES") or os.environ.get("LANG"))
# Some of the subprocesses must be run without i18n because the text
# output is parsed.
os.environ["LANGUAGE"] = "C"
class Command:
def __init__(self):
self.password = None
# Supershell/worker variables
self.supershell_process = None
self.worker_lock = threading.Lock()
self.worker_breakin = False
self.worker_active = False
# Keep a record of mounts
self.mounts = []
def gui_init(self):
"""The user interface must already be running before entering here.
"""
ui.init()
# Initialize gui modules
self.pages = [ProjectPage(), InstallPage(), BuildPage(),
MediumPage(), TweakPage()]
# Connect up the signals and slots
self.addconnections([
("$$$uiquit$$$", self.uiquit),
("$$$cancel$$$", self.cancel),
(":showlog*clicked", self._showlog),
(":docs*clicked", self._showdocs),
(":notebook*changed", self.pageswitch),
])
for p in self.pages:
self.addconnections(p.connect())
def addconnections(self, connlist):
for s, f in connlist:
ui.addslot(s, f)
def run(self):
# Start on the project page
self.pageswitch(0)
ui.go()
def pageswitch(self, index):
if ui.docviewer:
ui.docviewer.gohome(doc_home[index])
if (not self.pages[index].setup()) and (index != 0):
ui.command(":notebook.set", 0)
def log(self, line):
# The line should not have a newline terminator
logqueue.put(line)
def worker(self, fun, *args):
"""Functions run via this function will have their output displayed
in the progress view, and it should be possible to cancel them
manually.
This method should only be called from a background thread (i.e.
from an '&'-signal handler.
"""
self.worker_lock.acquire()
if self.worker_active:
self.worker_lock.release()
fatal_error("*BUG* Attempted to start second worker thread")
return
self.worker_breakin = False
self.worker_active = True
self.worker_lock.release()
ui.progress.start()
try:
fun(*args)
result = True
except:
result = False
#debug("WORKER: " + traceback.format_exc())
ui.progress.end()
self.worker_lock.acquire()
self.worker_active = False
self.worker_lock.release()
return result
def worker_wait(self):
"""Used by the cli (only) to wait for the completion of an operation.
It doesn't use any locking, so it should be used carefully
"""
while self.worker_active:
time.sleep(0.1)
def supershell(self, cmd):
"""Run a command as root and wait until it completes.
To be used (only) in the 'worker' thread.
Return a pair (ok, output)
Note that 'cmd' may not contain '"' characters!
To handle 'Cancel' operations, interrupts are checked before and
after execution of supershell commands.
"""
if (os.getuid() != 0) and not self.password:
while True:
okpw = ui.textLineDialog(_("Please enter root password"),
"larch: root pw", pw=True)
if okpw[0]:
self.password = okpw[1]
# Run a command as root, using the known password.
child = pexpect.spawn('su -c "echo _OK_"')
child.expect('Password:')
child.sendline(self.password)
child.expect(pexpect.EOF)
o = child.before.strip()
if o.endswith('_OK_'):
break
run_error(_("Incorrect root password"))
continue
else:
run_error( _("No root password, cancelling operation"))
assert False
self.worker_lock.acquire()
if self.worker_breakin:
self.worker_lock.release()
assert False
if self.supershell_process:
self.worker_lock.release()
fatal_error("*BUG* Attempted to start second supershell process")
assert False
self.supershell_process = self.asroot(cmd)
if not self.supershell_process:
self.worker_lock.release()
fatal_error(_("Supershell couldn't be started"))
assert False
self.log(">" + cmd)
self.worker_lock.release()
result = []
line0 = ""
while True:
ch = self.supershell_process.read(1)
if not ch:
if not line0:
break
elif ch == "\n":
continue
elif ch != "\r":
line0 += ch
continue
line = line0.strip()
line0 = ""
self.log(":-" + line)
result.append(line)
self.supershell_process.close()
rc = self.supershell_process.exitstatus
self.worker_lock.acquire()
self.supershell_process = None
interrupted = self.worker_breakin or (rc == None)
self.log("@%d" % (-1 if rc == None else rc))
self.worker_lock.release()
assert not interrupted
return (rc == 0, result)
def asroot(self, cmd):
"""Run the command as root with pexpect. Return the spawned process.
If it cannot be started return None.
"""
p = pexpect.spawn('''su -s /bin/bash -c "echo _GO_ && %s"''' % cmd,
timeout=None)
e = p.expect(["_GO_.*\n", pexpect.TIMEOUT, "Password:"], 5)
while e != 0:
if e == 2:
p.sendline(self.password)
e = p.expect(["_GO_.*\n", pexpect.TIMEOUT], 5)
else:
return None
return p
def script(self, cmd):
s = self.supershell("%s/%s" % (script_dir, cmd))
if s[0]:
return ""
else:
return "SCRIPT ERROR: (%s)\n" % cmd + "".join(s[1])
def worker_cancel(self):
"""Cancel the current worker thread.
This is achieved by inserting traps in the supershell calls.
"""
self.worker_lock.acquire()
if self.supershell_process:
self.asroot("pkill -g %d" % self.supershell_process.pid)
self.worker_breakin = True
self.worker_lock.release()
def _showlog(self):
ui.runningtab(2)
def _showdocs(self):
ui.runningtab(3)
def _browse(self, btype, path):
simple_thread(self._browse_set, btype, path)
def _browse_set(self, btype, path):
appcall = config.get(btype)
while (call(["which", appcall.split()[0]],
stdout=PIPE, stderr=STDOUT) != 0):
ok, new = ui.textLineDialog(
_("Enter '%s' application ('$' for path argument):") % btype,
text=appcall)
if ok:
appcall = new
config.set(btype, appcall)
else:
return
Popen(appcall.replace("$", path) + " &", shell=True)
def sigint(self, num, frame):
"""CONSOLE MODE ONLY: A handler for SIGINT. Tidy up properly and quit.
First kill potential running supershell process, then terminate
logger, and then, finally, terminate the ui process.
"""
self.uiquit()
def uiquit(self):
self.cancel(True)
def cancel(self, terminate=False):
# This is a bit experimental - I'm not sure the worker threads will handle
# the break-ins sensibly.
# This is not called from the worker thread, so it mustn't block.
self.qthread = simple_thread(self._quit_run, terminate)
def _quit_run(self, terminate):
# Kill any running supershell process
self.worker_cancel()
self.worker_wait()
if terminate:
# Tell the logger to quit
self.log("L:/\n")
# Wait until logger process has terminated
lthread.join()
# Tell the user interface to exit
ui.quit()
self.worker_breakin = False
command.unmount()
def mount(self, src, dst, opts=""):
if supershell("mount %s %s %s" % (opts, src, dst))[0]:
self.mounts.append(dst)
return True
return False
def unmount(self, dst=None):
if dst == None:
mounts = list(self.mounts)
elif type(dst) in (list, tuple):
mounts = list(dst)
else:
mounts = [dst]
r = True
for m in mounts:
if supershell("umount %s" % m)[0]:
self.mounts.remove(m)
else:
r = False
return r
def edit(self, fname, source=None, label=None, filter=None):
"""The file to be edited can be either an absolute path or else
relative to the profile directory. If the file already exists its
contents will be taken as the starting point, otherwise file at
the 'source' path will be read in. Whichever file is available
its contents can be filtered by an optional 'filter' function,
which takes the file contents as a string as argument and
returns the transformed contents as another string.
"""
def revert():
"""If a file is addressed by 'source' revert to its contents,
if source is "", clear the contents, otherwise revert to the
contents as they were before entering the editor.
"""
return textsrc if source != None else text0
def endfile(text):
t = text.encode("utf8")
if t and (t[-1] != "\n"):
t += "\n"
d = os.path.dirname(f)
if not os.path.isdir(d):
os.makedirs(d)
savefile(f, text)
if source != None:
textsrc = "" if source == "" else readfile(source, filter)
if fname[0] == "/":
f = fname
else:
f = os.path.join(config.get("profile"), fname)
if os.path.isfile(f):
text0 = readfile(f, filter)
else:
assert source != None # The file must be present
text0 = textsrc
if not label:
label = _("Editing '%s'") % fname
ui.editor.start(label, endfile, text0, revert)
def browser(self, path):
self._browse("filebrowser", path)
def chroot(self, cmd, mounts=[]):
ip = config.ipath()
if ip != "/":
for m in mounts:
self.mount("/" + m, "%s/%s" % (ip, m), "--bind")
cmd = "chroot %s %s" % (ip, cmd)
s = supershell(cmd)
if ip != "/":
self.unmount(["%s/%s" % (ip, m) for m in mounts])
if s[0]:
if s[1]:
return s[1]
else:
return True
return False
def check_platform(self, report=True):
arch = Popen(["cat", config.ipath(".ARCH")],
stdout=PIPE, stderr=PIPE).communicate()[0].strip()
if arch:
if arch != config.get("platform"):
if report:
config_error(_("Platform error, installed system is %s")
% arch)
return False
else:
if report:
config_error(_("No installed system found"))
return False
return True
def enable_install(self):
withinstall = (config.get("install_path") != "/")
ui.command(":notebook.enableTab", 1, withinstall)
ui.command(":notebook.enableTab", 4, withinstall)
ui.command(":pacmanops.enable", withinstall
and self.check_platform(report=False))
def get_partitions(self):
"""Get a list of available partitions (only unmounted ones
are included).
"""
# First get a list of mounted devices
mounteds = []
fh = open("/etc/mtab")
for l in fh:
dev = l.split()[0]
if dev.startswith("/dev/sd"):
mounteds.append(dev)
fh.close()
# Get a list of partitions
partlist = []
for line in supershell("sfdisk -uM -l")[1]:
if line.startswith("/dev/sd"):
fields = line.replace("*", "").replace(" - ", " ? ")
fields = fields.replace("+", "").replace("-", "").split()
#debug("F5 '%s'" % fields[5])
if fields[5] in ["0", "5", "f", "82"]:
#debug("No")
continue # ignore uninteresting partitions
if fields[0] in mounteds:
continue # ignore mounted patitions
# Keep a tuple (partition, size in MiB)
partlist.append("%-12s %12s MiB" % (fields[0], fields[3]))
return partlist
def readfile(f, filter=None):
try:
fh = open(f)
r = fh.read()
fh.close()
return filter(r) if filter else r
except:
run_error(_("Couldn't read file '%s'") % f)
return ""
def savefile(f, d):
try:
fh = open(f, "w")
fh.write(d)
fh.close()
except:
run_error(_("Couldn't save file '%s'") % f)
def config_error(text):
ui.error(text, _("CONFIG ERROR"))
__builtin__.config_error = config_error
def run_error(text):
ui.error(text, _("BUILD ERROR"))
__builtin__.run_error = run_error
def fatal_error(text):
ui.error(text, _("FATAL ERROR"), fatal=True)
command.uiquit()
__builtin__.fatal_error = fatal_error
#---------------------------
# Catch all unhandled errors.
def errorTrap(type, value, tb):
etext = "".join(traceback.format_exception(type, value, tb))
ui.error(etext, _("This error could not be handled"), fatal=True)
command.uiquit()
sys.excepthook = errorTrap
#---------------------------
def simple_thread(func, *args):
t = threading.Thread(target=func, args=args)
t.start()
return t
re_mksquashfs = re.compile(r":-\[.*\](.* ([0-9]+)%)")
re_pacman = re.compile(r":-(?:\(([^/]+/[^)]+)\))?(.*?)\[([-#]+)\]\s+([0-9]+)%")
re_mkisofs = re.compile(r":-\d\d?\.\d\d%")
def ltstart():
"""A thread function for reading log lines from the log queue and
passing them to the logger.
"""
progress = ""
line0 = ""
text0 = ""
while True:
line = logqueue.get()
logqueue.task_done()
if line.startswith("L:/"):
# Quit logging
break
mp = re_pacman.match(line)
if mp:
#xfromy, text, bars, percent = m.groups()
text = mp.group(2).strip()
xfromy = mp.group(1)
t = text if xfromy else text[:20]
if (t != text0) and line0:
logline(line0)
line0 = "(%s) %s" % (xfromy, text) if xfromy else text
ui.progress.set(line[2:])
text0 = t
continue
# Filter the output of mksquashfs
ms = re_mksquashfs.match(line)
if ms:
percent = ms.group(2)
if progress != percent:
progress = percent
ui.progress.set("mksquashfs " + ms.group(1))
continue
# Test for mkisofs progress
if re_mkisofs.match(line):
ui.progress.set("mkisofs " + line[2:])
continue
ui.progress.set()
progress = ""
if line0:
logline(line0)
line0 = ""
text0 = ""
logline(line)
def logline(line):
ui.logger.addLine(line)
ui.progress.addLine(line)
logfile.write(line + '\n')
def usage():
errout(_(" Please see '%s/%s'\n for usage information.\n")
% (base_dir, "docs/html/larch_console.html"))
__builtin__.usage = usage
if __name__ == "__main__":
# Various ui toolkits could be supported, but at the moment there
# is only support for pyqt (apart from the console)
if (len(sys.argv) > 1) and (sys.argv[1].startswith("-c")):
from console import *
guiexec = None
else:
from gui import *
if (len(sys.argv) == 1) or (sys.argv[1] == "--pyqt"):
guiexec = "quip"
else:
errout(_("ERROR: Unsupported option - '%s'\n") % sys.argv[1])
errout(_("Start without arguments or with '--pyqt' to start pyqt gui.\n"))
errout(_("The command line interface is started with '-c':\n"))
usage()
sys.exit(1)
# The queue for log lines must be available before starting the supershell
__builtin__.logqueue = Queue()
# Set up ui
__builtin__.ui = Ui(guiexec)
# This starts the command interface
__builtin__.command = Command()
__builtin__.installation = Installation()
__builtin__.config = base.LarchConfig(os.environ["HOME"])
command.gui_init()
# Start log reader thread
logfile = open(config.working_dir + "/larch_log", "w")
lthread = simple_thread(ltstart)
__builtin__.supershell = command.supershell
signal.signal(signal.SIGINT, command.sigint)
command.run()
cc = ui.mainloop()
logfile.close()
exit(cc)