Why Gemfury? Push, build, and install  RubyGems npm packages Python packages Maven artifacts PHP packages Go Modules Debian packages RPM packages NuGet packages

Repository URL to install this package:

Details    
crossover / opt / cxoffice / lib / python / cxaiecore.py
Size: Mime:
# (c) Copyright 2009-2013, 2015. CodeWeavers, Inc.

import errno
import os
import os.path
import shutil
import stat
import zipfile
import tarfile
import tempfile
import threading
import traceback

import cxlog
import cxulog
import cxutils
import globtree

import bottlequery
import cxaiebase
import cxaiemedia
import appdetector
import distversion


# for localization
from cxutils import cxgettext as _

def tar_member_depth(member):
    "Returns the number of directories leading up to this file"
    return os.path.normpath(member.name).count('/')

#####
#
# The core application installation task
#
#####

class AIECore(cxaiebase.AIETask):
    """Installs a specific application."""

    #####
    #
    # Limit concurrency
    #
    #####

    def _getniceness(self):
        """Start downloads before all else."""
        return self.depth

    niceness = property(_getniceness)


    def can_run(self):
        """Returns True if this task is runnable and no other instance of it
        is running.
        """
        if 'AIECore' in self.scheduler.userdata:
            return False
        return cxaiebase.AIETask.can_run(self)


    def schedule(self):
        cxaiebase.AIETask.schedule(self)
        self.scheduler.userdata['AIECore'] = self


    def done(self):
        cxaiebase.AIETask.done(self)
        try:
            del self.scheduler.userdata['AIECore']
        except KeyError:
            cxlog.err(cxlog.to_str(self) + ": AIECore should have been set")


    #####
    #
    # Initialization
    #
    #####

    def __init__(self, engine, installer):
        cxaiebase.AIETask.__init__(self, engine, _("Installing %s") % installer.parent.name)
        self.installer = installer

        # Internal housekeeping variables
        self._has_printed_install_log_err = False

        # Related objects to download the application or handle CDs
        self.download = None
        self.insert_media = None
        self.scan_media = None
        self.release_media = None
        self.canceled = False

        # Nearest AIECore objects with an associated media
        self.parent_media = set()

        self.cleanup_lock = threading.Lock()

        # depth is 0 for the top profile, -1 for those it depends on, etc.
        # Note that depth always reflects the depth of the longest path from
        # the top-level profile.
        self.depth = 0

        # Stores the state information used at run time
        self.state = {}


    def _getname(self):
        """A helper function that returns the associated profile name."""
        return self.installer.parent.name

    name = property(_getname)


    def _getaiecore(self):
        """A helper function that makes AIECore more similar to other types of
        tasks.
        """
        return self

    aiecore = property(_getaiecore)


    def __unicode__(self):
        return "%s(%s)" % (self.__class__.__name__, self.name)


    def create_download_task(self):
        self.download = cxaiemedia.AIEDownload(self)


    def create_media_tasks(self):
        self.insert_media = cxaiemedia.AIEInsertMedia(self)
        self.scan_media = cxaiemedia.AIEScanMedia(self)
        self.scan_media.add_dependency(self.insert_media)
        self.release_media = cxaiemedia.AIEReleaseMedia(self)
        self.release_media.add_dependency(self.scan_media)


    def run_routines(self, prefix, routines):
        if 'iter' in self.state:
            iterator = self.state['routine_iterator']
        else:
            iterator = iter(routines)
            self.state['routine_iterator'] = iterator
        for routine in iterator:
            skip_routine = 'no_' + routine
            if skip_routine in self.state:
                continue
            if self.canceled:
                return True
            try:
                func = AIECore.__dict__['_%s_%s' % (prefix, routine)]
            except KeyError:
                cxlog.err('AIECore._%s_%s does not exist!' % (cxlog.to_str(prefix), cxlog.to_str(routine)))
                continue
            cxlog.log_("aie", "  running " + cxlog.to_str(routine))
            if not func(self):
                cxlog.log_("aie", "  -> returned False")
                if not self.error:
                    cxlog.err(cxlog.to_str(routine) + ' did not set self.error')
                    self.error = _("An unknown error occurred in %s") % routine
                return False

        del self.state['routine_iterator']
        return True



    #####
    #
    # Usage logging
    #
    #####

    def log_usage(self):
        if self.install_failure_detected():
            fail = 1
        else:
            fail = 0
        log_str = "install %s %d %d\n" % (self.installer.parent.appid, self.canceled, fail)
        cxulog.log_usage(log_str)


    #####
    #
    # Prepare
    #
    #####

    def _prep_all_fonts(self):
        cxlog.log_('aie', 'appid=' + cxlog.to_str(self.installer.parent.appid))
        if self.installer.parent.appid == 'com.codeweavers.c4.6959':
            for font in self.dependencies:
                if isinstance(font, AIECore):
                    font.state['silentfontinstall'] = True
                    cxlog.log_('aie', 'set silentfontinstall on ' + cxlog.to_str(font.name))
        return True


    def _prep_collate_skips(self):
        engine = self.scheduler
        if 'skipassoc' not in engine.state:
            engine.state['skipassoc'] = True
            engine.state['skipmenu'] = True
            engine.state['installnsplugins'] = False
            # These are only set when the corresponding task succeeds though
            engine.state['defaulteassocs'] = set()
            engine.state['alteassocs'] = set()
        engine.state['skipassoc'] &= self.installer.skip_assoc_creation
        engine.state['skipmenu'] &= self.installer.skip_menu_creation
        engine.state['installnsplugins'] |= self.installer.install_nsplugins
        return True


    def _prep_collate_nsplugins(self):
        engine = self.scheduler
        if 'pre_nsplugins' not in engine.state:
            engine.state['pre_nsplugins'] = set()
            # This one is only set when the corresponding task is run though
            engine.state['ignore_nsplugins'] = set()
        engine.state['pre_nsplugins'].update(self.installer.pre_install_nsplugin_dlls)
        cxlog.log_("aie", "collate_nsplugins %s" % cxlog.to_str(engine.state['pre_nsplugins']))

        return True


    def prepare(self):
        """This method is called by the install engine after all the
        AIECore tasks have been created. The calls are made on children before
        the tasks that depend on them. They are not allowed to fail.
        """
        routines = ('all_fonts',
                    'collate_skips',
                    'collate_nsplugins',
                   )
        return self.run_routines('prep', routines)



    #####
    #
    # Main
    #
    #####

    def _main_setup_drive_letter(self):
        """Create a drive letter for the install source if needed."""
        install_source = self.state.get('install_source')

        if install_source and os.path.isdir(install_source):
            drive_letter = bottlequery.add_drive(self.scheduler.installtask.bottlename, install_source)
            if drive_letter:
                self.state['temporary_drive'] = drive_letter

        return True


    def _cleanup_delete_drive_letter(self):
        """Delete the temporary drive letter if we created one."""
        if 'temporary_drive' in self.state:
            bottlequery.rm_drive(self.scheduler.installtask.bottlename, self.state['temporary_drive'])
            del self.state['temporary_drive']

        return True


    def _main_setup_winver(self):
        """Temporarily change the windows version of the bottle if needed."""
        if self.installer.installer_winver:
            installer_winver = bottlequery.version_nicknames.get(self.installer.installer_winver, self.installer.installer_winver)
            bottlename = self.scheduler.installtask.bottlename
            orig_winver = bottlequery.get_windows_version(bottlename)
            if installer_winver != orig_winver:
                self.state['orig_winver'] = orig_winver
                bottlequery.set_windows_version(bottlename, installer_winver)

        return True


    def _cleanup_reset_winver(self):
        """Restore the windows version in this bottle if we changed it."""
        if 'orig_winver' in self.state:
            bottlequery.set_windows_version(self.scheduler.installtask.bottlename, self.state['orig_winver'])
            del self.state['orig_winver']

        return True


    def _main_set_installer_file(self):
        """Check that we have an installation source and initialize
        installer_file as appropriate.
        """
        install_source = self.state.get('install_source')
        if 'virtual' in self.installer.parent.app_profile.flags:
            # Virtual profiles are not supposed to have an installation source
            if install_source:
                cxlog.warn("install_source = %s for the %s virtual profile!" % (cxlog.debug_str(install_source), cxlog.debug_str(self.name)))
                del self.state['install_source']
            return True
        if not install_source:
            self.error = _("No installation source was provided for %s") % self.name
            return False

        if os.path.isfile(install_source):
            self.state['installer_file'] = install_source
            self.state['install_source'] = os.path.dirname(install_source)
        elif install_source.lower().startswith("steam:"):
            self.state['installer_file'] = install_source
            self.state['install_source'] = install_source
        elif not os.path.isdir(install_source):
            self.error = _("The '%(source)s' installation source of %(name)s is neither a file nor a directory!") % {'source': install_source, 'name': self.name}
            return False

        return True


    def _main_setup_environment(self):
        """Set up the Unix environment to be used by this task's child
        processes.
        """
        env = self.state['environ'] = self.scheduler.state['environ'].copy()
        for envvar in self.installer.installer_environment:
            env[envvar.name] = self.scheduler.expand_win_string(envvar.value)

        # Defer the menu creation until we run cxmenu --sync
        env['CX_NO_WINESHELLLINK'] = '1'

        # Prevent the application installers from scanning all of the user's
        # hard drive.
        env['CX_HACK_REMOTE_DRIVES'] = env.get('CX_HACK_REMOTE_DRIVES', 'yz')

        if self.state.get('install_source', '').lower().startswith('steam:'):
            env['WINE_WAIT_CHILD_PIPE_IGNORE'] = "steam.exe"

        if self.installer.parent.appid == 'com.codeweavers.c4.206' or \
            self.state.get('install_source', '').lower().startswith('steam:'):
            # Do not ever start a steam process with CX_NO_WINESHELLLINK
            # because steam makes its own menus independent of our install logic.
            del env['CX_NO_WINESHELLLINK']

        return True


    def _main_setup_registry_pre(self):
        """Tweak the windows registry for child processes.

        Do it early enough so that it's ready self-extracting installers.
        """
        for regentry in self.installer.pre_install_registry:
            for regvalue in regentry.values:
                if not bottlequery.set_registry_key(self.scheduler.installtask.bottlename, regentry.key, regvalue.name, regvalue.data):
                    return False
        return True


    def _main_uncompress(self):
        """Check if the installer file is an archive.

        If so uncompress it into a temporary directory and arrange for it to
        be used as the install_source in the following steps. This installers
        can be compressed and it's transparent to the later steps. Also arrange
        for the temporary directory to be deleted at the end.
        """
        if 'install_source' not in self.state:
            # Nothing to do for virtual packages
            return True

        installer_file = self.state.get('installer_file')
        if installer_file is None:
            # Nothing to uncompress. We are done here.
            return True

        archive_type = None
        if self.installer.installer_treatas:
            lower = self.installer.installer_treatas.lower()
        else:
            lower = installer_file.lower()
        if lower.endswith('.zip'):
            archive_type = 'zip'
        elif lower.endswith('.cab'):
            archive_type = 'cab'
        elif lower.endswith('.tgz'):
            archive_type = 'tar'
        elif lower.endswith('.tar.gz'):
            archive_type = 'tar'
        elif lower.endswith('.tar.bz2'):
            archive_type = 'tar'
        elif lower.endswith('.tar'):
            archive_type = 'tar'
        elif lower.endswith('.tbz'):
            archive_type = 'tar'
        elif lower.endswith('.tb2'):
            archive_type = 'tar'
        elif lower.endswith('.rar'):
            archive_type = 'rar'
        elif lower.endswith('.7z'):
            archive_type = '7z'
        elif self.installer.selfextract_threshold is not None:
            try:
                if os.path.getsize(installer_file) >= self.installer.selfextract_threshold * 1024:
                    archive_type = 'selfextract'
            except OSError, ose:
                cxlog.log_("aie", cxlog.to_str(ose))
        if archive_type is None:
            # Not an archive. We are done here.
            return True

        # Get a temporary directory to extract into
        wintmpdir = self.scheduler.expand_win_string("%temp%")
        tmpdir = bottlequery.get_native_path(self.scheduler.installtask.bottlename, wintmpdir)
        if not tmpdir:
            self.error = _("Unable to get a temporary directory for %s") % self.name
            return False
        tmpdir = tempfile.mkdtemp(dir=tmpdir)
        self.state['temporary_source'] = tmpdir

        # save the cwd so that we can restore it later on, then
        #  point at the tempdir in case any of our archive models
        #  extract directly to the cwd.
        previous_cwd = os.getcwd()
        os.chdir(tmpdir)


        # Zip file support
        if archive_type == 'zip':
            zipf = zipfile.ZipFile(installer_file, 'r', allowZip64=True)
            for source in zipf.namelist():
                if source.startswith('/') or '..' in source.split('/'):
                    # These would be created outside the given path
                    cxlog.warn("Skipping file %s in %s (bad filename)" % (cxlog.debug_str(source), cxlog.debug_str(installer_file)))
                    continue
                if self.installer.zip_encoding:
                    target = os.path.join(tmpdir, source.decode(self.installer.zip_encoding))
                else:
                    target = os.path.join(tmpdir, source)
                try:
                    os.makedirs(target.rsplit('/', 1)[0])
                except OSError:
                    # File already exists
                    pass
                if not target.endswith('\\') and not target.endswith('/'):
                    if self.installer.zip_encoding:
                        data = zipf.read(source)
                        dfile = open(target, 'wb')
                        dfile.write(data)
                        dfile.close()
                    else:
                        try:
                            # With extract(), introduced in Python 2.6, we don't
                            # have to read the whole archive member into memory
                            # which helps a lot for large files.
                            # pylint: disable=E1101
                            zipf.extract(source, tmpdir)
                        except AttributeError:
                            data = zipf.read(source)
                            dfile = open(target, 'wb')
                            dfile.write(data)
                            dfile.close()
            zipf.close()

        # Microsoft Cabinet file support
        if archive_type == 'cab':
            engine = self.scheduler
            # pylint: disable=W0632
            win_installer_file, win_extractdir = bottlequery.get_windows_paths(
                engine.installtask.bottlename,
                (installer_file, tmpdir))
            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename, "--no-convert",
                   "--wait-children", "--wl-app", "extract.exe", "--",
                   "/E", "/L", win_extractdir, "/R", win_installer_file]

            # FIXME: Make interruptible.
            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                os.chdir(previous_cwd)
                return False

        # Tar file support
        if archive_type == 'tar':
            cxlog.log_('tar', "Extracting archive %s to %s" % (cxlog.debug_str(installer_file), cxlog.debug_str(tmpdir)))
            tarf = tarfile.open(installer_file, 'r')
            members = tarf.getmembers()
            # Extract deeper files first to avoid the issues described at
            # http://www.python.org/doc/2.5.2/lib/tarfile-objects.html#l2h-2413
            members = sorted(members, key=tar_member_depth, reverse=True)
            for member in members:
                if member.name.startswith('/') or '..' in member.name.split('/'):
                    # These would be created outside the given path
                    cxlog.warn("Skipping file %s in %s (bad filename)" % (cxlog.debug_str(member.name), cxlog.debug_str(installer_file)))
                    continue
                # Make sure everything leading up to and including this file is a directory
                path_elements = os.path.normpath(member.name).split('/')
                bad_path = False
                for i in range(len(path_elements)):
                    path = os.path.join(tmpdir, '/'.join(path_elements[0:i+1]))
                    try:
                        mode = os.lstat(path).st_mode # use lstat because symlinks to directories are bad
                    except OSError:
                        # Path does not exist; this is fine
                        break
                    if not stat.S_ISDIR(mode):
                        bad_path = True
                        break
                if bad_path:
                    # This could be a symlink to something outside the extraction directory.
                    # Even if it's a regular file, we don't expect to overwrite any of those.
                    cxlog.warn("Skipping file %s in %s (not a directory)" % (cxlog.debug_str(member.name), cxlog.debug_str(installer_file)))
                    continue
                cxlog.log_('tar', "Extracting file %s to %s" % (cxlog.debug_str(member.name), cxlog.debug_str(tmpdir)))
                tarf.extract(member, tmpdir)
            tarf.close()

        # RAR file support
        if archive_type == 'rar':
            engine = self.scheduler
            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "unrar"),
                   "x", "-idq", installer_file, tmpdir]

            # FIXME: Make interruptible.
            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                os.chdir(previous_cwd)
                return False

        # 7z and NSIS (NullSoft Scriptable Install System) file support
        if archive_type == '7z':
            engine = self.scheduler
            cmd = ["7z", "x", "-o" + tmpdir, installer_file]

            # FIXME: Make interruptible.
            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                os.chdir(previous_cwd)
                return False

        # Self-extracting executable
        if archive_type == 'selfextract':
            engine = self.scheduler

            win_installer_file = bottlequery.get_windows_path(engine.installtask.bottlename, installer_file)

            # Set %ExtractDir% in the 'Windows' environment variable block so
            # it is available when building the self-extracting installer
            # command. This way it will know where to extract the files.
            # Note that we need to put a Windows path there. We cannot rely on
            # the wine script doing the conversion for us because the path is
            # likely to be embedded in an option like so '/T:%ExtractDir%'.
            # Also note that we modify a variable which may be used by the
            # other tasks. That's ok because no other task is going to use
            # 'winenv' while AIECore is running.
            if not wintmpdir.endswith('\\'):
                wintmpdir += '\\'
            winenv = engine.get_win_environ()
            # The key must be lower case
            winenv['extractdir'] = wintmpdir + os.path.basename(tmpdir)

            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--no-convert",
                   "--bottle", engine.installtask.bottlename,
                   "--wait-children", win_installer_file]
            for option in self.installer.selfextract_options:
                cmd.append(engine.expand_win_string(option))
            if self.state.get('silentinstall'):
                for option in self.installer.selfextract_silent_options:
                    cmd.append(engine.expand_win_string(option))
            del winenv['extractdir']

            # FIXME: Make interruptible.
            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                os.chdir(previous_cwd)
                return False

        os.chdir(previous_cwd)
        self.state['install_source'] = tmpdir
        del self.state['installer_file']
        return True


    def _main_locate_installer(self):
        if not cxaiemedia.needs_installer_file(self.installer):
            # Virtual and 'copy-only' profiles don't need an installer file
            # and should skip the corresponding steps.
            self.state['no_installer'] = True
            return True
        if 'installer_file' in self.state:
            # Nothing to do if we already have an installer file
            return True

        install_source = self.state['install_source']
        if os.path.isdir(install_source):
            installer_file = cxaiemedia.locate_installer(self.installer, install_source)
            if installer_file:
                self.state['installer_file'] = installer_file
                cxlog.log_("aie", "installer_file = " + cxlog.debug_str(installer_file))
                return True
        self.error = _("Unable to find an installer for %(name)s in '%(source)s'") % {'name':self.name, 'source':install_source}
        return False


    def _main_build_install_command(self):
        if 'no_installer' in self.state:
            return True
        if 'installer_command' in self.state:
            # Nothing to do if we already have a command
            return True

        installer_file = self.state['installer_file']
        cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
               "--bottle", self.scheduler.installtask.bottlename,
               "--untrusted", "--wait-children", '--no-convert', "--new-console"]
        if self.installer.installer_dlloverrides:
            cmd.extend(['--dll', self.installer.installer_dlloverrides])

        if self.installer.installer_treatas:
            lower = self.installer.installer_treatas.lower()
        else:
            lower = installer_file.lower()

        # pylint: disable=W0632
        win_installer_file, win_autorun_dir = bottlequery.get_windows_paths(
            self.scheduler.installtask.bottlename,
            (installer_file, os.path.dirname(installer_file)))

        if lower.endswith('/autorun.inf'):
            cmd.extend(['--wl-app', 'autorun.exe', '--', win_autorun_dir])

        elif lower.startswith('steam:'):
            cmd.extend(['--start', installer_file])

        elif lower.endswith('.exe') or lower.endswith('.com') or lower.endswith('.bat') or lower.endswith('.cmd'):
            cmd.append('--')
            cmd.append(win_installer_file)
            for option in self.installer.installer_options:
                cmd.append(self.scheduler.expand_win_string(option))
            if self.state.get('silentinstall'):
                for option in self.installer.installer_silent_options:
                    cmd.append(self.scheduler.expand_win_string(option))

        elif lower.endswith('.msi'):
            cmd.append('--')
            cmd.extend(['msiexec.exe', '/i', win_installer_file])
            if self.state.get('silentinstall'):
                cmd.append('/quiet')

        elif lower.endswith('.msp'):
            cmd.append('--')
            cmd.extend(['msiexec.exe', '/p', win_installer_file])

        elif lower.endswith('.otf') or \
             lower.endswith('.ttc') or \
             lower.endswith('.ttf'):
            cmd.remove('--new-console')
            cmd.append('--')
            cmd.extend(['cxinstallfonts.exe', win_installer_file])

        else:
            self.error = _("'%(file)s' is of an unknown installer type for %(name)s") % {'file':installer_file, 'name':self.name}
            return False

        self.state['installer_command'] = cmd
        cxlog.log_("aie", "installer_command=" + ' '.join(cxlog.debug_str(x) for x in cmd))
        return True


    def _main_collate_eassocs(self):
        engine = self.scheduler
        engine.state['defaulteassocs'].update(self.installer.default_eassocs)
        engine.state['alteassocs'].update(self.installer.alt_eassocs)
        return True


    def _main_collate_nsplugins(self):
        engine = self.scheduler
        engine.state['ignore_nsplugins'].update(self.installer.ignore_nsplugin_dlls)
        return True


    def install_failure_detected(self):
        profile = self.installer.parent
        if not profile.app_profile.installed_key_pattern and \
           not profile.app_profile.installed_display_pattern and \
           not profile.app_profile.installed_registry_globs and \
           not profile.app_profile.installed_file_globs:
            if not self._has_printed_install_log_err:
                cxlog.log_("aie", "We have no way to detect success or failure for this package. Assuming success, with optimism.")
                self._has_printed_install_log_err = True
            return False
        if self.state.get('installer_file', '').lower().startswith('steam:'):
            return False
        else:
            return not appdetector.is_profile_installed(self.scheduler.installtask.bottlename, profile)

    def _main_run_installer_and_check(self):
        if 'no_installer' in self.state:
            return True
        # We cannot trust the installer's return code so we ignore it entirely.
        # FIXME: We should try to be interruptible

        is_steam_install = self.state.get('installer_file', '').lower().startswith('steam:')

        engine = self.scheduler
        source = engine.installtask.GetInstallerSource()
        if source and os.path.isdir(source):
            workdir = source
        else:
            workdir = bottlequery.get_native_path(engine.installtask.bottlename, engine.expand_win_string("C:/windows/temp"))
        cxutils.system(self.state['installer_command'],
                       env=self.state['environ'], cwd=workdir,
                       background=is_steam_install)

        if self.install_failure_detected():
            self.error = _("The installer has exited but %s does not seem to be installed") % self.name
            return False
        else:
            return True

    def _check_fake_dll(self, path):
        if os.path.exists(path):
            sfile = open(path, "rb")
            sfile.seek(0x40)
            tag = sfile.read(20)
            if tag == "Wine placeholder DLL":
                return True
        return False

    def _main_remove_fake_dlls(self):
        engine = self.scheduler
        for dllfile in self.installer.pre_rm_fake_dlls:
            source = bottlequery.get_native_path(engine.installtask.bottlename, engine.expand_win_string(dllfile))
            if not os.path.exists(source):
                source = engine.expand_win_string("%WinSysDir%")
                target = source
                source = os.path.join(source, dllfile)
                source = bottlequery.get_native_path(engine.installtask.bottlename, source)
                target = os.path.join(target, dllfile + ".bak")
                target = bottlequery.get_native_path(engine.installtask.bottlename, target)
            else:
                target = source + ".bak"

            if self._check_fake_dll(source):
                if os.path.exists(target):
                    os.remove(target)
                shutil.move(source, target)
            else:
                cxlog.log(" %s is not a fake dll " % (cxlog.debug_str(dllfile)))
        return True


    def _main_copy_files(self):
        if 'install_source' not in self.state:
            # Nothing to do for virtual packages
            return True

        engine = self.scheduler
        matches = {}

        if self.installer.files_to_copy and self.installer.files_to_copy[0].glob == ':installer:':
            copyentry = self.state.get('installer_file')
            matches[self.installer.files_to_copy[0]] = [copyentry]
        else:
            globber = globtree.FileGlobTree()
            for copyentry in self.installer.files_to_copy:
                globber.add_glob(copyentry.glob, copyentry)

            install_source = self.state['install_source']
            for match, copyentry in globber.matches(install_source):
                if copyentry in matches:
                    matches[copyentry].append(match)
                else:
                    matches[copyentry] = [match]

        destinations = {}
        # Preserve the order of the copy entries so the later ones overwrite
        # the former ones
        for copyentry in self.installer.files_to_copy:
            if copyentry not in matches:
                continue
            sources = matches[copyentry]
            cxlog.log_("aie", "* %s -> %s" % (cxlog.debug_str(copyentry.glob), cxlog.debug_str(copyentry.destination)))

            # First figure out what the destination is, trying to minimise the
            # number of patch conversions
            if copyentry.destination in destinations:
                destination = destinations[copyentry.destination]
            else:
                destination = bottlequery.get_native_path(engine.installtask.bottlename, engine.expand_win_string(copyentry.destination))
                destinations[copyentry.destination] = destination

            # Note that if destination exists, it may have lost its trailing
            # '/'. So check the profile intent using copyentry.destination.
            # Also the copy operation should be idempotent. This is what
            # requires the isdir(sources[0]) check.
            if len(sources) == 1 and \
                    not copyentry.destination.endswith("/") and \
                    not os.path.isdir(destination) and \
                    not os.path.isdir(sources[0]):
                dst_dir = os.path.dirname(destination)
            else:
                dst_dir = destination
                destination = None

            # Then create the appropriate destination directory
            try:
                os.makedirs(dst_dir)
            except OSError, ose:
                if ose.errno != errno.EEXIST:
                    self.error = _("Unable to create the destination directory '%(dir)s' for '%(profile)s':\n%(error)s") % {
                        'dir': dst_dir,
                        'profile': self.name,
                        'error': unicode(ose)}
                    return False
            if not os.path.isdir(dst_dir):
                self.error = _("Cannot copy the %(profile)s file(s) into '%(dir)s' because it is not a directory.") % {
                    'profile': self.name,
                    'dir': dst_dir}
                return False

            # Finally, do the actual copying
            for source in sources:
                dst = destination
                if not dst:
                    dst = os.path.join(dst_dir, os.path.basename(source))
                try:
                    cxlog.log_("aie", "copying %s to '%s'" % (cxlog.debug_str(source), cxlog.debug_str(dst)))
                    if os.path.isfile(source):
                        shutil.copy2(source, dst)
                    else:
                        # FIXME: Should also work if the destination exists
                        shutil.copytree(source, dst)
                except OSError, ose:
                    self.error = _("An error occurred while copying '%(source)s' to '%(destination)s':\n%(error)s") % {
                        'source': source,
                        'destination': dst,
                        'error': unicode(ose)}
                    return False
        return True


    def _main_reboot(self):
        if not self.installer.post_install_reboot:
            return True

        engine = self.scheduler
        cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
               "--bottle", engine.installtask.bottlename,
               "--wait-children", "--wl-app", "reboot.exe"]
        # FIXME: Make interruptible.
        retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                         stderr=cxutils.GRAB)
        if retcode:
            self.error = err
            return False
        return True


    def _main_create_lnk_files(self):
        engine = self.scheduler
        for lnk in self.installer.lnk_files:
            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "--wait-children", "--wl-app", "cxmklnk.exe", "--",
                   "--lnkfile", engine.expand_win_string(lnk.shortcut) + ".lnk",
                   "--target", engine.expand_win_string(lnk.target)]
            if lnk.workdir:
                cmd.extend(("--workdir", engine.expand_win_string(lnk.workdir)))

            # FIXME: Make interruptible.
            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                return False
        return True


    def _main_apply_menu_hints(self):

        if not self.installer.skip_menu_creation:

            engine = self.scheduler

            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "cxmenu"),
                   "--bottle", engine.installtask.bottlename,
                   "--sync", "--mode", "install"]

            if self.installer.mainmenu_never:
                cmd.append("--mmenu-never")
                cmd.append(':'.join(self.installer.mainmenu_never))

            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                return False

        return True


    def _main_register_dlls(self):
        engine = self.scheduler
        for dll in self.installer.post_registerdll:
            expanded_dll = engine.expand_win_string(dll)
            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "--wait-children", "--no-convert", "--wl-app", "regsvr32.exe",
                   "--", "/s", expanded_dll]

            workdir = None
            if ('/' in expanded_dll) or ('\\' in expanded_dll):
                path = bottlequery.get_native_path(engine.installtask.bottlename, expanded_dll)
                basedir = os.path.dirname(path)
                if os.path.isdir(basedir):
                    workdir = basedir

            # FIXME: Make interruptible.
            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB, cwd=workdir)

            if retcode:
                self.error = err
                return False
        return True


    def _cleanup_delete_uncompressed_files(self):
        if 'temporary_source' in self.state:
            shutil.rmtree(self.state['temporary_source'])
            del self.state['temporary_source']
        return True

    def _main_check_silent_steam_install(self):
        # If Steam is being installed as a dependency, make
        #  the install silent. That allows the installer to return
        #  immediately so we can get on with our business.
        if self.installer.parent.appid == 'com.codeweavers.c4.206':
            for parent in self.parents:
                if type(parent).__name__ == 'AIECore':
                    self.state['silentinstall'] = True
                    break
        return True

    # The routines implementing the 'All Fonts' hack

    def _main_check_silent_font_install(self):
        cxlog.log_('aie', 'appid=%s silentfontinstall=%s global=%s' % (cxlog.to_str(self.installer.parent.appid), cxlog.to_str(self.state.get('silentfontinstall')), cxlog.to_str(self.scheduler.state.get('silentfontinstall'))))
        if self.state.get('silentfontinstall') and self.scheduler.state.get('silentfontinstall'):
            self.state['silentinstall'] = True
            cxlog.log_('aie', ' -> silentinstall = True')
        return True


    def _main_set_silent_font_install(self):
        cxlog.log_('aie', 'appid=%s' % cxlog.to_str(self.installer.parent.appid))
        if self.state.get('silentfontinstall'):
            cxlog.log_('aie', ' -> global silentfontinstall = True')
            self.scheduler.state['silentfontinstall'] = True
        return True


    def _main_cxhack_steam(self):
        # custom hacks for com.codeweavers.c4.206 aka Steam
        # occurs after install finishes on non-macs
        if not distversion.IS_MACOSX:
            if self.installer.parent.appid == 'com.codeweavers.c4.206':
                engine = self.scheduler
                target = os.path.join(engine.expand_win_string("%ProgramFiles%"), "Steam")
                target = bottlequery.get_native_path(engine.installtask.bottlename, target)
                if target:
                    cmd = ["chattr", "-R", "-D", target]
                    cxutils.run(cmd, env=engine.state['environ'], stderr=cxutils.GRAB)
        return True

    def _main_cxhack_dcom98_pre(self):
        if self.installer.parent.appid == 'com.codeweavers.c4.6936':
            # We need to move stdole32.tbl out of the way.
            engine = self.scheduler
            source = os.path.join(engine.expand_win_string("%WinDir%"), "system32", "stdole32.tlb")
            target = os.path.join(engine.expand_win_string("%WinDir%"), "system32", "stdole32.tlb.bak")
            source = bottlequery.get_native_path(engine.installtask.bottlename, source)
            target = bottlequery.get_native_path(engine.installtask.bottlename, target)

            if os.path.exists(source):
                if os.path.exists(target):
                    os.remove(target)
                shutil.move(source, target)
        return True

    def _main_cxhack_ie6_pre(self):
        # custom hacks for com.codeweavers.c4.15 aka IE6
        # occurs before install
        # also applies to Office XP (1544), which will install IE5 in win98 mode
        if self.installer.parent.appid == 'com.codeweavers.c4.15' or \
           self.installer.parent.appid == 'com.codeweavers.c4.7853' or \
           self.installer.parent.appid == 'com.codeweavers.c4.8054' or \
           (self.installer.parent.appid == 'com.codeweavers.c4.1544' and
            self.scheduler.installtask.target_template == 'win98'):
            engine = self.scheduler
            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "--wait-children", "--dll", "advpack=b",
                   "--wl-app", "iexplore.exe", "-unregserver"]
            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                return False
            # move the builtin iexplore.exe out of the way
            source = os.path.join(engine.expand_win_string("%ProgramFiles%"), "Internet Explorer")
            target = source
            source = os.path.join(source, "iexplore.exe")
            source = bottlequery.get_native_path(engine.installtask.bottlename, source)
            target = os.path.join(target, "iexplore.exe.bak")
            target = bottlequery.get_native_path(engine.installtask.bottlename, target)
            if os.path.exists(source):
                if os.path.exists(target):
                    os.remove(target)
                shutil.move(source, target)

            # If we aren't on win98, we're going to need to extract
            # inseng.dll. On win98 this change isn't needed.
            # The extraction doesn't work with some older versions of IE,
            # but fortunately those versions are all installed on win98 anyway.
            if self.scheduler.installtask.target_template != 'win98':
                installer_file = self.state['installer_file']
                wintmpdir = self.scheduler.expand_win_string("%temp%")
                cwintmpdir = bottlequery.get_native_path(engine.installtask.bottlename, wintmpdir)
                tmpdir = tempfile.mkdtemp(dir=cwintmpdir)
                wintmpdir = bottlequery.get_windows_path(engine.installtask.bottlename, tmpdir)
                win_installer_file = bottlequery.get_windows_path(engine.installtask.bottlename, installer_file)
                cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                       "--bottle", engine.installtask.bottlename, "--no-convert",
                       "--wait-children", "--", win_installer_file, "/T:"+wintmpdir,
                       "/C"]
                retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                                 stderr=cxutils.GRAB)
                if retcode:
                    cxlog.err('Failed to extract dlls from the IE6 installed.\n')
                    self.error = err
                    return False

                source = os.path.join(tmpdir, "inseng.dll")
                target = os.path.join(engine.expand_win_string("%WinDir%"), "system32", "inseng.dll")
                target = bottlequery.get_native_path(engine.installtask.bottlename, target)
                if os.path.exists(source):
                    if os.path.exists(target):
                        os.remove(target)
                    shutil.move(source, target)
                    shutil.rmtree(tmpdir)


        return True

    def _main_cxhack_ie6_post(self):
        # custom hacks for com.codeweavers.c4.15 aka IE6
        # occurs after install
        # also applies to Office XP (1544), which will install IE5 in win98 mode
        if self.installer.parent.appid == 'com.codeweavers.c4.15' or \
           self.installer.parent.appid == 'com.codeweavers.c4.8054' or \
           (self.installer.parent.appid == 'com.codeweavers.c4.1544' and
            self.scheduler.installtask.target_template == 'win98'):
            engine = self.scheduler
            # if iexplore.exe does not exist restore our backup
            source = os.path.join(engine.expand_win_string("%ProgramFiles%"), "Internet Explorer")
            target = source
            target = os.path.join(source, "iexplore.exe")
            target = bottlequery.get_native_path(engine.installtask.bottlename, target)
            if not globtree.file_exists_insensitive(target):
                source = os.path.join(source, "iexplore.exe.bak")
                source = bottlequery.get_native_path(engine.installtask.bottlename, source)
                if os.path.exists(source):
                    shutil.move(source, target)

        return True

    def _main_cxhack_mplayer2(self):
        # custom hacks for com.codeweavers.c4.96 aka Windows Media Player 6.4
        # occurs before install
        if self.installer.parent.appid == 'com.codeweavers.c4.96':
            engine = self.scheduler
            # remove wininit.ini
            target = os.path.join(engine.expand_win_string("%WinDir%"), "wininit.ini")
            target = bottlequery.get_native_path(engine.installtask.bottlename, target)
            if os.path.exists(target):
                os.remove(target)
        return True

    def _main_cxhack_cxhtml(self):
        # custom hacks for com.codeweavers.c4.cxhtml
        # occurs before install
        if self.installer.parent.appid == 'com.codeweavers.c4.cxhtml' or \
           self.installer.parent.appid == 'com.codeweavers.c4.7828':
            engine = self.scheduler

            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "--wait-children", "--wl-app", "iexplore.exe", "-regserver"]
            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                return False
        return True

    def _main_cxhack_dotnet(self):
        # dotnet needs to whip out the wine version before it installs
        # custom hacks for com.codeweavers.c4.597
        # occurs before install
        if self.installer.parent.appid == 'com.codeweavers.c4.597' or \
           self.installer.parent.appid == 'com.codeweavers.c4.1578' or \
           self.installer.parent.appid == 'com.codeweavers.c4.5917':
            engine = self.scheduler
            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "uninstaller", "--remove",
                   "{E45D8920-A758-4088-B6C6-31DBB276992E}"]
            _retcode, _out, _err = cxutils.run(cmd, env=engine.state['environ'],
                                               stderr=cxutils.GRAB)
        return True

    def _main_cxhack_dotnet30(self):
        # dotnet 3.0  and 3.0 sp 1 needs to remove some service keys
        # custom hacks for com.codeweavers.c4.9383 and 3356
        # occurs before install
        if self.installer.parent.appid == 'com.codeweavers.c4.9383' or \
           self.installer.parent.appid == 'com.codeweavers.c4.3356':
            engine = self.scheduler
            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "--wait-children", "--wl-app", "sc.exe",
                   "delete", "FontCache3.0.0.0"]
            _retcode, _out, _err = cxutils.run(cmd, env=engine.state['environ'],
                                               stderr=cxutils.GRAB)
        return True

    def _main_cxhack_dotnet20sp(self):
        # dotnet 2.0 sps needs to make sure a service is shut down
        # custom hacks for com.codeweavers.c4.9322 and 6945
        # occurs before install
        if self.installer.parent.appid == 'com.codeweavers.c4.9322' or \
           self.installer.parent.appid == 'com.codeweavers.c4.6945':
            engine = self.scheduler

            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "--wl-app", "sc", "stop",
                   "clr_optimization_v2.0.50727_32"]

            cxutils.run(cmd, env=engine.state['environ'])

            # We do not want to report failure here as if the service
            # does not exist that is just fine also.
        return True

    def _cleanup_cxhack_dotnet20sp(self):
        # dotnet 2.0 sps needs to make sure everything is shut down at the end
        # custom hacks for com.codeweavers.c4.9322 and 6945
        # occurs before install
        if self.installer.parent.appid == 'com.codeweavers.c4.9322' or \
           self.installer.parent.appid == 'com.codeweavers.c4.6945':
            engine = self.scheduler

            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "--wl-app", "wineboot", "--", "--end-session",
                   "--shutdown", "--force", "--kill"]

            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                return False

            bottlequery.shutdown_manipulator(engine.installtask.bottlename)
        return True

    def _cleanup_cxhack_taskkill(self):
        # custom hacks for com.codeweavers.c4.8882 aka TaskKill
        # occurs after install
        if self.installer.parent.appid == 'com.codeweavers.c4.8882':
            engine = self.scheduler

            cmd = [os.path.join(cxutils.CX_ROOT, "bin", "wine"),
                   "--bottle", engine.installtask.bottlename,
                   "--wl-app", "wineboot", "--", "--end-session",
                   "--shutdown", "--force", "--kill"]

            retcode, _out, err = cxutils.run(cmd, env=engine.state['environ'],
                                             stderr=cxutils.GRAB)
            if retcode:
                self.error = err
                return False

            bottlequery.shutdown_manipulator(engine.installtask.bottlename)
        return True

    def _cleanup_delete_downloaded_installer(self):
        # Deletes the downloaded installer file if <alwaysredownload> is true
        temp_install_source = self.state.get('temp_install_source')
        if temp_install_source:
            os.remove(temp_install_source)
        return True

    def _cleanup(self):
        result = True
        routines = ('reset_winver',
                    'delete_drive_letter',
                    'delete_uncompressed_files',
                    'cxhack_dotnet20sp',
                    'cxhack_taskkill',
                    'delete_downloaded_installer',
                   )
        self.cleanup_lock.acquire() # If we run cleanup functions concurrently, they could return too early
        try:
            for routine in routines:
                # Don't use run_routines for this because we always want to run all of them.
                try:
                    if not getattr(self, '_cleanup_%s' % routine)():
                        result = False
                except:
                    cxlog.warn("Cleanup function failed:\n%s" % traceback.format_exc())
                    result = False
        finally:
            self.cleanup_lock.release()
        return result

    def can_cancel(self):
        return True

    def cancel(self):
        cxaiebase.AIETask.cancel(self)
        # We can't interrupt an installer in progress, but we can stop the process.
        self.canceled = True
        self._cleanup()

    def main(self):
        routines = ('setup_drive_letter',
                    'set_installer_file',
                    'check_silent_font_install',
                    'check_silent_steam_install',
                    'setup_environment',
                    'cxhack_dotnet',
                    'setup_winver',
                    'setup_registry_pre',
                    'uncompress',
                    'locate_installer',
                    'build_install_command',
                    'collate_eassocs',
                    'collate_nsplugins',
                    'cxhack_ie6_pre',
                    'cxhack_dcom98_pre',
                    'cxhack_mplayer2',
                    'cxhack_cxhtml',
                    'cxhack_dotnet30',
                    'remove_fake_dlls',
                    'cxhack_dotnet20sp',
                    'run_installer_and_check',
                    'copy_files',
                    'reboot',
                    'create_lnk_files',
                    'apply_menu_hints',
                    'register_dlls',
                    'set_silent_font_install',
                    'cxhack_steam',
                    'cxhack_ie6_post',
                   )

        try:
            main_success = self.run_routines('main', routines)
        finally:
            cleanup_success = self._cleanup()
        return main_success and cleanup_success