#!/usr/bin/python

import os
import sys
import subprocess
import time
import signal
import errno
import imp
import grp
import pwd

import debconf

sys.path.insert(0, '/usr/lib/ubiquity')

import ubiquity.frontend
from ubiquity import osextras
from ubiquity import gsettings
from ubiquity.misc import create_bool
from ubiquity.casper import get_casper
from ubiquity.debconfcommunicator import DebconfCommunicator

def ck_open_session(uid):
    import dbus
    bus = dbus.SystemBus ()
    manager_obj = bus.get_object ('org.freedesktop.ConsoleKit',
            '/org/freedesktop/ConsoleKit/Manager')
    manager = dbus.Interface (manager_obj, 'org.freedesktop.ConsoleKit.Manager')
    params = dbus.Array ([], signature = "(sv)")
    params.append(("unix-user", dbus.Int32(uid)))
    params.append(("session-type", dbus.String("")))
    params.append(("x11-display", dbus.String(os.environ['DISPLAY'])))
    params.append(("x11-display-device", dbus.String('/dev/tty7')))
    params.append(("is-local", dbus.Boolean(True)))
    cookie = manager.OpenSessionWithParameters(params)
    os.environ['XDG_SESSION_COOKIE'] = cookie

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

    default_locale = open('/etc/default/locale', 'w')
    print >>default_locale, 'LANG="%s"' % locale
    default_locale.close()

    environment = open('/etc/environment')
    environment_lines = environment.readlines()
    environment.close()
    environment = open('/etc/environment', 'w')
    seen_lang = False
    for line in environment_lines:
        if line.startswith('LANG='):
            print >>environment, 'LANG="%s"' % locale
            seen_lang = True
        else:
            print >>environment, line.rstrip('\n')
    if not seen_lang:
        print >>environment, 'LANG="%s"' % locale
    environment.close()

    locale_gen = open('/etc/locale.gen', 'w')
    print >>locale_gen, '%s UTF-8' % locale
    locale_gen.close()

    logfile = open('/var/log/installer/dm', 'a')
    subprocess.call(['/usr/sbin/locale-gen', locale],
                    stdout=logfile, stderr=logfile)
    logfile.close()

class XStartupError(EnvironmentError):
    pass

class MissingProgramError(EnvironmentError):
    pass

