#!/usr/bin/python3

import errno
import getpass
import grp
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
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.
        from importlib import util
        frontend_names = ['gtk_ui', 'kde_ui']
        self.frontend = None
        for f in frontend_names:
            if util.find_spec('ubiquity.frontend.%s' % f) is not None:
                self.frontend = f
                break
        if self.frontend is None:
            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'

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

        if osextras.find_on_path("dbus-update-activation-environment"):
            subprocess.Popen(
                [
                    "dbus-update-activation-environment",
                    "--verbose",
                    "--systemd",
                    "DISPLAY",
                ],
                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') or
                        osextras.find_on_path('budgie-wm')):
                    gsettings_keys.append(
                        ('org.gnome.settings-daemon.plugins.background',
                         'active', 'false'))
                    gsettings_keys.remove(
                        ('org.gnome.desktop.wm.preferences', 'num-workspaces',
                         '1'))
                    os.environ['XDG_SESSION_TYPE'] = 'x11'

                if (osextras.find_on_path('gnome-shell')):
                    # set this environment variable since we have
                    # session-specific GSettings overrides for Ubuntu/Shell,
                    # like the theme.
                    os.environ['XDG_CURRENT_DESKTOP'] = 'ubuntu:GNOME'
                    if (osextras.find_on_path(
                            'dbus-update-activation-environment')):
                        subprocess.Popen(
                            ['dbus-update-activation-environment',
                             '--verbose', '--systemd', 'XDG_CURRENT_DESKTOP'],
                            stdin=null, stdout=logfile, stderr=logfile,
                            preexec_fn=self.drop_privileges)

                if (osextras.find_on_path('budgie-wm')):
                    gsettings_keys.append(
                        ('org.gnome.desktop.interface', 'icon-theme',
                         "'ubuntu-mono-dark'"))
                    os.environ['XDG_CURRENT_DESKTOP'] = 'Budgie'

                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)

                msd = '/usr/bin/mate-settings-daemon'
                usd = '/usr/lib/unity-settings-daemon/unity-settings-daemon'
                gsd = ['/usr/libexec/gsd-a11y-keyboard',
                       '/usr/libexec/gsd-a11y-settings',
                       '/usr/libexec/gsd-clipboard',
                       '/usr/libexec/gsd-keyboard',
                       '/usr/libexec/gsd-media-keys',
                       '/usr/libexec/gsd-power',
                       '/usr/libexec/gsd-xsettings']

                if osextras.find_on_path(msd):
                    extras.append(subprocess.Popen(
                        [msd], stdin=null, stdout=logfile, stderr=logfile,
                        preexec_fn=self.drop_privileges))

                elif (osextras.find_on_path(usd)):
                    # Wait until xsettings plugin is activated
                    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('gnome-shell') or
                        osextras.find_on_path('budgie-wm')):
                    for gsdbinary in gsd:
                        if os.path.exists(gsdbinary):
                            extras.append(subprocess.Popen(
                                [gsdbinary],
                                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))
                    try:
                        os.environ['GTK_MODULES'] += os.pathsep + 'gail'
                    except KeyError:
                        os.environ['GTK_MODULES'] = 'gail'

            if osextras.find_on_path('gnome-shell'):
                wm_cmd = ['gnome-shell', '--sm-disable', '--mode=ubiquity']
            elif osextras.find_on_path('budgie-wm'):
                wm_cmd = ['budgie-wm', '--sm-disable']
            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 '
                    'gnome-shell, budgie-wm, 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 and
                        "xfwm4" 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') and
                    "gnome-shell" not in wm_cmd):
                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.jpg'
                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 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 <vt[1-N]> <:[0-N]> <username> <program> '
                         '[<arguments>]\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())
