#!/usr/bin/python3 from __future__ import print_function import errno import getpass import grp import imp import os import pwd import signal import subprocess import sys import sysconfig import traceback import time import debconf import PAM sys.path.insert(0, '/usr/lib/ubiquity') from ubiquity import gsettings, osextras from ubiquity.casper import get_casper from ubiquity.debconfcommunicator import DebconfCommunicator import ubiquity.frontend from ubiquity.misc import create_bool, utf8 logfile = None def log(msg): print('ubiquity-dm: ' + msg, file=logfile, flush=True) def _pam_conv(auth, query_list, userData): resp = [] for query, type in query_list: if type == PAM.PAM_PROMPT_ECHO_ON: val = input(query) resp.append((val, 0)) elif type == PAM.PAM_PROMPT_ECHO_OFF: val = getpass.getpass(query) resp.append((val, 0)) elif type in (PAM.PAM_PROMPT_ERROR_MSG, PAM.PAM_PROMPT_TEXT_INFO): print(query) resp.append(('', 0)) else: return None return resp def set_locale(): db = DebconfCommunicator('ubiquity', cloexec=True) locale = '' try: locale = db.get('debian-installer/locale') except debconf.DebconfError: pass db.shutdown() if not locale: return with open('/etc/default/locale', 'w') as default_locale: print('LANG="%s"' % locale, file=default_locale) with open('/etc/environment') as environment: environment_lines = environment.readlines() with open('/etc/environment', 'w') as environment: seen_lang = False for line in environment_lines: if line.startswith('LANG='): print('LANG="%s"' % locale, file=environment) seen_lang = True else: print(line.rstrip('\n'), file=environment) if not seen_lang: print('LANG="%s"' % locale, file=environment) with open('/etc/locale.gen', 'w') as locale_gen: print('%s UTF-8' % locale, file=locale_gen) subprocess.call(['/usr/sbin/locale-gen', locale], stdout=logfile, stderr=logfile) def add_ubiquity_kdedir(): os.environ['KDEDIRS'] = '/usr/share/ubiquity/qt:' + \ os.environ.get('KDEDIRS', '') class XStartupError(EnvironmentError): pass class MissingProgramError(EnvironmentError): pass class SignalWatcher: def __init__(self, owner, program, interface, object_path, signal, expected): self.owner = owner self.program = program self.connection = None self.interface = interface self.object_path = object_path self.signal = signal self.expected = expected self.processes = [] from gi.repository import GLib, Gio owner.drop_privileges() self.loop = GLib.MainLoop() Gio.bus_get(Gio.BusType.SESSION, None, self.on_got_bus, None) def signal_timeout(self, user_data): log("SignalWatcher: signal timed out, continuing with ubiquity-dm") self.loop.quit() def on_got_bus(self, source, result, user_data): try: from gi.repository import GLib, Gio self.connection = Gio.bus_get_finish(result) self.connection.signal_subscribe(None, self.interface, self.signal, self.object_path, None, Gio.DBusSignalFlags.NONE, self.on_signal, None) self.processes.append(subprocess.Popen( [self.program], stdin=None, stdout=logfile, stderr=logfile)) self.owner.regain_privileges() GLib.timeout_add_seconds(5, self.signal_timeout, None) except Exception: log("failed to ensure xsettings plugin was started:") log(traceback.format_exc()) self.loop.quit() def on_signal(self, connection, sender, path, interface, signal, params, user_data): (plugin, ) = params # log ('on_signal: got %s' % plugin) if plugin == "xsettings": self.loop.quit() def run(self): self.loop.run() return self.processes class DM: def __init__(self, vt, display, default_username): self.auth = PAM.pam() self.vt = vt self.display = display self.server_started = False self.username = get_casper('USERNAME', default_username) try: self.uid, self.gid = pwd.getpwnam(self.username)[2:4] except KeyError: import syslog syslog.syslog('Could not find %s, falling back to root.' % self.username) self.username = 'root' self.uid, self.gid = 0, 0 self.homedir = pwd.getpwnam(self.username)[5] self.uid = int(self.uid) self.gid = int(self.gid) self.groups = [] for g in grp.getgrall(): if self.username in g[3] or g[0] == self.username: self.groups.append(g[2]) # Look for a frontend module; we won't actually use it (yet), but # this lets us find out which window manager etc. to launch. Be # careful that importing this here will cause the underlying library # to try to talk to the X server, which won't go well. frontend_names = ['gtk_ui', 'kde_ui'] self.frontend = None for f in frontend_names: try: imp.find_module(f, ubiquity.frontend.__path__) self.frontend = f break except ImportError: pass else: raise AttributeError('No frontend available; tried %s' % ', '.join(frontend_names)) db = DebconfCommunicator('ubiquity', cloexec=True) try: self.force_failsafe = create_bool( db.get('ubiquity/force_failsafe_graphics')) except debconf.DebconfError: self.force_failsafe = False db.shutdown() def sigusr1_handler(self, signum, frame): self.server_started = True def active_vt(self): import fcntl import array console = os.open('/dev/tty0', os.O_RDONLY | os.O_NOCTTY) try: VT_GETSTATE = 0x5603 vt_stat = array.array('H', [0, 0, 0]) fcntl.ioctl(console, VT_GETSTATE, vt_stat) return vt_stat[0] finally: os.close(console) def drop_privileges(self): os.setgroups(self.groups) os.setresgid(self.gid, self.gid, 0) os.setresuid(self.uid, self.uid, 0) def regain_privileges(self): os.setresuid(0, 0, 0) os.setresgid(0, 0, 0) os.setgroups([]) def server_preexec(self): signal.signal(signal.SIGUSR1, signal.SIG_IGN) def run_hooks(self, hookdir): if os.path.isdir(hookdir): # Exclude hooks containing '.', so that *.dpkg-* et al are avoided. hooks = [entry for entry in os.listdir(hookdir) if '.' not in entry] for hookentry in hooks: hook = os.path.join(hookdir, hookentry) subprocess.call( hook, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges) def pam_open_session(self): self.auth.start('su') self.auth.set_item(PAM.PAM_USER, self.username) self.auth.set_item(PAM.PAM_CONV, _pam_conv) self.auth.putenv('XDG_SESSION_CLASS=greeter') self.auth.putenv('XDG_SEAT=seat0') # at the time pam_open_session is called self.vt is the # correct vt: either the one originally passed as cmd line # arg or as determined by active_vt() # # self.vt is of the form str("vt10") self.auth.putenv('XDG_VTNR=%s' % self.vt[2:]) self.auth.authenticate() self.auth.open_session() os.environ.update( [i.split('=', 1) for i in self.auth.getenvlist()]) def pam_close_session(self): if self.auth: self.auth.close_session() self.auth = None def run(self, *program): # Extract the program basename to see if we are in oem-config or # ubiquity. program_basename = os.path.basename(program[0]) extras = [] null = open('/dev/null', 'w') log('starting') signal.signal(signal.SIGUSR1, self.sigusr1_handler) signal.signal(signal.SIGTTIN, signal.SIG_IGN) signal.signal(signal.SIGTTOU, signal.SIG_IGN) servercommand = ['X', '-br', '-ac', '-noreset', '-nolisten', 'tcp'] log('plymouth') try: plymouth_running = subprocess.call(['plymouth', '--ping']) == 0 except OSError: plymouth_running = False if plymouth_running: subprocess.call(['plymouth', 'deactivate']) if subprocess.call(['plymouth', '--has-active-vt']) == 0: self.vt = 'vt%d' % self.active_vt() servercommand.extend(['-background', 'none']) else: subprocess.call(['plymouth', 'quit']) plymouth_running = False servercommand.extend([self.vt, self.display]) log('start X {}'.format(servercommand)) for attempt in ('main', 'fbdev', 'vesa'): command = list(servercommand) if attempt == 'main' and self.force_failsafe: continue elif attempt != 'main': # TODO cjwatson 2010-02-11: This is a bodge. The # duplication is nasty, but fortunately bullet-proof X # actually turns out not to be very complicated nowadays. # Most of the complexity is in the fallback session, which I # haven't attempted to integrate here, so you won't get # things like interactive reconfiguration. I believe Evan # is working on doing that, but is blocked on a couple of # Upstart bugs; once all that's resolved, we should back # this out. if attempt == 'fbdev' and not os.path.exists('/dev/fb0'): continue xorg_conf_failsafe = '/etc/X11/xorg.conf.failsafe' command.extend(['-config', xorg_conf_failsafe]) command.extend(['-logfile', '/var/log/Xorg.%s.log' % attempt]) with open(xorg_conf_failsafe, 'w') as xorg_conf_failsafe_file: print('''\ Section "Device" \tIdentifier "Configured Video Device" \tDriver "%s" EndSection Section "Monitor" \tIdentifier "Configured Monitor" EndSection Section "Screen" \tIdentifier "Default Screen" \tMonitor "Configured Monitor" \tDevice "Configured Video Device" EndSection ''' % attempt, file=xorg_conf_failsafe_file) server = subprocess.Popen( command, stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.server_preexec) # Really we should select on a pipe or something, but it's not # worth the effort for now. try: timeout = 60 while not self.server_started: status = server.poll() if type(status) is int and status != 0: if plymouth_running: subprocess.call(['plymouth', 'quit']) raise XStartupError('X server exited with return ' 'code ' + str(status)) if timeout == 0: if plymouth_running: subprocess.call(['plymouth', 'quit']) raise XStartupError('X server failed to start after 60' ' seconds') time.sleep(1) timeout -= 1 if plymouth_running: subprocess.call(['plymouth', 'quit', '--retain-splash']) except XStartupError: if attempt == 'vesa': raise if self.server_started: break log('set vars') os.environ['DISPLAY'] = self.display os.environ['HOME'] = self.homedir # Give ubiquity a UID and GID that it can drop privileges to. os.environ['PKEXEC_UID'] = str(self.uid) os.environ['GVFS_DISABLE_FUSE'] = '1' # Overlay scrollbars are now a gtk module os.environ['GTK_MODULES'] = 'overlay-scrollbar' log('pam_open_session') self.pam_open_session() # run simple, custom scripts during install time if program_basename == 'ubiquity': log('dm-scripts') self.run_hooks('/usr/lib/ubiquity/dm-scripts/install') # run simple, custom scripts during oem-config if program_basename == 'oem-config-wrapper': log('oem dm-scripts') self.run_hooks('/usr/lib/ubiquity/dm-scripts/oem') # Session bus, apparently needed by most interfaces now if ('DBUS_SESSION_BUS_ADDRESS' not in os.environ and osextras.find_on_path('dbus-launch')): log('dbus') dbus_subp = subprocess.Popen( ['dbus-launch', '--exit-with-session'], stdin=null, stdout=subprocess.PIPE, stderr=logfile, preexec_fn=self.drop_privileges, universal_newlines=True) for line in dbus_subp.stdout: try: name, value = line.rstrip('\n').split('=', 1) os.environ[name] = value except ValueError: pass dbus_subp.stdout.close() dbus_subp.wait() # dconf writer if os.path.exists("/usr/lib/dconf/dconf-service"): log('dconf-service') extras.append(subprocess.Popen( ['/usr/lib/dconf/dconf-service'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) # Accessibility infrastructure proc_cmdline = [] with open('/proc/cmdline', 'r') as fp: proc_cmdline = fp.readline().split() log('start frontend {}'.format(self.frontend)) if self.frontend == 'gtk_ui': # Set a desktop wallpaper. visual_a11y = 'access=v' in proc_cmdline background_image = None for background in ( '/usr/share/xfce4/backdrops/xubuntu-wallpaper.png', '/usr/share/backgrounds/' 'ubuntustudio/ubuntustudio-default.png', '/usr/share/lubuntu/wallpapers/' 'lubuntu-default-wallpaper.png'): exists = os.access(background, os.R_OK) if exists: background_image = background break accessibility = False if gsettings._gsettings_exists(): accessibility = gsettings.get( 'org.gnome.desktop.interface', 'toolkit-accessibility', self.username) # Set gsettings keys gsettings_keys = [ ('org.gnome.desktop.lockdown', 'disable-lock-screen', 'true'), ('org.gnome.desktop.lockdown', 'disable-user-switching', 'true'), ('org.gnome.settings-daemon.plugins.background', 'active', 'true'), ('org.gnome.desktop.background', 'draw-background', 'true'), ('org.gnome.desktop.background', 'show-desktop-icons', 'false'), ('org.gnome.metacity', 'compositing-manager', 'true'), ('org.gnome.desktop.wm.preferences', 'num-workspaces', '1'), ] # Setting a wallpaper image, or solid color. if visual_a11y: gsettings_keys.append( ('org.gnome.desktop.background', 'picture-options', 'none')) gsettings_keys.append( ('org.gnome.desktop.background', 'picture-uri', "''")) if osextras.find_on_path('marco'): gsettings_keys = [ ('org.mate.lockdown', 'disable-lock-screen', 'true'), ('org.mate.lockdown', 'disable-user-switching', 'true'), ('org.mate.SettingsDaemon.plugins.background', 'active', 'true'), ('org.mate.background', 'draw-background', 'true'), ('org.mate.background', 'show-desktop-icons', 'false'), ('org.mate.Marco.general', 'compositing-manager', 'true'), ('org.mate.Marco.general', 'num-workspaces', '1'), ] # Setting a wallpaper image, or solid color. if visual_a11y: gsettings_keys.append( ('org.mate.background', 'picture-options', 'none')) gsettings_keys.append( ('org.mate.background', 'picture-filename', "''")) if osextras.find_on_path('gnome-shell'): gsettings_keys.append( ('org.gnome.settings-daemon.plugins.background', 'active', 'false')) gsettings_keys.remove( ('org.gnome.desktop.wm.preferences', 'num-workspaces', '1')) for gs_schema, gs_key, gs_value in gsettings_keys: subprocess.call( ['gsettings', 'set', gs_schema, gs_key, gs_value], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges) usd = '/usr/lib/unity-settings-daemon/unity-settings-daemon' gsd = '/usr/lib/gnome-settings-daemon/gnome-settings-daemon' msd = '/usr/bin/mate-settings-daemon' if osextras.find_on_path(usd): xsettings = SignalWatcher(self, usd, "org.gnome.SettingsDaemon", "/org/gnome/SettingsDaemon", "PluginActivated", "xsettings") # the SignalWatcher will run until the signal is seen... extras.extend(xsettings.run()) # At this point we're sure the usd xsettings plugin is # available, we can continue setting up the session. elif osextras.find_on_path(gsd): extras.append(subprocess.Popen( [gsd], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) elif osextras.find_on_path(msd): extras.append(subprocess.Popen( [msd], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) elif background_image and osextras.find_on_path('feh'): subprocess.call( ['feh', '--bg-fill', background_image], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges) if (accessibility or 'maybe-ubiquity' in proc_cmdline or 'only-ubiquity' in proc_cmdline or program_basename == 'oem-config-wrapper'): launcher = '/usr/lib/at-spi2-core/at-spi-bus-launcher' if os.path.exists(launcher): extras.append(subprocess.Popen( [launcher, '--launch-immediately'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) os.environ['GTK_MODULES'] += os.pathsep + 'gail' if osextras.find_on_path('gnome-shell'): wm_cmd = ['gnome-shell', '--sm-disable', '--mode=ubiquity'] elif osextras.find_on_path('marco'): wm_cmd = ['marco', '--sm-disable'] elif osextras.find_on_path('metacity'): wm_cmd = ['metacity', '--sm-disable'] elif osextras.find_on_path('xfwm4'): wm_cmd = ['xfwm4', '--compositor=off', '--sm-client-disable'] elif osextras.find_on_path('matchbox-window-manager'): wm_cmd = ['matchbox-window-manager'] elif osextras.find_on_path('openbox-lubuntu'): wm_cmd = ['openbox-lubuntu'] elif osextras.find_on_path('openbox'): wm_cmd = ['openbox'] elif osextras.find_on_path('compiz'): wm_cmd = ['compiz', '--sm-disable', 'decor', 'resize', 'place', 'move'] else: raise MissingProgramError( 'No window manager found (tried ' 'marco, metacity, xfwm4, matchbox-window-manager, ' 'openbox-lubuntu, openbox, compiz)') wm = subprocess.Popen( wm_cmd, stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges) if osextras.find_on_path('xfsettingsd'): extras.append(subprocess.Popen( ['xprop', '-root', '-format', '_NET_NUMBER_OF_DESKTOPS', '32c', '-set', '_NET_NUMBER_OF_DESKTOPS', '1'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) extras.append(subprocess.Popen( ['xfsettingsd', '--sm-client-disable'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) if osextras.find_on_path('lxsession'): extras.append(subprocess.Popen( ['lxsession', '-s', 'Lubuntu', '-e', 'LXDE', '-a'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) if os.path.exists('/usr/lib/ubiquity/panel'): if ("openbox-lubuntu" not in wm_cmd and "openbox" not in wm_cmd and "gnome-shell" not in wm_cmd): multiarchdir = os.path.split( sysconfig.get_config_var('multiarchsubdir'))[-1] indicators = list(filter(os.path.isfile, [ os.path.join('/usr/lib', multiarchdir, i) for i in ( 'indicator-application/' 'indicator-application-service', 'indicator-session/indicator-session-service', 'indicator-sound/indicator-sound-service', 'indicator-bluetooth/indicator-bluetooth-service', 'indicator-keyboard/indicator-keyboard-service', 'indicator-keyboard-service', 'indicator-power/indicator-power-service', )])) extras.append(subprocess.Popen( ['/usr/lib/ubiquity/panel'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) for indicator in indicators: extras.append(subprocess.Popen( [indicator], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) if (osextras.find_on_path('nm-applet') and "gnome-shell" not in wm_cmd): extras.append(subprocess.Popen( ['nm-applet'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) if osextras.find_on_path('ibus-daemon'): extras.append(subprocess.Popen( ['ibus-daemon'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) # Simply start bluetooth-applet, ubiquity-bluetooth-agent will # override it from casper to make sure it also covers the regular # live session if osextras.find_on_path('bluetooth-applet'): extras.append(subprocess.Popen( ['bluetooth-applet'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) # Accessibility tools if accessibility: # FIXME: launch onboard, when touch screen detected if 'access=m2' in proc_cmdline: if osextras.find_on_path('onboard'): extras.append(subprocess.Popen( ['onboard'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) else: if osextras.find_on_path('orca'): time.sleep(15) extras.append(subprocess.Popen( ['orca'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) elif self.frontend == 'kde_ui': # Force Qt5 KDE theming to load for kwin and friends. os.environ["QT_QPA_PLATFORMTHEME"] = "kde" if 'access=v1' not in proc_cmdline: log('paint background') path = \ '/usr/share/wallpapers/Next/contents/images/2560x1600.png' extras.append(subprocess.Popen( ['ubiquity-qtsetbg', path], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges)) log("add_ubiquity_kdedir") add_ubiquity_kdedir() log('start kde4breeze') if osextras.find_on_path('kf5-config'): output = subprocess.check_output( ['kf5-config', '--path', 'lib'], preexec_fn=self.drop_privileges) output = output.decode() output = output.replace('\n', '') output = output.split(':')[1] breeze = subprocess.Popen( [output + '/kconf_update_bin/kde4breeze'], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges) ret = breeze.wait() log('kde4breeze exited with code {}'.format(ret)) if ret != 0: raise else: raise log('start kwin') if osextras.find_on_path('kwin'): wm_cmd = ['kwin'] elif osextras.find_on_path('kwin_x11'): wm_cmd = ['kwin_x11'] wm = subprocess.Popen( wm_cmd, stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges) log('start greeter') greeter = subprocess.Popen( program, stdin=null, stdout=logfile, stderr=logfile) ret = greeter.wait() log('greeter exited with code {}'.format(ret)) reboot = False if ret != 0: db = DebconfCommunicator('ubiquity', cloexec=True) try: error_cmd = db.get('ubiquity/failure_command') if error_cmd: subprocess.call(['sh', '-c', error_cmd]) except debconf.DebconfError: pass reboot = False try: if '--automatic' in program: reboot = db.get('ubiquity/reboot_on_failure') == 'true' except debconf.DebconfError: pass if reboot: question = 'ubiquity/install_failed_reboot' else: question = 'ubiquity/install_failed' title = '' message = '' try: title = utf8(db.metaget(question, 'description'), errors='replace') message = utf8(db.metaget(question, 'extended_description'), errors='replace') except debconf.DebconfError: pass db.shutdown() if title and message: if self.frontend == 'gtk_ui': cmd = ['zenity', '--error', '--title=%s' % title, '--text=%s' % message] subprocess.call(cmd) elif self.frontend == 'kde_ui': cmd = ['kdialog', '--title=%s' % title, '--msgbox=%s' % message] subprocess.call(cmd) else: # Not ideal, but if we cannot let the user know what's # going on, it's best to drop them into a desktop and let # them figure it out. reboot = False # Revert gnome-settings to default, for dropping to desktop if self.frontend == 'gtk_ui' and gsettings._gsettings_exists(): for gs_schema, gs_key, gs_value in gsettings_keys: subprocess.call( ['gsettings', 'reset', gs_schema, gs_key], stdin=null, stdout=logfile, stderr=logfile, preexec_fn=self.drop_privileges) def kill_if_exists(pid, signum): try: os.kill(pid, signum) except OSError as e: if e.errno != errno.ESRCH: raise def sigalrm_handler(signum, frame): kill_if_exists(wm.pid, signal.SIGKILL) for extra in extras: kill_if_exists(extra.pid, signal.SIGKILL) kill_if_exists(wm.pid, signal.SIGTERM) for extra in extras: kill_if_exists(extra.pid, signal.SIGTERM) signal.signal(signal.SIGALRM, sigalrm_handler) signal.alarm(1) # low patience with WMs failing to exit on demand processes = set(extras) processes.add(wm) while processes: done = set() for process in processes: try: process.wait() done.add(process) except OSError as e: if e.errno == errno.EINTR: continue raise processes -= done signal.alarm(0) # Clear the console so we don't see boot-time messages on switch try: with open('/dev/tty' + self.vt[2:], 'r+') as vthandle: subprocess.call(['clear'], stdin=vthandle, stdout=vthandle) except IOError: pass kill_if_exists(server.pid, signal.SIGTERM) server.wait() null.close() if reboot: subprocess.Popen(['reboot']) if ret is not None and ret >= 0: return ret else: return 1 def run(vt, display, username): try: dm = DM(vt, display, username) except XStartupError: log("XStartupError") return 1 ret = dm.run(*sys.argv[4:]) if ret == 0: log("set_locale") set_locale() dm.pam_close_session() return ret def main(): global logfile if len(sys.argv) < 4: sys.stderr.write('Usage: %s <:[0-N]> ' '[]\n' % sys.argv[0]) return 1 vt, display, username = sys.argv[1:4] try: os.makedirs('/var/log/installer') except OSError as e: # be happy if someone already created the path if e.errno != errno.EEXIST: raise logfile = open('/var/log/installer/dm', 'w') try: ret = run(vt, display, username) log('Exiting with code {}'.format(ret)) except Exception: log('Failed with an exception:') log(traceback.format_exc()) return 1 if __name__ == '__main__': sys.exit(main())