# -*- coding: utf-8; Mode: Python; indent-tabs-mode: nil; tab-width: 4 -*-

from collections import namedtuple
import contextlib
import grp
import os
import pwd
import re
import shutil
import subprocess
import syslog
import json

from ubiquity import osextras
from gi.repository import Gio, GLib, GObject


def utf8(s, errors="strict"):
    """Decode a string as UTF-8 if it isn't already Unicode."""
    if isinstance(s, str):
        return s
    else:
        return str(s, "utf-8", errors)


def is_swap(device):
    try:
        with open('/proc/swaps') as fp:
            for line in fp:
                if line.startswith(device + ' '):
                    return True
    except Exception:
        pass
    return False


_dropped_privileges = 0


def set_groups_for_uid(uid):
    if uid == os.geteuid() or uid == os.getuid():
        return
    user = pwd.getpwuid(uid).pw_name
    try:
        os.setgroups([g.gr_gid for g in grp.getgrall() if user in g.gr_mem])
    except OSError:
        import traceback
        for line in traceback.format_exc().split('\n'):
            syslog.syslog(syslog.LOG_ERR, line)


def drop_all_privileges():
    # gconf needs both the UID and effective UID set.
    global _dropped_privileges
    uid = os.environ.get('PKEXEC_UID', os.environ.get('SUDO_UID'))
    gid = None
    if uid is not None:
        uid = int(uid)
        set_groups_for_uid(uid)
        gid = pwd.getpwuid(uid).pw_gid
    if gid is not None:
        gid = int(gid)
        os.setregid(gid, gid)
    if uid is not None:
        uid = int(uid)
        os.setreuid(uid, uid)
        os.environ['HOME'] = pwd.getpwuid(uid).pw_dir
        os.environ['LOGNAME'] = pwd.getpwuid(uid).pw_name
    _dropped_privileges = None


def drop_privileges():
    global _dropped_privileges
    assert _dropped_privileges is not None
    if _dropped_privileges == 0:
        uid = os.environ.get('PKEXEC_UID', os.environ.get('SUDO_UID'))
        gid = None
        if uid is not None:
            uid = int(uid)
            set_groups_for_uid(uid)
            gid = pwd.getpwuid(uid).pw_gid
        if gid is not None:
            gid = int(gid)
            os.setegid(gid)
        if uid is not None:
            os.seteuid(uid)
    _dropped_privileges += 1


def regain_privileges():
    global _dropped_privileges
    assert _dropped_privileges is not None
    _dropped_privileges -= 1
    if _dropped_privileges == 0:
        os.seteuid(0)
        os.setegid(0)
        os.setgroups([])


def drop_privileges_save():
    """Drop the real UID/GID as well, and hide them in saved IDs."""
    # At the moment, we only know how to handle this when effective
    # privileges were already dropped.
    assert _dropped_privileges is not None and _dropped_privileges > 0
    uid = os.environ.get('PKEXEC_UID', os.environ.get('SUDO_UID'))
    gid = None
    if uid is not None:
        uid = int(uid)
        set_groups_for_uid(uid)
        gid = pwd.getpwuid(uid).pw_gid
    if gid is not None:
        gid = int(gid)
        os.setresgid(gid, gid, 0)
    if uid is not None:
        os.setresuid(uid, uid, 0)


def regain_privileges_save():
    """Recover our real UID/GID after calling drop_privileges_save."""
    assert _dropped_privileges is not None and _dropped_privileges > 0
    # We need to call os.setresuid and os.setresgid twice to avoid
    # permission issues when calling os.setgroups (see LP: #646827).
    _, euid, _ = os.getresuid()
    _, egid, _ = os.getresgid()
    os.setresuid(0, 0, 0)
    os.setresgid(0, 0, 0)
    os.setgroups([])
    os.setresgid(-1, egid, -1)
    os.setresuid(-1, euid, -1)


@contextlib.contextmanager
def raised_privileges():
    """As regain_privileges/drop_privileges, but in context manager style."""
    regain_privileges()
    try:
        yield
    finally:
        drop_privileges()


def raise_privileges(func):
    """As raised_privileges, but as a function decorator."""
    from functools import wraps

    @wraps(func)
    def helper(*args, **kwargs):
        with raised_privileges():
            return func(*args, **kwargs)

    return helper