class DM:
    def __init__(self, vt, display, default_username):
        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
        os.environ['SUDO_USER'] = self.username
        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.setgid(self.gid)
        os.setuid(self.uid)

    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)
                with open('/var/log/installer/dm', 'a') as logfile:
                    subprocess.call(
                        hook, stdout=logfile, stderr=logfile,
                        preexec_fn=self.drop_privileges)

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

        null = open('/dev/null', 'w')
        try:
            os.makedirs('/var/log/installer')
        except OSError, e:
            # be happy if someone already created the path
            if e.errno != errno.EEXIST:
                raise
        logfile = open('/var/log/installer/dm', 'w')

        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']

        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.append('-nr')
            else:
                subprocess.call(['plymouth', 'quit'])
                plymouth_running = False

        servercommand.extend([self.vt, self.display])

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

                xorg_conf_failsafe_file = open(xorg_conf_failsafe, 'w')
                print >>xorg_conf_failsafe_file, '''\
Section "Device"
	Identifier	"Configured Video Device"
	Driver		"%s"
EndSection

Section "Monitor"
	Identifier	"Configured Monitor"
EndSection

Section "Screen"
	Identifier	"Default Screen"
	Monitor		"Configured Monitor"
	Device		"Configured Video Device"
EndSection
''' % attempt
                xorg_conf_failsafe_file.close()

            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

        os.environ['DISPLAY'] = self.display
        os.environ['HOME'] = self.homedir
        # Give ubiquity a UID and GID that it can drop privileges to.
        os.environ['SUDO_UID'] = str(self.uid)
        os.environ['SUDO_GID'] = str(self.gid)
        os.environ['GVFS_DISABLE_FUSE' ] = '1'

        ck_open_session(self.uid)

        # run simple, custom scripts during install time
        if program_basename == 'ubiquity':
            self.run_hooks('/usr/lib/ubiquity/dm-scripts/install')

        # run simple, custom scripts during  oem-config
        if program_basename == 'oem-config-wrapper':
            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')):
            dbus_subp = subprocess.Popen(
                ['dbus-launch', '--exit-with-session'],
                stdin=null, stdout=subprocess.PIPE, stderr=logfile,
                preexec_fn=self.drop_privileges)
            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()

        gconfd_running = False
        if (self.frontend == 'gtk_ui' and
            osextras.find_on_path('gconftool-2')):
            subprocess.call(['gconftool-2', '--spawn'],
                            stdin=null, stdout=logfile, stderr=logfile,
                            preexec_fn=self.drop_privileges)
            gconfd_running = True

        extras = []
        if self.frontend == 'gtk_ui':
            gconf_dir = ('xml:readwrite:%s' %
                os.path.expanduser('~%s/.gconf' %
                self.username))
            accessibility = False
            if (osextras.find_on_path('gconftool-2') and
               gsettings._gsettings_exists()):
                # Set number of workspaces to 1 in metacity
                # (still using gconf, GNOME: #621204)
                subprocess.call(
                    ['gconftool-2', '--config-source', gconf_dir,
                     '--type', 'int', '--set',
                     '/apps/metacity/general/num_workspaces', '1',],
                    stdin=null, stdout=logfile, stderr=logfile,
                    preexec_fn=self.drop_privileges)

                # Set gsettings keys
                gsettings_keys = [
                    ('org.gnome.desktop.lockdown', 'disable-lock-screen', 'true'),
                    ('org.gnome.desktop.lockdown', 'disable-user-switching', 'true'),
                ]
                for gs_schema, gs_key, gs_value in gsettings_keys:
                    gsettings.set(gs_schema, gs_key, gs_value, self.username)

                accessibility = gsettings.get('org.gnome.desktop.interface',
                    'toolkit-accessibility', self.username)
            # Accessibility infrastructure
            with open('/proc/cmdline', 'r') as fp:
                if (accessibility == True or 'maybe-ubiquity' in fp.readline()):
                    if os.path.exists('/usr/lib/at-spi2-core/at-spi-bus-launcher'):
                        extras.append(subprocess.Popen(
                            ['/usr/lib/at-spi2-core/at-spi-bus-launcher',
                            '--launch-immediately'],
                            stdin=null, stdout=logfile, stderr=logfile,
                            preexec_fn=self.drop_privileges))
                        os.environ['GTK_MODULES'] = 'gail:atk-bridge'

            # Set a desktop wallpaper.
            with open('/proc/cmdline', 'r') as fp:
                for background in ('/usr/share/backgrounds/edubuntu_default.png',
                    '/usr/share/xfce4/backdrops/macinnis_wallpaper.png',
                    '/usr/share/xfce4/backdrops/xubuntu-precise-right.png',
                    '/usr/share/backgrounds/warty-final-ubuntu.png'):
                    exists = os.access(background, os.R_OK)
                    if (exists and not 'access=v1' in fp.readline()):
                        extras.append(subprocess.Popen(['/usr/lib/ubiquity/wallpaper', background],
                                stdin=null, stdout=logfile, stderr=logfile,
                                preexec_fn=self.drop_privileges))
                        break

        if self.frontend == 'gtk_ui':
            if gconfd_running and osextras.find_on_path('metacity'):
                wm_cmd = ['metacity', '--sm-disable']
            elif osextras.find_on_path('xfwm4'):
                wm_cmd = ['xfwm4']
            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']
            else:
                raise MissingProgramError('No window manager found (tried '
                    'metacity, xfwm4, matchbox-window-manager, '
                    'openbox-lubuntu, openbox)')
            wm = subprocess.Popen(wm_cmd,
                stdin=null, stdout=logfile, stderr=logfile,
                preexec_fn=self.drop_privileges)

            if os.path.exists('/usr/lib/gnome-settings-daemon/gnome-settings-daemon'):
                proc = subprocess.Popen(
                    ['/usr/lib/gnome-settings-daemon/gnome-settings-daemon'],
                    stdin=null, stdout=logfile, stderr=logfile,
                    preexec_fn=self.drop_privileges)
                extras.append(proc)

            if os.path.exists('/usr/bin/xfsettingsd'):
                extras.append(subprocess.Popen(
                    ['xsetroot', '-solid', 'black'],
                    stdin=null, stdout=logfile, stderr=logfile,
                    preexec_fn=self.drop_privileges))
                extras.append(subprocess.Popen(
                    ['/usr/bin/xfsettingsd'],
                    stdin=null, stdout=logfile, stderr=logfile,
                    preexec_fn=self.drop_privileges))

            if osextras.find_on_path('lxsession'):
                proc = subprocess.Popen(
                    ['lxsession','-s','Lubuntu', '-e', 'LXDE', '-a'],
                    stdin=null, stdout=logfile, stderr=logfile,
                    preexec_fn=self.drop_privileges)
                extras.append(proc)

            if os.path.exists('/usr/lib/ubiquity/panel') and "xfwm4" not in wm_cmd:
                if "openbox-lubuntu" not in wm_cmd and "openbox" not in wm_cmd:
                    extras.append(subprocess.Popen(
                        ['/usr/lib/ubiquity/panel'],
                        stdin=null, stdout=logfile, stderr=logfile,
                        preexec_fn=self.drop_privileges))

            if osextras.find_on_path('nm-applet'):
                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 == True:
                with open('/proc/cmdline', 'r') as fp:
                    if 'access=m2' in fp.readline():
                        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', '-n'],
                                stdin=null, stdout=logfile, stderr=logfile,
                                preexec_fn=self.drop_privileges))
        elif self.frontend == 'kde_ui':
            # Don't show the background image in v1 accessibility profile.
            os.setegid(self.gid)
            os.seteuid(self.uid)
            path = '/usr/share/wallpapers/kde-default.png'
            with open('/proc/cmdline', 'r') as fp:
                if (os.access(path, os.R_OK)
                    and not 'access=v1' in fp.readline()):
                    # Draw a background image for the install progress window.
                    from PyQt4.QtCore import Qt
                    from PyQt4 import QtGui
                    a = QtGui.QApplication(sys.argv)
                    root_win = a.desktop()
                    wallpaper = QtGui.QPixmap(path)
                    wallpaper = wallpaper.scaled(root_win.width(),
                        root_win.height(), Qt.KeepAspectRatioByExpanding,
                        Qt.SmoothTransformation)
                    palette = QtGui.QPalette()
                    palette.setBrush(root_win.backgroundRole(),
                                     QtGui.QBrush(wallpaper))
                    root_win.setPalette(palette)
                    root_win.setCursor(Qt.ArrowCursor)
                    a.processEvents()
                    # Force garbage collection so we don't end up with stray X
                    # resources when we kill the X server (LP: #556555).
                    del a
            os.seteuid(0)
            os.setegid(0)

            subprocess.call(
                ['kwriteconfig', '--file', 'kwinrc', '--group', 'Compositing',
                                 '--key', 'Enabled', 'false'],
                stdin=null, stdout=logfile, stderr=logfile,
                preexec_fn=self.drop_privileges)
            wm = subprocess.Popen('kwin',
                stdin=null, stdout=logfile, stderr=logfile,
                preexec_fn=self.drop_privileges)

        greeter = subprocess.Popen(program, stdin=null, stdout=logfile, stderr=logfile)
        ret = greeter.wait()

        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 = unicode(db.metaget(question, 'description'),
                                'utf-8', 'replace')
                message = unicode(db.metaget(question, 'extended_description'),
                                  'utf-8', '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

        def kill_if_exists(pid, signum):
            try:
                os.kill(pid, signum)
            except OSError, 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)
        if gconfd_running:
            subprocess.call(['gconftool-2', '--shutdown'],
                            stdin=null, stdout=logfile, stderr=logfile,
                            preexec_fn=self.drop_privileges)
        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, 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:
            vthandle = open('/dev/tty' + self.vt[2:], 'r+')
            subprocess.call(['clear'], stdin=vthandle, stdout=vthandle)
        except IOError:
            pass

        kill_if_exists(server.pid, signal.SIGTERM)
        server.wait()

        if reboot:
            subprocess.Popen(['reboot'])
        if ret is not None and ret >= 0:
            return ret
        else:
            return 1

if len(sys.argv) < 4:
    sys.stderr.write('Usage: %s <vt[1-N]> <:[0-N]> <username> <program> '
                     '[<arguments>]\n' % sys.argv[0])
    sys.exit(1)

vt, display, username = sys.argv[1:4]
try:
    dm = DM(vt, display, username)
except XStartupError:
    sys.exit(1)
ret = dm.run(*sys.argv[4:])
if ret == 0:
    set_locale()
sys.exit(ret)
