# -*- coding: utf-8 -*-
#
# Copyright © 2008-2013  Red Hat, Inc. All rights reserved.
# Copyright © 2008-2013  Luke Macken <lmacken@redhat.com>
#
# This copyrighted material is made available to anyone wishing to use, modify,
# copy, or redistribute it subject to the terms and conditions of the GNU
# General Public License v.2.  This program is distributed in the hope that it
# will be useful, but WITHOUT ANY WARRANTY expressed or implied, including the
# implied warranties of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
# See the GNU General Public License for more details.  You should have
# received a copy of the GNU General Public License along with this program; if
# not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth
# Floor, Boston, MA 02110-1301, USA. Any Red Hat trademarks that are
# incorporated in the source code or documentation are not subject to the GNU
# General Public License and may only be used or replicated with the express
# permission of Red Hat, Inc.

"""
Our main Tails Installer module.

This contains the TailsInstallerCreator parent class, which is an abstract interface
that provides platform-independent methods. Platform specific implementations
include the LinuxTailsInstallerCreator and the WindowsTailsInstallerCreator.
"""

import subprocess
import tempfile
import logging
import hashlib
import shutil
import signal
import time
import os
import platform
import re
import stat
import sys

from StringIO import StringIO
from datetime import datetime
from pprint import pformat

if 'linux' in sys.platform:
    import gi
    gi.require_version('UDisks', '2.0')
    from gi.repository import UDisks, GLib

from tails_installer.utils import (_move_if_exists, _unlink_if_exists, unicode_to_utf8,
                           unicode_to_filesystemencoding, is_running_from_tails,
                           _set_liberal_perms_recursive, underlying_physical_device,
                           get_open_write_fd, write_to_block_device,
                           MiB_to_bytes)
from tails_installer import _
from tails_installer.config import config
from tails_installer.source import SourceError

#XXX: size should be configurable
SYSTEM_PARTITION_FLAGS = [0,    # system partition
                          2,    # legacy BIOS bootable
                          60,   # read-only
                          62,   # hidden
                          63    # do not automount
                          ]
# EFI System Partition
ESP_GUID = 'C12A7328-F81F-11D2-BA4B-00A0C93EC93B'

class TailsInstallerError(Exception):
    """ A generic error message that is thrown by the Tails Installer"""


class TailsInstallerCreator(object):
    """ An OS-independent parent class for Tails Installer Creators """

    min_installation_device_size = config['min_installation_device_size'] # MiB
    source = None       # the object representing our live source image
    label = config['branding']['partition_label'] # if one doesn't already exist
    fstype = None       # the format of our usb stick
    drives = {}         # {device: {'label': label, 'mount': mountpoint}}
    overlay = 0         # size in mb of our persisten overlay
    dest = None         # the mount point of of our selected drive
    uuid = None         # the uuid of our selected drive
    pids = []           # a list of pids of all of our subprocesses
    output = StringIO() # log subprocess output in case of errors
    totalsize = 0       # the total size of our overlay + iso
    _drive = None       # mountpoint of the currently selected drive
    mb_per_sec = 0      # how many megabytes per second we can write
    log = None
    ext_fstypes = set(['ext2', 'ext3', 'ext4'])
    valid_fstypes = set(['vfat', 'msdos']) | ext_fstypes
    force_reinstall = False

    drive = property(fget=lambda self: self.drives[self._drive],
                     fset=lambda self, d: self._set_drive(d))

    def __init__(self, opts):
        self.opts = opts
        import sys
        if sys.platform != "win32" and not opts.unprivileged:
            if os.getuid() != 0:
                print >> sys.stderr, _("You must run this application as root")
                sys.exit(1)
        self._error_log_filename = self._setup_error_log_file()
        self._setup_logger()

    def _setup_error_log_file(self):
        temp = tempfile.NamedTemporaryFile(mode='a', delete=False,
                                           prefix='tails-installer-')
        temp.close()
        return temp.name

    def _setup_logger(self):
        self.log = logging.getLogger()
        self.log.setLevel(logging.DEBUG if self.opts.verbose else logging.INFO)

        formatter = logging.Formatter("%(asctime)s [%(filename)s:%(lineno)s (%(funcName)s)] %(levelname)s: %(message)s")

        self.stream_handler = logging.StreamHandler()
        self.stream_handler.setFormatter(formatter)
        self.log.addHandler(self.stream_handler)

        self.file_handler = logging.FileHandler(self._error_log_filename)
        self.file_handler.setFormatter(formatter)
        self.log.addHandler(self.file_handler)

    def detect_supported_drives(self, callback=None):
        """ This method should populate self.drives with supported devices """
        raise NotImplementedError

    def verify_filesystem(self):
        """
        Verify the filesystem of our device, setting the volume label
        if necessary.  If something is not right, this method throws a
        TailsInstallerError.
        """
        raise NotImplementedError

    def get_free_bytes(self, drive=None):
        """ Return the number of free bytes on a given drive.

        If drive is None, then use the currently selected device.
        """
        raise NotImplementedError

    def extract_iso(self):
        """ Extract our ISO with 7-zip directly to the USB key """
        self.log.info(_("Extracting live image to the target device..."))
        start = datetime.now()
        self.source.clone(self.dest)
        delta = datetime.now() - start
        if delta.seconds:
            self.mb_per_sec = (self.source.size / delta.seconds) / 1024**2
            if self.mb_per_sec:
                self.log.info(_("Wrote to device at %(speed)d MB/sec") % {
                    'speed': self.mb_per_sec})

    def syslinux_options(self):
        opts = []
        if self.opts.force:
            opts.append('-f')
        if self.opts.safe:
            opts.append('-s')
        return opts

    def install_bootloader(self):
        """ Install the bootloader to our device.

        Platform-specific classes inheriting from the TailsInstallerCreator are
        expected to implement this method to install the bootloader to the
        specified device using syslinux.
        """
        return

    def get_kernel_args(self):
        """ Grab the kernel arguments from our syslinux configuration """
        args = []
        cfg = file(self.get_liveos_file_path('isolinux', 'syslinux.cfg'))
        for line in cfg.readlines():
            if 'append' in line:
                args.extend([arg for arg in line.split()[1:]
                             if not arg.startswith('initrd')])
                break
        cfg.close()
        return args

    def terminate(self):
        """ Terminate any subprocesses that we have spawned """
        raise NotImplementedError

    def mount_device(self):
        """ Mount self.drive, setting the mount point to self.mount """
        raise NotImplementedError

    def unmount_device(self):
        """ Unmount the device mounted at self.mount """
        raise NotImplementedError

    def _set_partition_flags(self, partition, flags):
        flags_total = 0
        for flag in flags:
            flags_total |= (1<<flag)
        partition.call_set_flags_sync(flags_total,
                                      GLib.Variant('a{sv}', None),
                                      None)

    def partition_device(self):
        """ Partition device listed at self.drive """
        raise NotImplementedError

    def system_partition_size(self, device_size_in_bytes):
        """ Return the optimal system partition size (in bytes) for
        a device_size_in_bytes bytes large destination device: 4 GiB on devices
        smaller than 16000 MiB, 8 GiB otherwise.
        """
        # 1. Get unsupported cases out of the way
        if device_size_in_bytes \
           < MiB_to_bytes(self.min_installation_device_size):
            raise NotImplementedError
        # 2. Handle supported cases (note: you might be surprised if
        # you looked at the actual size of USB sticks labeled "16 GB"
        # in the real world, hence the weird definition of "16 GB"
        # used below)
        elif device_size_in_bytes >= MiB_to_bytes(14500):
            return MiB_to_bytes(8 * 1024)
        else:
            return MiB_to_bytes(4 * 1024)

    def is_device_big_enough_for_installation(self, device_size_in_bytes):
        return (
            device_size_in_bytes
            >= MiB_to_bytes(self.min_installation_device_size)
        )

    def can_read_partition_table(self, device=None):
        if not device:
            device = self.drive['device']

        proc = self.popen(['/sbin/sgdisk', '--print', device],
                          shell=False, passive=True)
        if proc.returncode:
            return False
        return True

    def clear_all_partition_tables(self, device=None):
        if not device:
            device = self.drive['device']

        # We need to ignore errors because sgdisk returns error code
        # 2 when it successfully zaps partition tables it cannot
        # understand... while we want to make it do this reset
        # precisely to fix that unreadable partition table issue.
        # Chicken'n'egg, right.
        self.popen(['/sbin/sgdisk', '--zap-all', device],
                   shell=False, passive=True)

    def switch_drive_to_system_partition(self):
        pass

    def switch_back_to_full_drive(self):
        pass

    def popen(self, cmd, passive=False, ret='proc', **user_kwargs):
        """ A wrapper method for running subprocesses.

        This method handles logging of the command and it's output, and keeps
        track of the pids in case we need to kill them.  If something goes
        wrong, an error log is written out and a TailsInstallerError is thrown.

        @param cmd: The commandline to execute.  Either a string or a list.
        @param passive: Enable passive process failure.
        @param kwargs: Extra arguments to pass to subprocess.Popen
        """
        if isinstance(cmd, list):
            cmd_decoded = u' '.join(cmd)
            cmd_bytes = [unicode_to_filesystemencoding(el) for el in cmd]
        else:
            cmd_decoded = cmd
            cmd_bytes = unicode_to_filesystemencoding(cmd)
        self.log.debug(cmd_decoded)
        self.output.write(cmd_bytes)
        kwargs = {'shell': True, 'stdin': subprocess.PIPE}
        kwargs.update(user_kwargs)
        proc = subprocess.Popen(cmd_bytes, stdout=subprocess.PIPE,
                                stderr=subprocess.PIPE,
                                **kwargs)
        self.pids.append(proc.pid)
        out, err = proc.communicate()
        out = unicode_to_utf8(out)
        err = unicode_to_utf8(err)
        self.output.write(out + '\n' + err + '\n')
        if proc.returncode:
            if passive:
                self.log.debug(self.output.getvalue())
            else:
                self.log.info(self.output.getvalue())
                raise TailsInstallerError(_(
                        "There was a problem executing the following command: `%(command)s`.\nA more detailed error log has been written to '%(filename)s'.")
                        % {'command': cmd, 'filename': self._error_log_filename})
        if ret == 'stdout':
            return out
        return proc

    def verify_iso_sha1(self, progress=None):
        """ Verify the SHA1 checksum of our ISO if it is in our release list """
        if not self.source.supports_verify_sha1:
            return True
        if not progress:
            class DummyProgress:
                def set_max_progress(self, value): pass
                def update_progress(self, value): pass
            progress = DummyProgress()
        release = self.source.get_release()
        if release:
            progress.set_max_progress(self.source.size / 1024)
            if 'sha1' in release:
                self.log.info(_("Verifying SHA1 checksum of LiveCD image..."))
                hash = 'sha1'
                checksum = hashlib.sha1()
            elif 'sha256' in release:
                self.log.info(_("Verifying SHA256 checksum of LiveCD image..."))
                hash = 'sha256'
                checksum = hashlib.sha256()
            isofile = file(self.source.path, 'rb')
            bytes = 1024**2
            total = 0
            while bytes:
                data = isofile.read(bytes)
                checksum.update(data)
                bytes = len(data)
                total += bytes
                progress.update_progress(total / 1024)
            isofile.close()
            if checksum.hexdigest() == release[hash]:
                return True
            else:
                self.log.info(_("Error: The SHA1 of your Live CD is "
                                "invalid.  You can run this program with "
                                "the --noverify argument to bypass this "
                                "verification check."))
                return False
        else:
            self.log.debug(_('Unknown ISO, skipping checksum verification'))

    def check_free_space(self):
        """ Make sure there is enough space for the LiveOS and overlay """
        freebytes = self.get_free_bytes()
        self.log.debug('freebytes = %d' % freebytes)
        self.log.debug('source size = %d' % self.source.size)
        overlaysize = self.overlay * 1024**2
        self.log.debug('overlaysize = %d' % overlaysize)
        self.totalsize = overlaysize + self.source.size
        if self.totalsize > freebytes:
            raise TailsInstallerError(_(
                "Not enough free space on device." +
                "\n%dMB ISO + %dMB overlay > %dMB free space") %
                (self.source.size/1024**2, self.overlay,
                 freebytes/1024**2))

    def create_persistent_overlay(self):
        if self.overlay:
            self.log.info(_("Creating %sMB persistent overlay") % self.overlay)
            if self.fstype == 'vfat':
                # vfat apparently can't handle sparse files
                self.popen('dd if=/dev/zero of="%s" count=%d bs=1M'
                           % (self.get_overlay(), self.overlay))
            else:
                self.popen('dd if=/dev/zero of="%s" count=1 bs=1M seek=%d'
                           % (self.get_overlay(), self.overlay))

    def _update_configs(self, infile, outfile):
        outfile_new = "%s.new" % outfile
        shutil.copy(infile, outfile_new)
        infile = file(infile, 'r')
        outfile_new = file(outfile_new, 'w')
        usblabel = self.uuid and 'UUID=' + self.uuid or 'LABEL=' + self.label
        for line in infile.readlines():
            line = re.sub('/isolinux/', '/syslinux/', line)
            if "CDLABEL" in line:
                line = re.sub("CDLABEL=[^ ]*", usblabel, line)
                line = re.sub("rootfstype=[^ ]*",
                              "rootfstype=%s" % self.fstype,
                              line)
            if self.overlay and "liveimg" in line:
                line = line.replace("liveimg", "liveimg overlay=" + usblabel)
                line = line.replace(" ro ", " rw ")
            if self.opts.kernel_args:
                line = line.replace("liveimg", "liveimg %s" %
                                    ' '.join(self.opts.kernel_args.split(',')))
            outfile_new.write(line)
        infile.close()
        outfile_new.close()
        shutil.move(outfile_new.name, outfile)

    def update_configs(self):
        """ Generate our syslinux.cfg and grub.conf files """
        grubconf     = self.get_liveos_file_path("EFI", "BOOT", "grub.conf")
        bootconf     = self.get_liveos_file_path("EFI", "BOOT", "boot.conf")
        bootx64conf  = self.get_liveos_file_path("EFI", "BOOT", "bootx64.conf")
        bootia32conf = self.get_liveos_file_path("EFI", "BOOT", "bootia32.conf")
        updates = [(self.get_liveos_file_path("isolinux", "isolinux.cfg"),
                    self.get_liveos_file_path("isolinux", "syslinux.cfg")),
                   (self.get_liveos_file_path("isolinux", "stdmenu.cfg"),
                    self.get_liveos_file_path("isolinux", "stdmenu.cfg")),
                   (self.get_liveos_file_path("isolinux", "exithelp.cfg"),
                    self.get_liveos_file_path("isolinux", "exithelp.cfg")),
                   (self.get_liveos_file_path("EFI", "BOOT", "isolinux.cfg"),
                    self.get_liveos_file_path("EFI", "BOOT", "syslinux.cfg")),
                   (grubconf, bootconf)]
        copies = [(bootconf, grubconf),
                  (bootconf, bootx64conf),
                  (bootconf, bootia32conf)]

        for (infile, outfile) in updates:
            if os.path.exists(infile):
                self._update_configs(infile, outfile)
        # only copy/overwrite files we had originally started with
        for (infile, outfile) in copies:
            if os.path.exists(outfile):
                try:
                    shutil.copyfile(infile, outfile)
                except Exception, e:
                    self.log.warning(_("Unable to copy %(infile)s to %(outfile)s: %(message)s")
                                     % {'infile': infile,
                                        'outfile': outfile,
                                        'message': str(e)})

        syslinux_path = self.get_liveos_file_path("syslinux")
        _move_if_exists(self.get_liveos_file_path("isolinux"), syslinux_path)
        _unlink_if_exists(os.path.join(syslinux_path, "isolinux.cfg"))

    def delete_liveos(self):
        """ Delete the files installed by the existing Live OS, after
        chmod'ing them since Python for Windows is unable to delete
        read-only files.
        """
        self.log.info(_('Removing existing Live OS'))
        for path in self.get_liveos_toplevel_files(absolute=True):
            if not os.path.exists(path):
                continue
            self.log.debug("Considering " + path)
            if os.path.isfile(path):
                try:
                    os.chmod(path, 0644)
                except OSError, e:
                    self.log.debug(_("Unable to chmod %(file)s: %(message)s") % {'file': path, 'message': str(e)})
                try:
                    os.unlink(path)
                except:
                    raise TailsInstallerError(_(
                        "Unable to remove file from"
                        " previous LiveOS: %(message)s")
                        % {'message': str(e)})
            elif os.path.isdir(path):
                try:
                    _set_liberal_perms_recursive(path)
                except OSError, e:
                    self.log.debug(_("Unable to chmod %(file)s: %(message)s")
                                   % {'file': path, 'message': str(e)})
                try:
                    shutil.rmtree(path)
                except OSError, e:
                    raise TailsInstallerError(_(
                        "Unable to remove directory from"
                        " previous LiveOS: %(message)s")
                        % {'message': str(e)})

    def get_liveos(self):
        return self.get_liveos_file_path(config['main_liveos_dir'])

    def running_liveos_mountpoint(self):
        return config['running_liveos_mountpoint']

    def get_liveos_file_path(self, *args):
        """ Given a path relative to the root of the Live OS filesystem,
        returns the absolute path to it from the perspective of the system
        tails-installer is running on.
        """
        return os.path.join(self.dest + os.path.sep, *args)

    def get_liveos_toplevel_files(self, absolute=False):
        """ Returns the list of files install at top level in the Live
        OS filesystem.
        If absolute=True, return absolute paths from the perspective
        of the system tails-installer is running on; else, return paths
        relative to the root of the Live OS filesystem.
        """
        toplevels = config['liveos_toplevel_files']
        if absolute:
            return [self.get_liveos_file_path(f) for f in toplevels]
        return toplevels

    def existing_overlay(self):
        return os.path.exists(self.get_overlay())

    def get_overlay(self):
        return os.path.join(self.get_liveos(),
                            'overlay-%s-%s' % (self.label, self.uuid or ''))

    def _set_drive(self, drive):
        # XXX: sometimes fails with:
        # Traceback (most recent call last):
        #  File "tails-installer/git/tails_installer.gui.py", line 200, in run
        #    self.live.switch_drive_to_system_partition()
        #  File "tails-installer/git/tails_installer.creator.py", line 967, in switch_drive_to_system_partition
        #    self.drive = '%s%s' % (full_drive_name, append)
        #  File "tails-installer/git/tails_installer.creator.py", line 88, in <lambda>
        #    fset=lambda self, d: self._set_drive(d))
        #  File "tails-installer/git/tails_installer.creator.py", line 553, in _set_drive
        #    raise TailsInstallerError(_("Cannot find device %s") % drive)
        if not self.drives.has_key(drive):
            raise TailsInstallerError(_("Cannot find device %s") % drive)
        self.log.debug("%s selected: %s" % (drive, self.drives[drive]))
        self._drive = drive
        self.uuid = self.drives[drive]['uuid']
        self.fstype = self.drives[drive]['fstype']

    def get_proxies(self):
        """ Return a dictionary of proxy settings """
        return None

    def bootable_partition(self):
        """ Ensure that the selected partition is flagged as bootable """
        # Done on Windows by syslinux.exe -a option
        pass

    def get_mbr(self):
        pass

    def blank_mbr(self):
        pass

    def mbr_matches_syslinux_bin(self):
        """
        Return whether or not the MBR on the drive matches the system's
        syslinux gptmbr.bin
        """
        return True

    def reset_mbr(self):
        # Done on Windows by syslinux.exe -m option
        pass

    def flush_buffers(self):
        """ Flush filesystem buffers """
        pass

    def is_admin(self):
        raise NotImplementedError

    def running_device(self):
        """Returns the physical block device UDI (e.g.
        /org/freedesktop/UDisks2/devices/sdb) from which the system
        is running."""
        liveos_mountpoint = self.running_liveos_mountpoint()
        if is_running_from_tails() and os.path.exists(liveos_mountpoint):
            return underlying_physical_device(liveos_mountpoint)
        else:
            return False

    def connect_drive_monitor(self, callback):
        """Connects a callback to be called (at least) when the drive list
        changes."""
        raise NotImplementedError