def get_live_user_home():
    """Returns live user home directory, even if executed under SUDO or PKEXEC"""
    uid = os.environ.get('PKEXEC_UID', os.environ.get('SUDO_UID'))
    if uid is not None:
        return pwd.getpwuid(int(uid)).pw_dir
    return os.getenv('HOME', '')


@raise_privileges
def grub_options():
    """ Generates a list of suitable targets for grub-installer
        @return empty list or a list of ['/dev/sda1','Ubuntu Hardy 8.04'] """
    from ubiquity.parted_server import PartedServer

    ret = []
    try:
        oslist = {}
        subp = subprocess.Popen(
            ['os-prober'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            universal_newlines=True)
        result = subp.communicate()[0].splitlines()
        for res in result:
            res = res.split(':')
            res[0] = re.match(r'[/\w\d]+', res[0]).group()
            oslist[res[0]] = res[1]
        p = PartedServer()
        for disk in p.disks():
            p.select_disk(disk)
            with open(p.device_entry('model')) as fp:
                mod = fp.readline()
            with open(p.device_entry('device')) as fp:
                dev = fp.readline()
            with open(p.device_entry('size')) as fp:
                size = fp.readline()
            if dev and mod:
                if size.isdigit():
                    size = format_size(int(size))
                    ret.append([dev, '%s (%s)' % (mod, size)])
                else:
                    ret.append([dev, mod])
            for part in p.partitions():
                ostype = ''
                if part[4] == 'linux-swap':
                    continue
                if part[4] == 'free':
                    continue
                if os.path.exists(p.part_entry(part[1], 'format')):
                    # Don't bother looking for an OS type.
                    pass
                elif part[5] in oslist.keys():
                    ostype = oslist[part[5]]
                ret.append([part[5], ostype])
    except Exception:
        import traceback
        for line in traceback.format_exc().split('\n'):
            syslog.syslog(syslog.LOG_ERR, line)
    return ret


@raise_privileges
def boot_device():
    from ubiquity.parted_server import PartedServer

    boot = None
    root = None
    try:
        p = PartedServer()
        for disk in p.disks():
            p.select_disk(disk)
            for part in p.partitions():
                part = part[1]
                if p.has_part_entry(part, 'mountpoint'):
                    mp = p.readline_part_entry(part, 'mountpoint')
                    if mp == '/boot':
                        boot = disk.replace('=', '/')
                    elif mp == '/':
                        root = disk.replace('=', '/')
    except Exception:
        import traceback
        for line in traceback.format_exc().split('\n'):
            syslog.syslog(syslog.LOG_ERR, line)
    if boot:
        return boot
    return root


def is_removable(device):
    if device is None:
        return None
    device = os.path.realpath(device)
    devpath = None
    is_partition = False
    removable_bus = False
    subp = subprocess.Popen(['udevadm', 'info', '-q', 'property',
                             '-n', device],
                            stdout=subprocess.PIPE, universal_newlines=True)
    for line in subp.communicate()[0].splitlines():
        line = line.strip()
        if line.startswith('DEVPATH='):
            devpath = line[8:]
        elif line == 'DEVTYPE=partition':
            is_partition = True
        elif line == 'ID_BUS=usb' or line == 'ID_BUS=ieee1394':
            removable_bus = True

    if devpath is not None:
        if is_partition:
            devpath = os.path.dirname(devpath)
        is_removable = removable_bus
        try:
            with open('/sys%s/removable' % devpath) as removable:
                if removable.readline().strip() != '0':
                    is_removable = True
        except IOError:
            pass
        if is_removable:
            try:
                subp = subprocess.Popen(['udevadm', 'info', '-q', 'name',
                                         '-p', devpath],
                                        stdout=subprocess.PIPE,
                                        universal_newlines=True)
                return ('/dev/%s' %
                        subp.communicate()[0].splitlines()[0].strip())
            except Exception:
                pass

    return None


def mount_info(path):
    """Return filesystem name, type, and ro/rw for a given mountpoint."""
    fsname = ''
    fstype = ''
    writable = ''
    with open('/proc/mounts') as fp:
        for line in fp:
            line = line.split()
            if line[1] == path:
                fsname = line[0]
                fstype = line[2]
                writable = line[3].split(',')[0]
    return fsname, fstype, writable


def udevadm_info(args):
    fullargs = ['udevadm', 'info', '-q', 'property']
    fullargs.extend(args)
    udevadm = {}
    subp = subprocess.Popen(
        fullargs, stdout=subprocess.PIPE, universal_newlines=True)
    for line in subp.communicate()[0].splitlines():
        line = line.strip()
        if '=' not in line:
            continue
        name, value = line.split('=', 1)
        udevadm[name] = value
    return udevadm


def partition_to_disk(partition):
    """Convert a partition device to its disk device, if any."""
    udevadm_part = udevadm_info(['-n', partition])
    if ('DEVPATH' not in udevadm_part or
            udevadm_part.get('DEVTYPE') != 'partition'):
        return partition

    disk_syspath = '/sys%s' % udevadm_part['DEVPATH'].rsplit('/', 1)[0]
    udevadm_disk = udevadm_info(['-p', disk_syspath])
    return udevadm_disk.get('DEVNAME', partition)


@raise_privileges
def is_bitlocker_partition_encrypted(devpath):
    """Check if partition is BitLocker encryption."""
    subp = subprocess.Popen(
        ['blkid', '--match-tag', 'TYPE', devpath],
        stdout=subprocess.PIPE, stderr=subprocess.PIPE,
        universal_newlines=True)
    result = subp.communicate()[0].split()
    if result:
        for line in result:
            if devpath in line:
                continue
            name, value = line.split('=', 1)
            if 'TYPE' in name and 'BitLocker' in value[1:-1]:
                return True
    return False


def is_boot_device_removable(boot=None):
    if boot:
        return is_removable(boot)
    else:
        return is_removable(boot_device())


def cdrom_mount_info():
    """Return mount information for /cdrom.

    This is the same as mount_info, except that the partition is converted to
    its containing disk, and we don't care whether the mount point is
    writable.
    """
    cdsrc, cdfs, _ = mount_info('/cdrom')
    cdsrc = partition_to_disk(cdsrc)
    return cdsrc, cdfs


@raise_privileges
def grub_device_map():
    """Return the contents of the default GRUB device map."""
    subp = subprocess.Popen(['grub-mkdevicemap', '--no-floppy', '-m', '-'],
                            stdout=subprocess.PIPE, universal_newlines=True)
    return subp.communicate()[0].splitlines()


def grub_default(boot=None):
    """Return the default GRUB installation target."""

    # Much of this is intentionally duplicated from grub-installer, so that
    # we can show the user what device GRUB will be installed to before
    # grub-installer is run.  Pursuant to that, we intentionally run this in
    # the installer root as /target might not yet be available.

    bootremovable = is_boot_device_removable(boot=boot)
    if bootremovable is not None:
        return bootremovable

    devices = grub_device_map()
    target = None

    if boot is not None:
        for device in devices:
            try:
                candidate = os.path.realpath(device.split('\t')[1])
            except (IndexError, OSError):
                pass
            if candidate == boot:
                target = candidate
                break

    if target is None and devices:
        try:
            target = os.path.realpath(devices[0].split('\t')[1])
        except (IndexError, OSError):
            pass

    # last resort
    if target is None:
        target = '(hd0)'

    cdsrc, cdfs = cdrom_mount_info()
    try:
        # The target is usually under /dev/disk/by-id/, so string equality
        # is insufficient.
        same = os.path.samefile(cdsrc, target)
    except OSError:
        same = False
    if ((same or target == '(hd0)') and
            ((cdfs and cdfs != 'iso9660') or is_removable(cdsrc))):
        # Installing from removable media other than a CD.  Make sure that
        # we don't accidentally install GRUB to it.
        boot = boot_device()
        try:
            if boot:
                target = boot
            else:
                # Try the next disk along (which can't also be the CD source).
                target = os.path.realpath(devices[1].split('\t')[1])
            # Match the more specific patterns first, then move on to the more
            # generic /dev/[a-z]+.
            target = re.sub(r'(\
                            /dev/(cciss|ida)/c[0-9]d[0-9]|\
                            /dev/nvme[0-9]+n[0-9]+|\
                            /dev/[a-z]+\
                            ).*', r'\1', target)
        except (IndexError, OSError):
            pass

    return target


_os_prober_oslist = {}
_os_prober_osvers = {}
_os_prober_called = False


def find_in_os_prober(device, with_version=False):
    """Look for the device name in the output of os-prober.

    Return the friendly name of the device, or the empty string on error.
    """
    try:
        oslist, osvers = os_prober()
        if device in oslist:
            ret = oslist[device]
        elif is_swap(device):
            ret = 'swap'
        else:
            syslog.syslog('Device %s not found in os-prober output' % device)
            ret = ''
        ret = utf8(ret, errors='replace')
        ver = utf8(osvers.get(device, ''), errors='replace')
        if with_version:
            return ret, ver
        else:
            return ret
    except (KeyboardInterrupt, SystemExit):
        pass
    except Exception:
        import traceback
        syslog.syslog(syslog.LOG_ERR, "Error in find_in_os_prober:")
        for line in traceback.format_exc().split('\n'):
            syslog.syslog(syslog.LOG_ERR, line)
    return ''


@raise_privileges
def os_prober():
    global _os_prober_oslist
    global _os_prober_osvers
    global _os_prober_called

    if not _os_prober_called:
        _os_prober_called = True
        subp = subprocess.Popen(
            ['os-prober'], stdout=subprocess.PIPE, stderr=subprocess.PIPE,
            universal_newlines=True)
        result = subp.communicate()[0].splitlines()
        for res in result:
            res = res.split(':')
            # launchpad bug #1265192, fix os-prober Windows EFI path
            res[0] = re.match(r'[/\w\d]+', res[0]).group()
            if res[2] == 'Ubuntu':
                version = [v for v in re.findall('[0-9.]*', res[1]) if v][0]
                # Get rid of the superfluous (development version) (11.04)
                text = re.sub(r'\s*\(.*\).*', '', res[1])
                _os_prober_oslist[res[0]] = text
                _os_prober_osvers[res[0]] = version
            else:
                # Get rid of the bootloader indication. It's not relevant here.
                _os_prober_oslist[res[0]] = res[1].replace(' (loader)', '')
    return _os_prober_oslist, _os_prober_osvers


@raise_privileges
def remove_os_prober_cache():
    osextras.unlink_force('/var/lib/ubiquity/os-prober-cache')
    shutil.rmtree('/var/lib/ubiquity/linux-boot-prober-cache',
                  ignore_errors=True)


def windows_startup_folder(mount_path):
    locations = [
        # Windows 8
        'ProgramData/Microsoft/Windows/Start Menu/Programs/StartUp',
        # Windows 7
        'ProgramData/Microsoft/Windows/Start Menu/Programs/Startup',
        # Windows XP
        'Documents and Settings/All Users/Start Menu/Programs/Startup',
        # Windows NT
        'Winnt/Profiles/All Users/Start Menu/Programs/Startup',
    ]
    for location in locations:
        path = os.path.join(mount_path, location)
        if os.path.exists(path):
            return path
    return ''


ReleaseInfo = namedtuple('ReleaseInfo', 'name, version')


def get_release():
    if get_release.release_info is None:
        try:
            with open('/cdrom/.disk/info') as fp:
                line = fp.readline()
                if line:
                    line = line.split()
                    if line[2] == 'LTS':
                        line[1] += ' LTS'
                    line[0] = line[0].replace('-', ' ')
                    get_release.release_info = ReleaseInfo(
                        name=line[0], version=line[1])
        except Exception:
            syslog.syslog(syslog.LOG_ERR, 'Unable to determine the release.')

        if not get_release.release_info:
            get_release.release_info = ReleaseInfo(name='Ubuntu', version='')
    return get_release.release_info


get_release.release_info = None


def get_release_name():
    import warnings
    warnings.warn('get_release_name() is deprecated, '
                  'use get_release().name instead.',
                  category=DeprecationWarning)

    if not get_release_name.release_name:
        try:
            with open('/cdrom/.disk/info') as fp:
                line = fp.readline()
                if line:
                    line = line.split()
                    if line[2] == 'LTS':
                        get_release_name.release_name = ' '.join(line[:3])
                    else:
                        get_release_name.release_name = ' '.join(line[:2])
        except Exception:
            syslog.syslog(
                syslog.LOG_ERR,
                "Unable to determine the distribution name from "
                "/cdrom/.disk/info")
        if not get_release_name.release_name:
            get_release_name.release_name = 'Ubuntu'
    return get_release_name.release_name


get_release_name.release_name = ''


@raise_privileges
def get_install_medium():
    if not get_install_medium.medium:
        try:
            if os.access('/cdrom', os.W_OK):
                get_install_medium.medium = 'USB'
            else:
                get_install_medium.medium = 'CD'
        except Exception:
            syslog.syslog(
                syslog.LOG_ERR, "Unable to determine install medium.")
            get_install_medium.medium = 'CD'
    return get_install_medium.medium


get_install_medium.medium = ''


def execute(*args):
    """runs args* in shell mode. Output status is taken."""

    log_args = ['log-output', '-t', 'ubiquity']
    log_args.extend(args)

    try:
        status = subprocess.call(log_args)
    except IOError as e:
        syslog.syslog(syslog.LOG_ERR, ' '.join(log_args))
        syslog.syslog(syslog.LOG_ERR,
                      "OS error(%s): %s" % (e.errno, e.strerror))
        return False
    else:
        if status != 0:
            syslog.syslog(syslog.LOG_ERR, ' '.join(log_args))
            return False
        syslog.syslog(' '.join(log_args))
        return True


@raise_privileges
def execute_root(*args):
    return execute(*args)


def format_size(size):
    """Format a partition size."""
    if size < 1000:
        unit = 'B'
        factor = 1
    elif size < 1000 * 1000:
        unit = 'kB'
        factor = 1000
    elif size < 1000 * 1000 * 1000:
        unit = 'MB'
        factor = 1000 * 1000
    elif size < 1000 * 1000 * 1000 * 1000:
        unit = 'GB'
        factor = 1000 * 1000 * 1000
    else:
        unit = 'TB'
        factor = 1000 * 1000 * 1000 * 1000
    return '%.1f %s' % (float(size) / factor, unit)


def debconf_escape(text):
    if type(text) is not str:
        return text
    escaped = text.replace('\\', '\\\\').replace('\n', '\\n')
    return re.sub(r'(\s)', r'\\\1', escaped)


def create_bool(text):
    if text == 'true':
        return True
    elif text == 'false':
        return False
    else:
        return text


@raise_privileges
def dmimodel():
    model = ''
    kwargs = {}
    if os.geteuid() != 0:
        # Silence annoying warnings during the test suite.
        kwargs['stderr'] = open('/dev/null', 'w')
    try:
        proc = subprocess.Popen(
            ['dmidecode', '--quiet', '--string', 'system-manufacturer'],
            stdout=subprocess.PIPE, universal_newlines=True, **kwargs)
        manufacturer = proc.communicate()[0]
        if not manufacturer:
            return
        manufacturer = manufacturer.lower()
        if 'to be filled' in manufacturer:
            # Don't bother with products in development.
            return
        if 'bochs' in manufacturer or 'vmware' in manufacturer:
            model = 'virtual machine'
            # VirtualBox sets an appropriate system-product-name.
        else:
            if 'lenovo' in manufacturer or 'ibm' in manufacturer:
                key = 'system-version'
            else:
                key = 'system-product-name'
            proc = subprocess.Popen(
                ['dmidecode', '--quiet', '--string', key],
                stdout=subprocess.PIPE,
                universal_newlines=True)
            model = proc.communicate()[0]
        if 'apple' in manufacturer:
            # MacBook4,1 - strip the 4,1
            model = re.sub(r'[^a-zA-Z\s]', '', model)
        # Replace each gap of non-alphanumeric characters with a dash.
        # Ensure the resulting string does not begin or end with a dash.
        model = re.sub('[^a-zA-Z0-9]+', '-', model).rstrip('-').lstrip('-')
        if model.lower() == 'not-available':
            return
        if model.lower() == "To be filled by O.E.M.".lower():
            return
    except Exception:
        syslog.syslog(syslog.LOG_ERR, 'Unable to determine the model from DMI')
    finally:
        if 'stderr' in kwargs:
            kwargs['stderr'].close()
    return model


def set_indicator_keymaps(lang):
    import xml.etree.cElementTree as ElementTree
    from gi.repository import Xkl, GdkX11
    # GdkX11.x11_get_default_xdisplay() segfaults if Gtk hasn't been
    # imported; possibly finer-grained than this, but anything using this
    # will already have imported Gtk anyway ...
    from gi.repository import Gtk
    from ubiquity import gsettings

    # pacify pyflakes
    Gtk

    gsettings_key = ['org.gnome.libgnomekbd.keyboard', 'layouts']
    gsettings_sources = ('org.gnome.desktop.input-sources', 'sources')
    gsettings_options = ('org.gnome.desktop.input-sources', 'xkb-options')
    lang = lang.split('_')[0]
    variants = []

    # Map inspired from that of gfxboot-theme-ubuntu that's itself
    # based on console-setup's. This one has been restricted to
    # language => keyboard layout not locale => keyboard layout as
    # we don't actually know the exact locale
    default_keymap = {
        'ar': 'ara',
        'bs': 'ba',
        'de': 'de',
        'el': 'gr',
        'en': 'us',
        'eo': 'epo',
        'fr': 'fr_oss',
        'gu': 'in_guj',
        'hi': 'in',
        'hr': 'hr',
        'hy': 'am',
        'ka': 'ge',
        'kn': 'in_kan',
        'lo': 'la',
        'ml': 'in_mal',
        'pa': 'in_guru',
        'sr': 'rs',
        'sv': 'se',
        'ta': 'in_tam',
        'te': 'in_tel',
        'zh': 'cn',
    }

    def item_str(s):
        '''Convert a zero-terminated byte array to a proper str'''
        import array
        s = array.array('B', s).tobytes()
        i = s.find(b'\x00')
        return s[:i].decode()

    def process_variant(*args):
        if hasattr(args[2], 'name'):
            variants.append(
                '%s\t%s' % (item_str(args[1].name), item_str(args[2].name)))
        else:
            variants.append(item_str(args[1].name))

    def restrict_list(variants):
        new_variants = []

        # Start by looking by an explicit default layout in the keymap
        if lang in default_keymap:
            if default_keymap[lang] in variants:
                variants.remove(default_keymap[lang])
                new_variants.append(default_keymap[lang])
            else:
                tab_keymap = default_keymap[lang].replace('_', '\t')
                if tab_keymap in variants:
                    variants.remove(tab_keymap)
                    new_variants.append(tab_keymap)

        # Prioritize the layout matching the language (if any)
        if lang in variants:
            variants.remove(lang)
            new_variants.append(lang)

        # Uniquify our list (just in case)
        variants = list(set(variants))

        if len(variants) > 4:
            # We have a problem, X only supports 4

            # Add as many entry as we can that are layouts without variant
            country_variants = sorted(
                entry for entry in variants if '\t' not in entry)
            for entry in country_variants[:4 - len(new_variants)]:
                new_variants.append(entry)
                variants.remove(entry)

            if len(new_variants) < 4:
                # We can add some more
                simple_variants = sorted(
                    entry for entry in variants if '_' not in entry)
                for entry in simple_variants[:4 - len(new_variants)]:
                    new_variants.append(entry)
                    variants.remove(entry)

            if len(new_variants) < 4:
                # Now just add anything left
                for entry in variants[:4 - len(new_variants)]:
                    new_variants.append(entry)
                    variants.remove(entry)
        else:
            new_variants += list(variants)

        # gsettings doesn't understand utf8
        new_variants = [str(variant) for variant in new_variants]

        return new_variants

    def call_setxkbmap(variants):
        kb_layouts = []
        kb_variants = []

        for entry in variants:
            fields = entry.split('\t')
            if len(fields) > 1:
                kb_layouts.append(fields[0])
                kb_variants.append(fields[1])
            else:
                kb_layouts.append(fields[0])
                kb_variants.append("")

        execute(
            "setxkbmap", "-layout", ",".join(kb_layouts),
            "-variant", ",".join(kb_variants))

    iso_639 = ElementTree.parse('/usr/share/xml/iso-codes/iso_639.xml')
    nodes = [element for element in iso_639.findall('iso_639_entry')
             if element.get('iso_639_1_code') == lang]
    display = GdkX11.x11_get_default_xdisplay()
    engine = Xkl.Engine.get_instance(display)
    if nodes:
        configreg = Xkl.ConfigRegistry.get_instance(engine)
        configreg.load(False)

        # Apparently iso_639_2B_code doesn't always work (fails with French)
        for prop in ('iso_639_2B_code', 'iso_639_2T_code', 'iso_639_1_code'):
            code = nodes[0].get(prop)
            if code is not None:
                configreg.foreach_language_variant(code, process_variant, None)
                if variants:
                    restricted_variants = restrict_list(variants)
                    call_setxkbmap(restricted_variants)
                    gsettings.set_list(
                        gsettings_key[0], gsettings_key[1],
                        restricted_variants)
                    break
        else:
            # Use the system default if no other keymaps can be determined.
            gsettings.set_list(gsettings_key[0], gsettings_key[1], [])

        # Gnome Shell only does keyboard layout conversion from old
        # gsettings_key once. Recently we started to launch keyboard plugin
        # during ubiquity-dm, hence if we change that key, we should purge the
        # state that gsd uses, to determine if it should run the
        # conversion. Which are a stamp file, and having the new key set.
        # Ideally, I think ubiquity should be more universal and set the new key
        # directly, instead of relying on gsd keeping the conversion code
        # around. But it's too late for 20.10 release.
        gsettings_stamp = os.path.join(
            '/home',
            os.getenv("SUDO_USER", os.getenv("USER", "root")),
            '.local/share/gnome-settings-daemon/input-sources-converted')
        if os.path.exists(gsettings_stamp):
            os.remove(gsettings_stamp)
        gsettings.unset(*gsettings_sources)
        gsettings.unset(*gsettings_options)

    engine.lock_group(0)


NM = 'org.freedesktop.NetworkManager'
NM_STATE_CONNECTED_SITE = 60
NM_STATE_CONNECTED_GLOBAL = 70


def get_prop(obj, iface, prop):
    import dbus
    try:
        return obj.Get(iface, prop, dbus_interface=dbus.PROPERTIES_IFACE)
    except dbus.DBusException as e:
        if e.get_dbus_name() == 'org.freedesktop.DBus.Error.UnknownMethod':
            return None
        else:
            raise


def has_connection():
    return connection_state() == NM_STATE_CONNECTED_GLOBAL


def connection_state():
    import dbus
    bus = dbus.SystemBus()
    manager = bus.get_object(NM, '/org/freedesktop/NetworkManager')
    return get_prop(manager, NM, 'State')


def add_connection_watch(func, global_only=True):
    import dbus

    def connection_cb(state):
        is_connected = False
        if global_only:
            if state == NM_STATE_CONNECTED_GLOBAL:
                is_connected = True
        else:
            if state == NM_STATE_CONNECTED_GLOBAL or state == NM_STATE_CONNECTED_SITE:
                is_connected = True
        func(is_connected)

    bus = dbus.SystemBus()
    bus.add_signal_receiver(connection_cb, 'StateChanged', NM, NM)
    try:
        connection_cb(connection_state())
    except dbus.DBusException:
        # We can't talk to NM, so no idea.  Wild guess: we're connected
        # using ssh with X forwarding, and are therefore connected.  This
        # allows us to proceed with a minimum of complaint.
        func(True)


def install_size():
    if min_install_size:
        return min_install_size

    # Fallback size to 5 GB
    size = 5 * 1024 * 1024 * 1024

    # Maximal size to 8 GB
    max_size = 8 * 1024 * 1024 * 1024

    try:
        with open('/cdrom/casper/filesystem.size') as fp:
            size = int(fp.readline())
    except IOError:
        pass

    # TODO substitute into the template for the state box.
    min_disk_size = size * 2  # fudge factor

    # Set minimum size to 8GB if current minimum size is larger
    # than 8GB and we still have an extra 20% of free space
    if min_disk_size > max_size and size * 1.2 < max_size:
        min_disk_size = max_size

    return min_disk_size


min_install_size = None


def launch_uri(uri):
    subprocess.Popen(['xdg-open', uri], close_fds=True,
                     preexec_fn=drop_all_privileges)


def is_removable_device(path):
    """Returns True if path is on a removable device"""
    lsblk_output = ""
    cmd = "lsblk -J -o MOUNTPOINT,PATH,RM"

    # Find mount point of path
    mp = path
    while not os.path.ismount(mp):
        mp = os.path.dirname(mp)

    # In ubiquity environment / is the installation media
    if mp == "/":
        return True

    # Then search if the corresponding device is removable
    try:
        lsblk_output = subprocess.check_output(cmd.split())
    except subprocess.CalledProcessError:
        syslog.syslog(syslog.LOG_ERR, "Unable to determine if %s is on a removable device" % path)
        return False

    devices = json.loads(lsblk_output)
    for entry in devices["blockdevices"]:
        if entry["mountpoint"] == mp and entry["rm"]:
            return True
    return False


class SystemdUnitWatcher:
    def __init__(self, unit, cb):
        # try connecting to the bus
        try:
            self.system_bus = Gio.bus_get_sync(Gio.BusType.SYSTEM)
            self.system_bus.call_sync(
                "org.freedesktop.systemd1",
                "/org/freedesktop/systemd1",
                "org.freedesktop.systemd1.Manager",
                "Subscribe",
                None,  # parameters
                None,  # reply type
                Gio.DBusCallFlags.NONE,
                -1,  # timeout
                None,  # cancellable
            )
            self.system_bus.call(
                "org.freedesktop.systemd1",
                "/org/freedesktop/systemd1",
                "org.freedesktop.systemd1.Manager",
                "LoadUnit",
                GLib.Variant("(s)", (unit,)),  # parameters
                GLib.VariantType("(o)"),  # reply type
                Gio.DBusCallFlags.NONE,
                -1,  # timeout
                None,  # cancellable
                self._on_get_unit,
                cb,  # user_data
            )
            self.proxy = None
            self.properties_changed_signal = None
        except GLib.Error:
            pass
        except IOError:
            pass

    def stop(self):
        if self.properties_changed_signal:
            GObject.signal_handler_disconnect(
                self.proxy, self.properties_changed_signal
            )
            self.properties_changed_signal = None
        self.proxy = None

    def _on_properties_changed(
        self, proxy, changed_properties, invalidated_properties, cb
    ):
        try:
            if changed_properties["ActiveState"] == "active":
                self.stop()
                cb()
        except KeyError:  # this property didn't change
            pass

    def _on_got_unit_proxy(self, conn, res, cb):
        self.proxy = conn.new_finish(res)
        self.properties_changed_signal = self.proxy.connect(
            "g-properties-changed", self._on_properties_changed, cb
        )
        active_state = self.proxy.get_cached_property(
            "ActiveState"
        ).get_string()
        if active_state == "active":
            self.stop()
            cb()

    def _on_get_unit(self, conn, res, cb):
        try:
            object_path_v = conn.call_finish(res)
            object_path = object_path_v.get_child_value(0).get_string()
            Gio.DBusProxy.new(
                conn,
                Gio.DBusProxyFlags.GET_INVALIDATED_PROPERTIES,
                None,  # GDBusInterfaceInfo
                "org.freedesktop.systemd1",
                object_path,
                "org.freedesktop.systemd1.Unit",
                None,  # cancellable
                self._on_got_unit_proxy,
                cb,  # user_data
            )
        except GLib.Error as e:
            if (
                e.matches(Gio.io_error_quark(), Gio.IOErrorEnum.DBUS_ERROR) and
                Gio.DBusError.get_remote_error(e) ==
                "org.freedesktop.systemd1.NoSuchUnit"
            ):
                pass  # the unit doesn't exist, tweak this to error if desired
            else:
                raise


def sudo_wrapper(user):
    """Return a list of args suitable for use with the subprocess module,
    to invoke a command as a given user, while preserving a set of useful
    environment variables. Append the command and list of args to be invoked
    to the return value of this helper.
    """
    preserve_env = [
        'DBUS_SESSION_BUS_ADDRESS',
        'XDG_DATA_DIRS',
        'XDG_RUNTIME_DIR',
    ]
    return [
        'sudo',
        '--preserve-env={}'.format(','.join(preserve_env)),
        '-H',
        '-u', user
    ]


# vim:ai:et:sts=4:tw=80:sw=4:
