#!/usr/bin/env python # # controller.py - Manages file-system access and calling of larch scripts # # (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.19 import sys, os, pwd, traceback, __builtin__ try: import json as serialize except: import simplejson as serialize from subprocess import Popen, PIPE, call import threading import re import SocketServer from config import * start_translator(switchC=False) exports = {} def add_exports(elist): for key, method in elist: exports[key] = method __builtin__.add_exports = add_exports def error0(message): sys.stderr.write('>>ERROR>>' + message + '\n') sys.stderr.flush() __builtin__.error0 = error0 class Fs: """Collect file system access methods in one class. """ def __init__(self): add_exports( ( ('fetch_layout', self.fetch_layout), ('isfile', self.isfile), ('isdir', self.isdir), ('rm_rf', self.rm_rf), ('get_partitions', self.get_partitions), ('readfile', self.readfile), ('savefile', self.savefile), ('get_docs_url', self.get_docs_url), ('oldsqf', self.oldsqf), ('oldlocales', self.oldlocales), # ('getpath', self.getpath), ('browse', self.browse)) ) def fetch_layout(self, layout_file): fh = open(base_dir + '/gui/layouts/' + layout_file) r = fh.read() fh.close() return (True, eval(r)) def rm_rf(self, path): call(['rm', '-rf', self._getpath(path)]) return (True, None) # def exists(self, path): # return os.path.exists(self._getpath(path)) # def copy(self, src, dst): # shutil.copytree(self._getpath(src), self._getpath(dst)) def oldsqf(self): return (True, os.path.isfile(self._getpath('install:' + CHROOT_SYSTEMSQF)) or os.path.isfile(self._getpath('install:' + CHROOT_DIR_MEDIUM + '/larch/system.sqf'))) def oldlocales(self): return (True, os.path.isdir(self._getpath('install:%s/locale' % CHROOT_DIR_BUILD))) def isfile(self, path): return (True, os.path.isfile(self._getpath(path))) def isdir(self, path): return (True, os.path.isdir(self._getpath(path))) def browse(self, path): fpath = self._getpath(path) if call(['mkdir', '-p', fpath]) == 0: # Start file browser at fpath call(project_manager.appget('filebrowser').replace('$', fpath) + ' &', shell=True) return (True, None) else: return (False, None) # def makedirs(self, path): # os.makedirs(self._getpath(path)) # return (True, None) def readfile(self, f): f = self._getpath(f) try: fh = open(f) r = fh.read() fh.close() return (True, r) except: return (False, _("Couldn't read file '%s'") % f) def savefile(self, f, d): f = self._getpath(f) dir = os.path.dirname(f) if not os.path.isdir(dir): os.makedirs(dir) try: fh = open(f, "w") fh.write(d) fh.close() return (True, None) except: return (False, _("Couldn't save file '%s'") % f) def _getpath(self, f): if f[0] != "/": base, f = f.split(':') f = '/' + f if base == 'base': f = base_dir + f elif base == 'profile': f = project_manager.profile_path + f elif base == 'working': f = project_manager.project_dir + f else: f = project_manager.get_ipath()[1] + f return f def get_docs_url(self, page): if lang and (len(lang) > 1): p = base_dir + ('/docs/%s/html/' % lang[0:2]) + page if os.path.isfile(p): return (True, p) return (True, base_dir + '/docs/html/' + page) 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[5:]) fh.close() # Get a list of partitions partlist = [] fh = open('/proc/partitions') for l in fh: fields = l.split() if len(fields) == 4: dev = fields[3] if dev.startswith('sd') and (dev[-1] in '0123456789'): size = (int(fields[2]) + 512) / 1024 if (size > 200) and (dev not in mounteds): # Keep a tuple (partition, size in MiB) partlist.append("%-12s %12s MiB" % ('/dev/' + dev, size)) fh.close() return (True, partlist) class LarchScripts: """This class deals with calling the larch scripts. As they must be run as root a separate dispatcher process, running as root, is used to call the actual scripts. The dispatcher is started using 'sudo'. A call will be initiated from the gui, and is sent to the dispatcher, which starts the process and returns the output when it is available. If the script is interactive, it might also require input, which can be passed via the dispatcher. While reading output from the dispatcher the gui must remain responsive, so that the view can be switched and the subprocess aborted, if desired. To achieve this a separate thread is used for reading input from the dispatcher, together with a mechanism for activating a handler in a thread-safe way, 'ui.idle_add'. """ def __init__(self): self.larch_dispatcher = None # dispatcher subprocess self.progress_waiting = None # used by progress widget def call(self, cmd, arg=[], atend=None): self.cmd = cmd self.arg = arg # Callback on completion: self.atend = atend # returns True for automatic return to normal view if self.larch_dispatcher: self.runcmd() else: # Start a socket server to handle password requests # Use a unix domain server in the abstract namespace port = '\0larch-sudopw' # Create the server self.sserver = SocketServer.UnixStreamServer(port, MyHandler) self.sst = threading.Thread(target=self.sserver.serve_forever, args=()) self.sst.start() # Handle one request #self.sserver.handle_request() # Start the larch dispatcher script os.environ['SUDO_ASKPASS'] = base_dir + '/gui/askpass.py' self._sudo_wait() dispatcher = Popen(['sudo', '-A', '-k', base_dir + '/gui/dispatcher.py'], stdin=PIPE, stdout=PIPE, stderr=PIPE) # And a thread to read its output self.istream = dispatcher.stdin self.estream = dispatcher.stderr self.t = threading.Thread(target=self.readinput, args=(dispatcher,)) self.t.start() def runcmd(self): progress.start() # initialize progress widget ui.runningtab(1) # switch view to progress widget # Run command: cx = '%s %s:%s\n' % (self.cmd, project_manager.project_dir, serialize.dumps(self.arg)) logger.addLine('++' + cx) self.istream.write(cx) self.istream.flush() r = self.geterr() if r: ui.command('infoDialog', r, 'BUG') def geterr(self): r = "" while True: rx = self.estream.readline() if rx: rx = rx.strip() else: break if rx == '!+': break if r: r +='\n' r += rx return r def readinput(self, dispatcher): ostream = dispatcher.stdout while True: line = ostream.readline() if not line: break id, line = line.rstrip().split(':', 1) try: if line[0] == '=': self._stop_server() # The dispatcher has just started, make it available self.larch_dispatcher = dispatcher # Reenable the gui, and queue the command ui.idle_add(self._dispatcher_started, self.runcmd) continue elif line[0] != '/': line = serialize.loads(line) except: line = '[[%s]]' % line ui.idle_add(self.addline, line) # cross-thread call if self.larch_dispatcher == None: self._stop_server() ui.idle_add(self._dispatcher_started, None) def _sudo_wait(self): ui.command(':larch.busy', [':larch'], True) def _dispatcher_started(self, cmd): if cmd: cmd() else: ui.command('infoDialog', ("%s:\n %s" % (ui.data('authfail'), self.geterr())), 'sudo') ui.command(':larch.busy', [':larch'], False) def _stop_server(self): # Stop the password socket-server self.sserver.shutdown() self.sserver = None def close(self): if self.larch_dispatcher: self.istream.write('quit\n') self.istream.flush() self.larch_dispatcher.wait() def interrupt(self): logger.addLine('--Terminate--') self.istream.write('kill\n') self.istream.flush() def addline(self, message): """A line has been received from the script. This must be handled in the main thread. The input lines are filtered for pacman, mksquashfs and mkisofs progress output so that appropriate progress reports can be given. """ if 'pacman:' in message: progress.set(message[2:]) if message.endswith('|100'): progress.set() message = message.rsplit('|', 1)[0].rstrip() else: return if 'mksquashfs:' in message: progress.set(message[2:]) self.progress_waiting = message return if 'mkisofs:' in message: progress.set(message[2:]) self.progress_waiting = ">_mkisofs: completed" return if self.progress_waiting: progress.set() progress.addLine(self.progress_waiting) self.progress_waiting = None progress.addLine(message) if message[0] == '/': # process finished: auto = False if self.atend: auto = self.atend(int(message[1:])) progress.end(auto) self.cmd = None elif message.startswith('?>'): # a query (yes/no) # Pop up the query reply = '??YES' if ui.command('confirmDialog', message[2:], self.cmd) else '??NO' self.istream.write(reply + '\n') self.istream.flush() def archin(self, cmd, installrepos): args = ['-p', project_manager.profile_path, '-i', project_manager.get_ipath()[1], '-c', project_manager.get('pacman_cache')] rf = project_manager.project_dir + '/pacman.conf.repos' if installrepos and os.path.isfile(rf): args += ['-r', rf] self.call('archin', args + cmd.split()) def larchify(self, oldsyssqf, oldlocales): args = ['-p', project_manager.profile_path, '-i', project_manager.get_ipath()[1],] if oldsyssqf: args.append('-o') if oldlocales: args.append('-l') self.call('larchify', args) def testmedium(self, path, atend): self.call('live_iso', ['-T', '-S', path,], atend=atend) def writemedium(self, path, args, dest=None): if dest == 'BOOTISO': args.append(path) cmd = 'boot_iso' else: if path: args += ['-S', path] else: args += ['-p', project_manager.profile_path] args += ['-i', project_manager.get_ipath()[1]] if dest != None: args.append(dest) cmd = 'live_part' else: cmd = 'live_iso' self.call(cmd, args) class MyHandler(SocketServer.StreamRequestHandler): def handle(self): self._event = threading.Event() # self.rfile is a file-like object created by the handler; # we can now use e.g. readline() instead of raw recv() calls data = self.rfile.readline().strip() if data == 'pw-get': ui.idle_add(self.dialog) self._event.wait() # Likewise, self.wfile is a file-like object used to write back # to the client self.wfile.write(self.pw) def dialog(self): ok, self.pw = ui.command('textLineDialog', ui.data('getpw'), 'sudo', '', True) if not ok: self.pw = '' self._event.set() fs = Fs() def filesystem(key, *args): return exports[key](*args) __builtin__.filesystem = filesystem __builtin__.larchscripts = LarchScripts() import project project_manager.init()