class LinuxTailsInstallerCreator(TailsInstallerCreator):

    def __init__(self, *args, **kw):
        super(LinuxTailsInstallerCreator, self).__init__(*args, **kw)

        self.valid_fstypes -= self.ext_fstypes
        self.drives = {}
        self._udisksclient = UDisks.Client.new_sync()

    def detect_supported_drives(self, callback=None, force_partitions=False):
        """ Detect all supported (USB and SDIO) storage devices using UDisks.
        """
        mounted_parts = {}
        self.drives = {}
        for obj in self._udisksclient.get_object_manager().get_objects():
            block = obj.props.block
            self.log.debug("looking at %s" % obj.get_object_path())
            if not block:
                self.log.debug("skip %s which is not a block device"
                               % obj.get_object_path())
                continue
            partition = obj.props.partition
            filesystem = obj.props.filesystem
            drive = self._udisksclient.get_drive_for_block(block)
            if not drive:
                self.log.debug("skip %s which has no associated drive"
                               % obj.get_object_path())
                continue
            data = {
                'udi': obj.get_object_path(),
                'is_optical': drive.props.optical,
                'label': drive.props.id.replace(' ', '_'),
                'vendor': drive.props.vendor,
                'model': drive.props.model,
                'fstype': block.props.id_type,
                'fsversion': block.props.id_version,
                'uuid': block.props.id_uuid,
                'device': block.props.device,
                'mount': filesystem.props.mount_points if filesystem else None,
                'bootable': None, #'bootable': 'boot' in map(str, list(dev.Get(device, 'PartitionFlags'))),
                'parent': None,
                'parent_udi': None,
                'parent_size': None,
                'size': block.props.size,
                'mounted_partitions': set(),
                'is_device_big_enough_for_installation': True,
                'is_device_big_enough_for_upgrade': True,
                'removable': drive.props.removable,
            }

            # Check non-removable drives
            if not data['removable']:
                self.log.debug('Skipping non-removable device: %s'
                               % data['device'])

            # Only pay attention to USB and SDIO devices, unless --force'd
            iface = drive.props.connection_bus
            if iface != 'usb' and iface != 'sdio' \
               and self.opts.force != data['device']:
                self.log.warning(
                    "Skipping device '%(device)s' connected to '%(interface)s' interface"
                    % {'device': data['udi'], 'interface': iface}
                )
                continue

            # Skip optical drives
            if data['is_optical'] and self.opts.force != data['device']:
                self.log.debug('Skipping optical device: %s' % data['device'])
                continue

            # Skip things without a size
            if not data['size'] and not self.opts.force:
                self.log.debug('Skipping device without size: %s'
                               % data['device'])
                continue

            if partition:
                partition_table = self._udisksclient.get_partition_table(
                    partition)
                parent_block = partition_table.get_object().props.block
                data['label'] = partition.props.name
                data['parent'] = parent_block.props.device
                data['parent_size'] = parent_block.props.size
                data['parent_udi'] = parent_block.get_object_path()
            else:
                parent_block = None

            # Check for devices that are too small. Note that we still
            # allow devices that can be upgraded for supporting legacy
            # installations.
            if not self.is_device_big_enough_for_installation(
                    data['parent_size']
                    if data['parent_size']
                    else data['size']) \
               and not self.device_can_be_upgraded(data):
                self.log.warning(
                    'Device is too small for installation: %s'
                    % data['device'])
                data['is_device_big_enough_for_installation'] = False

            # To be more accurate we would need to either mount the candidate
            # device (which causes UX problems down the road) or to recursively
            # parse the output of fatcat.
            # We add a 5% margin to account for filesystem structures.
            if partition \
               and self.device_can_be_upgraded(data) \
               and hasattr(self, 'source') \
               and self.source is not None \
               and data['size'] < self.source.size * 1.05:
                self.log.warning(
                    'Device is too small for upgrade: %s'
                    % data['device'])
                data['is_device_big_enough_for_upgrade'] = False

            mount = data['mount']
            if mount:
                if len(mount) > 1:
                    self.log.warning('Multiple mount points for %s' %
                                     data['device'])
                mount = data['mount'] = data['mount'][0]
            else:
                mount = data['mount'] = None

            if parent_block and mount:
                if not data['parent'] in mounted_parts:
                    mounted_parts[data['parent']] = set()
                mounted_parts[data['parent']].add(data['udi'])

            data['free'] = mount and \
                    self.get_free_bytes(mount) / 1024**2 or None
            data['free'] = None

            self.log.debug(pformat(data))

            if not force_partitions and self.opts.partition:
                if self.device_can_be_upgraded(data):
                    self.drives[data['device']] = data
                # Add whole drive in partitioning mode
                elif data['parent'] is None:
                    # Ensure the device is writable
                    if block.props.read_only:
                        self.log.debug(_('Unable to write on %(device)s, skipping.')
                                       % {'device': data['device']})
                        continue
                    self.drives[data['device']] = data
            else:
                if self.device_is_isohybrid(data):
                    if data['parent']:
                        # We will target the parent instead
                        continue
                self.drives[data['device']] = data

            # Remove parent drives if a valid partition exists
            if not self.force_reinstall:
                for parent in [d['parent'] for d in self.drives.values()]:
                    if parent in self.drives:
                        del(self.drives[parent])

        self.log.debug(pformat(mounted_parts))

        for device, data in self.drives.iteritems():
            if self.source \
               and self.source.dev and data['udi'] == self.source.dev:
                continue
            if device in mounted_parts and len(mounted_parts[device]) > 0:
                data['mounted_partitions'] = mounted_parts[device]
                self.log.debug(_(
                    'Some partitions of the target device %(device)s are mounted. '
                    'They will be unmounted before starting the '
                    'installation process.') % {'device': data['device']})

        if callback:
            callback()

    def _storage_bus(self, dev):
        storage_bus = None
        try:
            storage_bus = dev.GetProperty('storage.bus')
        except Exception, e:
            self.log.exception(e)
        return storage_bus

    def _block_is_volume(self, dev):
        is_volume = False
        try:
            is_volume = dev.GetProperty("block.is_volume")
        except Exception, e:
            self.log.exception(e)
        return is_volume

    def _add_device(self, dev, parent=None):
        mount = str(dev.GetProperty('volume.mount_point'))
        device = str(dev.GetProperty('block.device'))
        if parent:
            parent = parent.GetProperty('block.device')
        self.drives[device] = {
            'label':     str(dev.GetProperty('volume.label')).replace(' ', '_'),
            'fstype':    str(dev.GetProperty('volume.fstype')),
            'fsversion': str(dev.GetProperty('volume.fsversion')),
            'uuid':      str(dev.GetProperty('volume.uuid')),
            'mount':     mount,
            'udi':       dev,
            'free':      mount and self.get_free_bytes(mount) / 1024**2 or None,
            'device':    device,
            'parent':    parent
        }

    def mount_device(self):
        """ Mount our device if it is not already mounted """
        if not self.fstype:
            raise TailsInstallerError(_("Unknown filesystem.  Your device "
                                        "may need to be reformatted."))
        if self.fstype not in self.valid_fstypes:
            raise TailsInstallerError(_("Unsupported filesystem: %s") %
                                      self.fstype)
        self.dest = self.drive['mount']
        if not self.dest:
            self.log.debug("Mounting %s" % self.drive['udi'])
            # XXX: this is racy and then it sometimes fails with:
            # 'NoneType' object has no attribute 'call_mount_sync'
            filesystem = self._get_object().props.filesystem
            mount = None
            try:
                mount = filesystem.call_mount_sync(
                    arg_options=GLib.Variant('a{sv}', None),
                    cancellable=None)
            except GLib.Error as e:
                if 'org.freedesktop.UDisks2.Error.AlreadyMounted' in e.message:
                    self.log.debug('Device already mounted')
                else:
                    raise TailsInstallerError(_(
                        'Unknown GLib exception while trying to '
                        'mount device: %(message)s')
                        % {'message': str(e)})
            except Exception, e:
                raise TailsInstallerError(_(
                    "Unable to mount device: %(message)s")
                    % {'message': str(e)})

            # Get the new mount point
            if not mount:
                self.log.error(_('No mount points found'))
            else:
                self.dest = self.drive['mount'] = mount
                self.drive['free'] = self.get_free_bytes(self.dest) / 1024**2
                self.log.debug("Mounted %s to %s " % (self.drive['device'],
                                                      self.dest))
        else:
            self.log.debug("Using existing mount: %s" % self.dest)

    def unmount_device(self):
        """ Unmount our device """
        self.log.debug(_("Entering unmount_device for '%(device)s'") % {
            'device': self.drive['device']
        })

        self.log.debug(pformat(self.drive))
        if self.drive['mount'] is None:
            udis = self.drive['mounted_partitions']
        else:
            udis = [self.drive['udi']]
        if udis:
            self.log.info(_("Unmounting mounted filesystems on '%(device)s'") % {
                'device': self.drive['device']
            })
        for udi in udis:
            self.log.debug(_("Unmounting '%(udi)s' on '%(device)s'") % {
                'device': self.drive['device'],
                'udi': udi
            })
            filesystem = self._get_object(udi).props.filesystem
            filesystem.call_unmount_sync(
                    arg_options=GLib.Variant('a{sv}', None),
                    cancellable=None)
        self.drive['mount'] = None
        if not self.opts.partition and self.dest is not None \
           and os.path.exists(self.dest):
            self.log.error(_("Mount %s exists after unmounting") % self.dest)
        self.dest = None
        # Sometimes the device is still considered as busy by the kernel
        # at this point, which prevents, when called by reset_mbr() ->
        # write_to_block_device() -> get_open_write_fd()
        # -> call_open_for_restore_sync() from opening it for writing.
        self.flush_buffers(silent=True)
        time.sleep(3)

    def partition_device(self):
        if not self.opts.partition:
            return

        self.log.info(_('Partitioning device %(device)s') % {
            'device': self.drive['device']
        })

        self.log.debug("Creating partition table")
        # Use udisks instead of plain sgdisk will allow unprivileged users
        # to get a refreshed partition table from the kernel
        for attempt in [1, 2]:
            try:
                self._get_object().props.block.call_format_sync(
                    'gpt',
                    arg_options=GLib.Variant('a{sv}', None),
                    cancellable=None)
            except GLib.Error as e:
                if attempt > 1:
                    raise
                # XXX: sometimes retrying fails as well
                # https://bugs.freedesktop.org/show_bug.cgi?id=76178
                if ('GDBus.Error:org.freedesktop.UDisks2.Error.Failed' in e.message and
                    'Error synchronizing after initial wipe' in e.message):
                    self.log.debug("Failed to synchronize. Trying again, which usually solves the issue. Error was: %s" % e.message)
                    self.flush_buffers(silent=True)
                    time.sleep(5)

        self.log.debug("Creating partition")
        partition_table = self._get_object().props.partition_table
        try:
            partition_table.call_create_partition_sync(
                    arg_offset=0,
                    arg_size=self.system_partition_size(
                        self.drive['parent_size']
                        if self.drive['parent_size']
                        else self.drive['size']),
                    arg_type=ESP_GUID,
                    arg_name=self.label,
                    arg_options=GLib.Variant('a{sv}', None),
                    cancellable=None)
        except GLib.Error as e:
            # XXX: as of Debian Jessie, we often get errors while wiping
            # (Debian bug #767457). We ignore them as they are not fatal
            # for us... but we need to fix a few things later...
            if ('GDBus.Error:org.freedesktop.UDisks2.Error.Failed' in e.message and
                    'Error wiping newly created partition' in e.message):
                self.log.debug("Ignoring error %s" % e.message)
            else:
                raise

        # Rescan the device as it seems this is not always done automatically.
        # And get the new object after the rescan, then its 1st partition
        # which is the one we just created on our new partition table.
        self.rescan_block_device(self._get_object().props.block)
        system_partition = self.first_partition(self.drive['udi'])

        # Get a fresh system_partition object, otherwise
        # _set_partition_flags sometimes fails with
        # "GDBus.Error:org.freedesktop.DBus.Error.UnknownMethod: No
        # such interface 'org.freedesktop.UDisks2.Partition' on object
        # at path /org/freedesktop/UDisks2/block_devices/sda1"
        self.rescan_block_device(self._get_object().props.block)
        system_partition = self.first_partition(self.drive['udi'])

        # XXX: This resets the partition type for some reason
        # (https://github.com/storaged-project/udisks/issues/418)
        # XXX: sometimes fails (https://labs.riseup.net/code/issues/10987)
        self._set_partition_flags(system_partition, SYSTEM_PARTITION_FLAGS)

        # _set_partition_flags resets the partition type with udisks2 2.7.3-4,
        # so let's set the right one again
        # XXX: sometimes fails (https://labs.riseup.net/code/issues/10987)
        system_partition.call_set_type_sync(ESP_GUID, GLib.Variant('a{sv}', None))

        # Give the system some more time to recognize the updated
        # partition, otherwise sometimes later on, when
        # switch_drive_to_system_partition is called, it calls
        # _set_drive, that fails with "Cannot find device /dev/sda1".
        self.rescan_block_device(self._get_object().props.block)

    def is_partition_GPT(self, drive=None):
        # Check if the partition scheme is GPT
        if drive:
            obj = self._get_object(drive['udi'])
        else:
            obj = self._get_object()

        # First check if we actually have found the object
        if obj is None:
           return False
        # and then if it has a partition
        if not obj.props.partition:
            return False

        partition_table = obj.props.partition_table
        if not partition_table:
            partition_table = self._udisksclient.get_partition_table(obj.props.partition)
        if partition_table.props.type == 'gpt':
            return True
        else:
            return False

    def device_can_be_upgraded(self, device_data=None):
        # Checks that device already has Tails installed
        if not device_data:
            device = self.drive
        else:
            device = device_data
        return not self.device_is_isohybrid(device) and self.is_partition_GPT(device) \
               and device['fstype'] == 'vfat' and device['label'] == 'Tails'

    def device_is_isohybrid(self, drive=None):
        if not drive:
            device = self.drive
        else:
            device = drive
        return device['fstype'] == 'iso9660'

    def save_full_drive(self):
        self._full_drive = self.drives[self._drive]

    def switch_drive_to_system_partition(self):
        full_drive_name = self._full_drive['device']
        append = False
        if full_drive_name.startswith('/dev/sd'):
            append = '1'
        elif full_drive_name.startswith('/dev/mmcblk'):
            append = 'p1'
        if not append:
            self.log.warning(
                _("Unsupported device '%(device)s', please report a bug." %
                  {'device': full_drive_name})
            )
            self.log.info(_('Trying to continue anyway.'))
            append = '1'
        self.drive = '%s%s' % (full_drive_name, append)

    def switch_back_to_full_drive(self):
        self.drives[self._full_drive['device']] = self._full_drive
        self.drive = self._full_drive['device']

    def verify_filesystem(self):
        self.log.info(_("Verifying filesystem..."))
        if self.fstype not in self.valid_fstypes:
            if not self.fstype:
                raise TailsInstallerError(_("Unknown filesystem.  Your device "
                                            "may need to be reformatted."))
            else:
                raise TailsInstallerError(_("Unsupported filesystem: %s" %
                                            self.fstype))
        if self.drive['label'] != self.label:
            self.log.info("Setting %(device)s label to %(label)s" %
                          {'device': self.drive['device'],
                           'label': self.label})
            try:
                if self.fstype in ('vfat', 'msdos'):
                    try:
                        self.popen('/sbin/dosfslabel %s %s' % (
                                   self.drive['device'], self.label))
                    except TailsInstallerError:
                        # dosfslabel returns an error code even upon success
                        pass
                else:
                    self.popen('/sbin/e2label %s %s' % (self.drive['device'],
                                                        self.label))
            except TailsInstallerError, e:
                self.log.error(_("Unable to change volume label: %(message)s") % {'message': str(e)})

    def install_bootloader(self):
        """ Run syslinux to install the bootloader on our devices """
        TailsInstallerCreator.install_bootloader(self)
        self.log.info(_("Installing bootloader..."))

        if not is_running_from_tails():
            # In this case, we are using the currently running system's
            # syslinux binary, so we have to install the corresponding COM32
            # modules, as syslinux doesn't guarantee their ABI.
            com32modules = [
                f for f in os.listdir(self.get_liveos_file_path('syslinux'))
                if f.endswith('.c32')
            ]
            for com32mod in com32modules:
                copied = False
                for orig_dir in ('/usr/lib/syslinux/modules/bios',
                                 '/usr/lib/syslinux/bios',
                                 '/usr/share/syslinux',
                                 '/usr/lib/syslinux'):
                    com32path = os.path.join(orig_dir, com32mod)
                    if os.path.isfile(com32path):
                        self.log.debug('Copying %s to the device' % com32path)
                        shutil.copyfile(com32path,
                                        os.path.join(
                                            self.get_liveos_file_path('syslinux'),
                                            com32mod))
                        copied = True
                        break
                if not copied:
                    raise TailsInstallerError(_(
                        "Could not find the '%s' COM32 module")
                        % com32mod)

        # Don't prompt about overwriting files from mtools (#491234)
        for ldlinux in [self.get_liveos_file_path(p, 'ldlinux.sys')
                        for p in ('syslinux', '')]:
            self.log.debug('Looking for %s' % ldlinux)
            if os.path.isfile(ldlinux):
                self.log.debug(_("Removing %(file)s") % {'file': ldlinux})
                os.unlink(ldlinux)

        # FAT
        if is_running_from_tails():
            syslinux_executable = 'syslinux';
            self.log.debug('Will use %s as the syslinux binary'
                           % syslinux_executable)
            iso_syslinux = self.get_liveos_file_path('utils', 'linux',
                                                     syslinux_executable)
            tmpdir = tempfile.mkdtemp()
            tmp_syslinux = os.path.join(tmpdir, syslinux_executable)
            shutil.copy(iso_syslinux, tmp_syslinux)
            os.chmod(tmp_syslinux,
                     os.stat(tmp_syslinux).st_mode | stat.S_IEXEC | stat.S_IXGRP | stat.S_IXOTH)
            self.flush_buffers()
            self.unmount_device()
            self.popen('%s %s -d %s %s' % (
                    tmp_syslinux,
                    ' '.join(self.syslinux_options()),
                    'syslinux', self.drive['device']))
            shutil.rmtree(tmpdir)
        else:
            self.flush_buffers()
            self.unmount_device()
            self.popen(
                '/usr/bin/pkexec /usr/bin/syslinux %s -d syslinux %s' % (
                    ' '.join(self.syslinux_options()),
                    self.drive['device']))

    def get_free_bytes(self, device=None):
        """ Return the number of available bytes on our device """
        import statvfs
        device = device and device or self.dest
        if device is None:
            return None
        stat = os.statvfs(device)
        return stat[statvfs.F_BSIZE] * stat[statvfs.F_BAVAIL]

    def _get_object(self, udi=None):
        """Return an UDisks.Object for our drive"""
        if not udi:
            udi = self.drive['udi']
        return self._udisksclient.get_object(udi)

    def first_partition(self, udi=None):
        """Return the UDisks2.Partition object for the first partition on the drive"""
        if not udi:
            udi = self.drive['udi']
        obj = self._get_object(udi)
        partition_table = obj.props.partition_table
        partitions = self._udisksclient.get_partitions(partition_table)
        return partitions[0]

    def terminate(self):
        for pid in self.pids:
            try:
                os.kill(pid, signal.SIGHUP)
                self.log.debug("Killed process %d" % pid)
            except OSError, e:
                self.log.debug(str(e))
        if os.path.exists(self._error_log_filename):
            if not os.path.getsize(self._error_log_filename):
                # We don't want any failure here to block other tear down tasks
                try:
                    os.unlink(self._error_log_filename)
                except:
                    print >> sys.stderr, "Could not delete log file."

    def get_proxies(self):
        """ Return the proxy settings.

        At the moment this implementation only works on KDE, and should
        eventually be expanded to support other platforms as well.
        """
        try:
            from PyQt4 import QtCore
        except ImportError:
            self.log.warning("PyQt4 module not installed; skipping KDE "
                             "proxy detection")
            return
        kioslaverc = QtCore.QDir.homePath() + '/.kde/share/config/kioslaverc'
        if not QtCore.QFile.exists(kioslaverc):
            return {}
        settings = QtCore.QSettings(kioslaverc, QtCore.QSettings.IniFormat)
        settings.beginGroup('Proxy Settings')
        proxies = {}
        # check for KProtocolManager::ManualProxy (the only one we support)
        if settings.value('ProxyType').toInt()[0] == 1:
            httpProxy = settings.value('httpProxy').toString()
            if httpProxy != '':
                proxies['http'] = httpProxy
            ftpProxy = settings.value('ftpProxy').toString()
            if ftpProxy != '':
                proxies['ftp'] = ftpProxy
        return proxies

    def bootable_partition(self):
        """ Ensure that the selected partition is flagged as bootable """
        if self.opts.partition:
            # already done at partitioning step
            return
        if self.drive.get('parent') is None:
            self.log.debug('No partitions on device; not attempting to mark '
                           'any partitions as bootable')
            return
        import parted
        try:
            disk, partition = self.get_disk_partition()
        except TailsInstallerError, e:
            self.log.exception(e)
            return
        if partition.isFlagAvailable(parted.PARTITION_BOOT):
            if partition.getFlag(parted.PARTITION_BOOT):
                self.log.debug(_('%s already bootable') % self._drive)
            else:
                partition.setFlag(parted.PARTITION_BOOT)
                try:
                    disk.commit()
                    self.log.info('Marked %s as bootable' % self._drive)
                except Exception, e:
                    self.log.exception(e)
        else:
            self.log.warning('%s does not have boot flag' % self._drive)

    def get_disk_partition(self):
        """ Return the PedDisk and partition of the selected device """
        import parted
        parent = self.drives[self._drive]['parent']
        dev = parted.Device(path=parent)
        disk = parted.Disk(device=dev)
        for part in disk.partitions:
            if self._drive == "/dev/%s" %(part.getDeviceNodeName(),):
                return disk, part
        raise TailsInstallerError(_("Unable to find partition"))

    def initialize_zip_geometry(self):
        """ This method initializes the selected device in a zip-like fashion.

        :Note: This feature is currently experimental, and will DESTROY ALL DATA
               on your device!

        More details on this can be found here:
            http://syslinux.zytor.com/doc/usbkey.txt
        """
        #from parted import PedDevice
        self.log.info('Initializing %s in a zip-like fashon' % self._drive)
        heads = 64
        cylinders = 32
        # Is this part even necessary?
        #device = PedDevice.get(self._drive[:-1])
        #cylinders = int(device.cylinders / (64 * 32))
        self.popen('/usr/lib/syslinux/mkdiskimage -4 %s 0 %d %d' % (
                   self._drive[:-1], heads, cylinders))

    def format_device(self):
        """ Format the selected partition as FAT32 """
        self.log.info(_('Formatting %(device)s as FAT32') % {'device': self._drive})
        dev = self._get_object()
        block = dev.props.block
        try:
            block.call_format_sync(
                'vfat',
                arg_options=GLib.Variant(
                    'a{sv}',
                    {'label': GLib.Variant('s', self.label),
                     'update-partition-type': GLib.Variant('s', 'FALSE')}))
        except GLib.Error as e:
            if ('GDBus.Error:org.freedesktop.UDisks2.Error.Failed' in e.message and
                ('Error synchronizing after formatting' in e.message
                 or 'Error synchronizing after initial wipe' in e.message)):
                self.log.debug("Failed to synchronize. Trying again, which usually solves the issue. Error was: %s" % e.message)
                self.flush_buffers(silent=True)
                time.sleep(5)
                block.call_format_sync(
                    'vfat',
                    arg_options=GLib.Variant(
                        'a{sv}',
                        {'label': GLib.Variant('s', self.label),
                         'update-partition-type': GLib.Variant('s', 'FALSE')}))
            else:
                raise

        self.fstype = self.drive['fstype'] = 'vfat'
        self.flush_buffers(silent=True)
        time.sleep(3)
        self._get_object().props.block.call_rescan_sync(GLib.Variant('a{sv}', None))

    def get_mbr(self):
        parent = self.drive.get('parent', self._drive)
        if parent is None:
            parent = self._drive
        parent = str(parent)
        self.log.debug('Checking the MBR of %s' % parent)
        drive = open(parent, 'rb')
        mbr = ''.join(['%02X' % ord(x) for x in drive.read(2)])
        drive.close()
        self.log.debug('mbr = %r' % mbr)
        return mbr

    def blank_mbr(self):
        """ Return whether the MBR is empty or not """
        return self.get_mbr() == '0000'

    def _get_mbr_bin(self):
        if is_running_from_tails():
            # We install syslinux' gptmbr.bin as mbr.bin there, for
            # compatibility with paths used by Tuxboot and possibly others
            return self.get_liveos_file_path('utils', 'mbr', 'mbr.bin')
        else:
            for mbr_bin in ('/usr/lib/syslinux/mbr/gptmbr.bin',
                            '/usr/lib/SYSLINUX/gptmbr.bin',
                            '/usr/lib/syslinux/gptmbr.bin',
                            '/usr/lib/syslinux/bios/gptmbr.bin',
                            '/usr/share/syslinux/gptmbr.bin'):
                if os.path.exists(mbr_bin):
                    return mbr_bin
        raise TailsInstallerError(_("Could not find syslinux' gptmbr.bin"))

    def mbr_matches_syslinux_bin(self):
        """
        Return whether or not the MBR on the drive matches the syslinux'
        gptmbr.bin found in the system being installed
        """
        mbr_bin = open(self._get_mbr_bin(), 'rb')
        mbr = ''.join(['%02X' % ord(x) for x in mbr_bin.read(2)])
        return mbr == self.get_mbr()

    def read_extracted_mbr(self):
        mbr_path = self._get_mbr_bin()
        self.log.info(_('Reading extracted MBR from %s') % mbr_path)
        with open(mbr_path, 'rb') as mbr_fd:
            self.extracted_mbr_content = mbr_fd.read()
        if not len(self.extracted_mbr_content):
            raise TailsInstallerError(_("Could not read the extracted MBR from %(path)s"
                                 % {path: mbr_path}))

    def reset_mbr(self):
        parent = parent = self.drive.get('parent', self._drive)
        if parent is None:
            parent = self._drive
            parent_udi = self.drive['udi']
        else:
            parent_udi = self.drive['parent_udi']
        parent_udi = str(parent_udi)
        parent = str(parent)
        if '/dev/loop' not in self.drive:
            self.log.info(_('Resetting Master Boot Record of %s') % parent)
            self.log.debug(_('Resetting Master Boot Record of %s') % parent_udi)
            obj = self._get_object(udi=parent_udi)
            block = obj.props.block
            write_to_block_device(block, self.extracted_mbr_content)
        else:
            self.log.info(_('Drive is a loopback, skipping MBR reset'))

    def calculate_device_checksum(self, progress=None):
        """ Calculate the SHA1 checksum of the device """
        self.log.info(_("Calculating the SHA1 of %s") % self._drive)
        if not progress:
            class DummyProgress:
                def set_max_progress(self, value): pass
                def update_progress(self, value): pass
            progress = DummyProgress()
        # Get size of drive
        #progress.set_max_progress(self.source.size / 1024)
        checksum = hashlib.sha1()
        device_name = str(self.drive['parent'])
        device = file(device_name, 'rb')
        bytes = 1024**2
        total = 0
        while bytes:
            data = device.read(bytes)
            checksum.update(data)
            bytes = len(data)
            total += bytes
            progress.update_progress(total / 1024)
        hexdigest = checksum.hexdigest()
        self.log.info("sha1(%s) = %s" % (device_name, hexdigest))
        return hexdigest

    def flush_buffers(self, silent=False):
        if not silent:
            self.log.info(_("Synchronizing data on disk..."))
        self.popen('sync')

    def rescan_block_device(self, block):
        self._udisksclient.settle()
        self.flush_buffers(silent=True)
        time.sleep(5)
        block.call_rescan_sync(GLib.Variant('a{sv}', None))

    def is_admin(self):
        return os.getuid() == 0

    def connect_drive_monitor(self, callback, data=None):
        self._udisksclient.connect('changed', callback, data)

class WindowsTailsInstallerCreator(TailsInstallerCreator):

    def detect_supported_drives(self, callback=None):
        import win32file, win32api, pywintypes
        self.drives = {}
        for drive in [l + ':' for l in 'ABCDEFGHIJKLMNOPQRSTUVWXYZ']:
            try:
                if win32file.GetDriveType(drive) == win32file.DRIVE_REMOVABLE or \
                   drive == self.opts.force:
                    vol = [None]
                    try:
                        vol = win32api.GetVolumeInformation(drive)
                    except pywintypes.error, e:
                        self.log.error('Unable to get GetVolumeInformation(%s): %s' % (drive, str(e)))
                        continue
                    self.drives[drive] = {
                        'label': vol[0],
                        'mount': drive,
                        'uuid': self._get_device_uuid(drive),
                        'free': self.get_free_bytes(drive) / 1024**2,
                        'fstype': 'vfat',
                        'device': drive,
                        'fsversion': vol[-1],
                        'size': self._get_device_size(drive)
                    }
            except Exception, e:
                self.log.exception(e)
                self.log.error(_("Error probing device"))
        if not len(self.drives):
            raise TailsInstallerError(_("Unable to find any supported device"))
        if callback:
            callback()

    def verify_filesystem(self):
        import win32api, win32file, pywintypes
        self.log.info(_("Verifying filesystem..."))
        try:
            vol = win32api.GetVolumeInformation(self.drive['device'])
        except Exception, e:
            raise TailsInstallerError(_("Make sure your USB key is plugged in and "
                                 "formatted with the FAT filesystem"))
        if vol[-1] not in ('FAT32', 'FAT'):
            raise TailsInstallerError(_("Unsupported filesystem: %s\nPlease backup "
                                 "and format your USB key with the FAT "
                                 "filesystem." % vol[-1]))
        self.fstype = 'vfat'
        if vol[0] != self.label:
            try:
                win32file.SetVolumeLabel(self.drive['device'], self.label)
                self.log.debug("Set %s label to %s" % (self.drive['device'],
                                                       self.label))
            except pywintypes.error, e:
                self.log.warning("Unable to SetVolumeLabel: " + str(e))

    def get_free_bytes(self, device=None):
        """ Return the number of free bytes on our selected drive """
        import win32file
        device = device and device or self.drive['device']
        try:
            (spc, bps, fc, tc) = win32file.GetDiskFreeSpace(device)
        except Exception, e:
            self.log.error("Problem determining free space: %s" % str(e))
            return 0
        return fc * (spc * bps) # free-clusters * bytes per-cluster

    def install_bootloader(self):
        """ Run syslinux to install the bootloader on our device """
        TailsInstallerCreator.install_bootloader(self)
        self.log.info(_("Installing bootloader..."))
        device = self.drive['device']
        syslinuxdir = os.path.join(device + os.path.sep, "syslinux")
        if os.path.isdir(syslinuxdir):
            # Python for Windows is unable to delete read-only files, and some
            # may exist here if the TailsInstaller stick was created in Linux
            for f in os.listdir(syslinuxdir):
                os.chmod(os.path.join(syslinuxdir, f), 0777)
            shutil.rmtree(syslinuxdir)
        _move_if_exists(os.path.join(device + os.path.sep, "isolinux"),
                        syslinuxdir)
        _unlink_if_exists(os.path.join(syslinuxdir, "isolinux.cfg"))

        # Don't prompt about overwriting files from mtools (#491234)
        for ldlinux in [os.path.join(device + os.path.sep, p, 'ldlinux.sys')
                        for p in (syslinuxdir, '')]:
            if os.path.isfile(ldlinux):
                os.chmod(ldlinux, 0777)
                self.log.debug(_("Removing %(file)s" % {'file': ldlinux}))
                os.unlink(ldlinux)

        self.popen('syslinux %s -m -a -d %s %s' %  (
                ' '.join(self.syslinux_options()), 'syslinux', device))

    # Cache these, because they are fairly expensive
    _win32_logicaldisk = {}

    def _get_win32_logicaldisk(self, drive):
        """ Return the Win32_LogicalDisk object for the given drive """
        import win32com.client
        cache = self._win32_logicaldisk.get('drive')
        if cache:
            return cache
        obj = None
        try:
            obj = win32com.client.Dispatch("WbemScripting.SWbemLocator") \
                         .ConnectServer(".", "root\cimv2") \
                         .ExecQuery("Select * from "
                                    "Win32_LogicalDisk where Name = '%s'" %
                                    drive)
            if not obj:
                self.log.error(_("Unable to get Win32_LogicalDisk; win32com "
                                 "query did not return any results"))
            else:
                obj = obj[0]
                self._win32_logicaldisk[drive] = obj
        except Exception, e:
            self.log.exception(e)
            self.log.error("Unable to get Win32_LogicalDisk")
        return obj

    def _get_device_uuid(self, drive):
        """ Return the UUID of our selected drive """
        if self.uuid:
            return self.uuid
        uuid = None
        try:
            uuid = self._get_win32_logicaldisk(drive).VolumeSerialNumber
            if uuid in (None, 'None', ''):
                uuid = None
            else:
                uuid = uuid[:4] + '-' + uuid[4:]
            self.log.debug("Found UUID %s for %s" % (uuid, drive))
        except Exception, e:
            self.log.exception(e)
            self.log.warning("Exception while fetching UUID: %s" % str(e))
        return uuid

    def _get_device_size(self, drive):
        """ Return the size of the given drive """
        size = None
        try:
            size = int(self._get_win32_logicaldisk(drive).Size)
            self.log.debug("Max size of %s: %d" % (drive, size))
        except Exception, e:
            self.log.exception(e)
            self.log.warning("Error getting drive size: %s" % str(e))
        return size

    def popen(self, cmd, **kwargs):
        import win32process
        if isinstance(cmd, basestring):
            cmd = cmd.split()
        prgmfiles = os.getenv('PROGRAMFILES')
        folder = 'TailsInstaller Creator'
        paths = [os.path.join(x, folder) for x in (prgmfiles, prgmfiles + ' (x86)')]
        paths += [os.path.join(os.path.dirname(__file__), '..', '..'), '.']
        tool = None
        for path in paths:
            exe = os.path.join(path, 'tools', '%s.exe' % cmd[0])
            if os.path.exists(exe):
                tool = '"%s"' % exe
                break
        else:
            raise TailsInstallerError(_("Cannot find") + ' %s.  ' % (cmd[0]) +
                               _("Make sure to extract the entire "
                                 "tails-installer zip file before "
                                 "running this program."))
        return TailsInstallerCreator.popen(self, ' '.join([tool] + cmd[1:]),
                                    creationflags=win32process.CREATE_NO_WINDOW,
                                    **kwargs)

    def terminate(self):
        """ Terminate any subprocesses that we have spawned """
        import win32api, win32con, pywintypes
        for pid in self.pids:
            try:
                handle = win32api.OpenProcess(win32con.PROCESS_TERMINATE,
                                              False, pid)
                self.log.debug("Terminating process %s" % pid)
                win32api.TerminateProcess(handle, -2)
                win32api.CloseHandle(handle)
            except pywintypes.error:
                pass

    def mount_device(self):
        self.dest = self.drive['mount']

    def unmount_device(self):
        pass

    def get_proxies(self):
        proxies = {}
        try:
            import _winreg as winreg
            settings = winreg.OpenKey(winreg.HKEY_CURRENT_USER,
                                      'Software\\Microsoft\\Windows'
                                      '\\CurrentVersion\\Internet Settings')
            proxy = winreg.QueryValueEx(settings, "ProxyEnable")[0]
            if proxy:
                server = str(winreg.QueryValueEx(settings, 'ProxyServer')[0])
                if ';' in server:
                    for p in server.split(';'):
                        protocol, address = p.split('=')
                        proxies[protocol] = '%s://%s' % (protocol, address)
                else:
                    proxies['http'] = 'http://%s' % server
                    proxies['ftp'] = 'ftp://%s' % server
            settings.Close()
        except Exception, e:
            self.log.warning('Unable to detect proxy settings: %s' % str(e))
        self.log.debug('Using proxies: %s' % proxies)
        return proxies

    def calculate_device_checksum(self, progress=None):
        """ Calculate the SHA1 checksum of the device """
        self.log.info(_("Calculating the SHA1 of %s" % self._drive))
        time.sleep(3)
        if not progress:
            class DummyProgress:
                def set_max_progress(self, value): pass
                def update_progress(self, value): pass
            progress = DummyProgress()
        progress.set_max_progress(self.drive['size'])
        checksum = hashlib.sha1()
        device_name = r'\\.\%s' % self.drive['device']
        device = file(device_name, 'rb')
        bytes = 1
        total = 0
        while bytes:
            data = device.read(1024**2)
            checksum.update(data)
            bytes = len(data)
            total += bytes
            progress.update_progress(total)
        hexdigest = checksum.hexdigest()
        self.log.info("sha1(%s) = %s" % (self.drive['device'], hexdigest))
        return hexdigest

    def calculate_liveos_checksum(self):
        """ Calculate the hash of the extracted LiveOS """
        chunk_size = 1024 # FIXME: optimize this.  we hit bugs when this is *not* 1024
        checksums = []
        for img in (os.path.join('LiveOS', 'osmin.img'),
                    os.path.join('LiveOS', 'squashfs.img'),
                    os.path.join('syslinux', 'initrd0.img'),
                    os.path.join('syslinux', 'vmlinuz0'),
                    os.path.join('syslinux', 'isolinux.bin')):
            hash = getattr(hashlib, self.opts.hash, 'sha1')()
            liveos = os.path.join(self.drive['device'], img)
            device = file(liveos, 'rb')
            self.log.info("Calculating the %s of %s" % (hash.name, liveos))
            bytes = 1
            while bytes:
                data = device.read(chunk_size)
                hash.update(data)
                bytes = len(data)
            checksum = hash.hexdigest()
            checksums.append(checksum)
            self.log.info('%s(%s) = %s' % (hash.name, liveos, checksum))

        # Take a checksum of all of the checksums
        hash = getattr(hashlib, self.opts.hash, 'sha1')()
        map(hash.update, checksums)
        self.log.info("%s = %s" % (hash.name, hash.hexdigest()))

    def format_device(self):
        """ Format the selected partition as FAT32 """
        self.log.info('Formatting %s as FAT32' % self.drive['device'])
        self.popen('format /Q /X /y /V:Fedora /FS:FAT32 %s' % self.drive['device'])

    def is_admin(self):
        import pywintypes
        try:
            from win32com.shell import shell
            return shell.IsUserAnAdmin()
        except pywintypes.com_error:
            # Thrown on certain XP installs
            return True
