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

beebox / crossover   deb

Repository URL to install this package:

Version: 18.5.0-1 

/ opt / cxoffice / lib / python / cxaiemedia.py

# (c) Copyright 2009-2013, 2015-2018. CodeWeavers, Inc.

import cookielib
import os
import os.path
import stat
import tempfile
import urllib2
import urlparse

import cxproduct
import cxlog
import cxurlget
import cxutils
import dircache
import globtree

import cddetector
import cxaiebase
import cxhtmlutils
import shutil


# for localization
from cxutils import cxgettext as _

#####
#
# Insert / Release Media
#
#####

def needs_installer_file(iprofile):
    """Returns True if the specified installer profile needs an installer
    file. False means the profile is either virtual, or just copies files
    around.
    """
    if 'virtual' in iprofile.parent.app_profile.flags:
        return False
    if iprofile.files_to_copy and \
       len(iprofile.installer_file_globs) >= 1 and \
       iprofile.installer_file_globs[0] == ':stop:':
        # This profile only copies files so there is no need for an installer
        # file.
        return False
    return True


def find_file(path, globs):
    """Looks for a file matching the one of the globs in the list."""
    globber = globtree.FileGlobTree()
    for i in range(len(globs)):
        globber.add_glob(globs[i], i)

    best_matches = []
    best_rank = len(globs)
    for match, rank in globber.matches(path):
        if rank < best_rank:
            best_matches = [match]
            best_rank = rank
        elif rank == best_rank:
            best_matches.append(match)
    return best_matches, best_rank


def locate_installer(iprofile, install_source):
    """Tries to find an installer file for the specified profile."""
    stop = False
    globs = []
    for glob in iprofile.installer_file_globs:
        if glob == ':stop:':
            stop = True
            break
        globs.append(glob)
    if not stop:
        globs.extend(("autorun.inf",
                      "autorun.exe",
                      "setup.exe",
                      "install.exe",
                      "setup*.exe",
                      "install*.exe",
                      "setup.bat",
                      "install.bat",
                      "setup*.bat",
                      "install*.bat",
                      "*.exe"))

    matches, index = find_file(install_source, globs)
    if not matches:
        cxlog.log("no match for " + cxlog.debug_str(globs))
        return None

    # Reject ties
    if len(matches) > 1:
        cxlog.log("got a tie for %s: %s" % (globs[index], cxlog.debug_str(matches)))
        return None
    return matches[0]


NO_MEDIA = 1
WRONG_CD = 2
NO_INSTALLER = 3

class AIEInsertMedia(cxaiebase.AIETask):
    """Asks the user to insert or specify the path to the installation media
    needed for the associated AIECore (either a CD or a file installer).

    This also marks the media as being in use until it is released by the
    corresponding AIEReleaseMedia task.
    """

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

    def can_run(self):
        """Returns True if this task is runnable and another media is not
        already in use.
        """
        if 'AIEMedia' in self.scheduler.userdata:
            return False
        return cxaiebase.AIETask.can_run(self)


    def _getniceness(self):
        """Run whatever installers are available before asking for new media.
        """
        return self.aiecore.niceness + 1000

    niceness = property(_getniceness)


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


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

    def __init__(self, aiecore):
        cxaiebase.AIETask.__init__(self, aiecore.scheduler, "")
        self.aiecore = aiecore
        aiecore.add_dependency(self)
        self.errcode = 0

        # install_source should be set by the GUI before main() is run
        self.install_source = None

        # If True, then AIEInsertMedia should check that the media matches
        # the CD profile. Otherwise it blindly assumes that it's the right CD.
        self.detect = True


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


    def main(self):
        # FIXME: We should have access to the list of potential CDs. Then we
        # would be able to autodetect the source media.
        if not self.install_source:
            self.errcode = NO_MEDIA
            self.error = _("The source media is not set.")
            return False

        if self.install_source.startswith("steam:"):
            self.aiecore.state['install_source'] = self.install_source
            return True

        if os.path.isfile(self.install_source):
            self.aiecore.state['install_source'] = self.install_source
            return True

        if not os.path.isdir(self.install_source):
            self.errcode = NO_MEDIA
            self.error = _("'%s' should either be a file or a directory") % self.install_source
            return False

        profile = self.aiecore.installer.parent
        if self.detect and profile.cd_profile and not cddetector.find_profile(self.aiecore.installer.parent, (profile,)):
            self.errcode = WRONG_CD
            self.error = _("The inserted CD does not match %s") % profile.name
            return False

        if self.aiecore.installer.udf_remount_check != "":
            if not os.path.exists(os.path.join(self.install_source, self.aiecore.installer.udf_remount_check)):
                source_device = None
                if self.install_source in self.scheduler.installtask.source_medias:
                    source_device = self.scheduler.installtask.source_medias[self.install_source].device
                if not source_device:
                    source_device = '/dev/cdrom'
                self.error = _("This disk contains two sections, one for Mac and one for Windows. "
                               "In order for CrossOver to use this disk it must be mounted so that "
                               "the Windows portion is visible.\n\n"
                               "To make this change, remount as root with this command:\n"
                               "mount -t udf -o users,ro,unhide,uid=%(uid)d,remount %(device)s "
                               "\"%(mountpoint)s\"") % {'uid': os.getuid(),
                                                        'device': source_device,
                                                        'mountpoint': self.install_source}
                return False

        if not needs_installer_file(self.aiecore.installer):
            return True

        # Make sure we can find an installer now so that we ask the user to
        # pick some other install source if we cannot.
        installer_file = locate_installer(self.aiecore.installer, self.install_source)
        if installer_file:
            self.aiecore.state['install_source'] = self.install_source
            self.aiecore.state['installer_file'] = installer_file
            return True

        # Trigger an error and force a new lookup
        self.errcode = NO_INSTALLER
        self.error = _("Unable to find an installer in '%s'") % self.install_source
        return False


class AIEReleaseMedia(cxaiebase.AIENop):
    """Releases the media that was inserted by the AIEInsertMedia task.

    This signals other AIEInsertMedia tasks that they can run.
    """

    def __init__(self, aiecore):
        cxaiebase.AIENop.__init__(self, aiecore.scheduler, "Release " + aiecore.name)
        self.aiecore = aiecore
        self.add_dependency(aiecore)


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


    def done(self):
        try:
            insert_media = self.scheduler.userdata['AIEMedia']
            if self.aiecore.insert_media != insert_media:
                cxlog.err("mismatched Insert/Release (%s / %s)" % (cxlog.to_str(insert_media), cxlog.to_str(self)))
            del self.scheduler.userdata['AIEMedia']
        except KeyError:
            cxlog.err(cxlog.to_str(self) + ": AIEMedia should have been set")
        cxaiebase.AIETask.done(self)



#####
#
# Download tasks
#
#####

def get_builtin_installer(appid):
    """If the application has a builtin installer, returns the path to that
    installer. Otherwise, returns None.
    """
    dirpath = os.path.join(cxutils.CX_ROOT, "lib", "installers")
    appid += "."
    cxlog.log_("aie", "looking for " + cxlog.to_str(appid))
    if os.path.exists(dirpath):
        for dentry in dircache.listdir(dirpath):
            if dentry.startswith(appid):
                path = os.path.join(dirpath, dentry)
                cxlog.log_("aie", "  -> " + cxlog.to_str(path))
                return path
    return None


def get_cached_filename(url, filename):
    """Use the hashed URL so we can cache each localized installer separately,
    and so we automatically update the installer if the URL changed.
    """
    md5hasher = cxutils.md5_hasher()
    # The anchor is irrelevant to the server so strip it
    md5hasher.update(url.split('#', 1)[0])
    return md5hasher.hexdigest() + "." + filename

def validate_installer(filename):
    """Checks whether the specified file is an html file, and if it is,
    whether it redirects us elsewhere.
    """
    return cxhtmlutils.is_html(filename, 4096)

def get_cached_installer(url):
    """Checks whether we have an installer for the specified url in our
    downloaded installers cache.
    """
    basename = get_cached_filename(url, "")
    cxlog.log_("aie", "looking for " + cxlog.to_str(basename))
    for dirpath in (cxproduct.get_user_dir(), cxproduct.get_managed_dir()):
        try:
            for dentry in dircache.listdir(os.path.join(dirpath, "installers")):
                if dentry.startswith(basename):
                    path = os.path.join(dirpath, "installers", dentry)
                    if not os.path.isfile(path):
                        continue
                    cxlog.log_("aie", "  -> " + cxlog.to_str(path))
                    is_html, _redirect = validate_installer(path)
                    if not is_html:
                        return path
                    # A bad file got cached somehow (maybe by an old
                    # version)
                    cxlog.log_("aie", "  -> deleted (html file)")
                    os.unlink(path)
        except OSError:
            # The directory does not exist or is not readable.
            # Just skip to the next
            pass
    return None


def _is_cache_filename(name):
    if name.startswith('local.tmp.'):
        return True

    if len(name) > 33 and name[32] == '.' and \
        all(ch in '0123456789abcdef' for ch in name[0:32]):
        return True

    return False


def size_cached_installers():
    path = os.path.join(cxproduct.get_user_dir(), "installers")
    if not os.path.exists(path):
        return 0

    result = 0

    for name in os.listdir(path):
        if not _is_cache_filename(name):
            continue

        filename = os.path.join(path, name)

        st = os.lstat(filename)

        if not stat.S_ISREG(st.st_mode):
            continue

        result += st.st_size

    return result


def delete_cached_installers():
    path = os.path.join(cxproduct.get_user_dir(), "installers")
    if not os.path.exists(path):
        return

    for name in os.listdir(path):
        if not _is_cache_filename(name):
            continue

        filename = os.path.join(path, name)

        st = os.lstat(filename)

        if not stat.S_ISREG(st.st_mode):
            continue

        os.unlink(filename)


class AIEDownload(cxaiebase.AIETask):

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

    def __init__(self, aiecore):
        """Initializes the download task for the specified AIECore object."""
        cxaiebase.AIETask.__init__(self, aiecore.scheduler,
                                   _("Downloading %s") % aiecore.name)
        self.aiecore = aiecore
        aiecore.add_dependency(self)
        self._getter = None
        self.canceled = False

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

    def can_cancel(self):
        return True

    def cancel(self):
        cxaiebase.AIETask.cancel(self)
        self.canceled = True

    def _getniceness(self):
        """Start downloads as soon as possible."""
        return self.aiecore.niceness - 1000

    niceness = property(_getniceness)


    #####
    #
    # Caching helpers
    #
    #####

    def _geturl(self):
        if (self.aiecore.installer.parent.app_profile.download_urls_64bit and
            self.scheduler.installtask.is64bit()):
            download_urls = self.aiecore.installer.parent.app_profile.download_urls_64bit
        else:
            download_urls = self.aiecore.installer.parent.app_profile.download_urls
        _lang, url = cxutils.get_language_value(
            download_urls,
            self.scheduler.installtask.realInstallerLocale)
        return url

    url = property(_geturl)


    def _needs_download(self):
        """Returns True if the installer needs to be downloaded, and False
        otherwise.
        """
        try:
            installer_file = self.aiecore.state['install_source']
            if os.path.exists(installer_file):
                # Presumably the installer was found on a CD so we don't need
                # to download it.
                return False
            del self.aiecore.state['install_source']
        except KeyError:
            pass

        installer_file = get_builtin_installer(self.aiecore.installer.parent.appid)
        if not installer_file and not self.aiecore.installer.always_redownload:
            installer_file = get_cached_installer(self.url)
        if installer_file:
            self.aiecore.state['install_source'] = installer_file
            return False

        return True

    needs_download = property(_needs_download)


    def set_installer_file(self, installer_file):
        """Sets the installer file to use."""
        cxlog.log_("aie", "%s: install_source = %s" % (cxlog.to_str(self), cxlog.debug_str(installer_file)))
        self.aiecore.state['install_source'] = installer_file
        if self.aiecore.installer.always_redownload:
            self.aiecore.state['temp_install_source'] = installer_file

    setInstallerFile_ = set_installer_file

    @staticmethod
    def get_installers_dir():
        dirpath = os.path.join(cxproduct.get_user_dir(), "installers")
        cxutils.mkdirs(dirpath)
        return dirpath

    installers_dir = property(get_installers_dir)

    #####
    #
    # Functions handling the actual download
    #
    #####

    def _getsize(self):
        """This is -1 if the download has not started yet.

        This is 0 if the download has started but the size is unknown.
        Otherwise it is the size in bytes.
        """
        if self._getter:
            return self._getter.bytes_total
        return -1

    size = property(_getsize)


    def _getdownloaded(self):
        """This is the number of bytes that have been downloaded."""
        if self._getter:
            return self._getter.bytes_downloaded
        return 0

    downloaded = property(_getdownloaded)


    def _getprogress(self):
        """This is a number between 0 and 1 representing the percentage that
        has been downloaded. If the download size is 0, then progress is -1.
        """
        if self._getter and self._getter.bytes_total:
            return 1.0 * self._getter.bytes_downloaded / self._getter.bytes_total
        return 0

    progress = property(_getprogress)


    def urlgetter_failed(self, urlgetter, exception):
        if isinstance(exception, urllib2.HTTPError):
            if exception.code == 403 and urlgetter.user_agent is None:
                # In fact we expected an installer but got an HTML file
                # so we tried again with Python's default user-agent
                # (look for user_agent below) but this backfired.
                self.error = _("'%s' returned an HTML file instead of the installer") % cxlog.to_str(urlgetter.url)
            elif exception.code == 404:
                self.error = _("HTTP error 404: The file '%s' could not be found.") % cxlog.to_str(urlgetter.url)
            else:
                self.error = _("The HTTP server returned failure code %s.") % exception.code
        else:
            self.error = cxlog.debug_str(exception)

    def urlgetter_progress(self, _urlgetter):
        if self.canceled:
            raise cxurlget.StopDownload()

    def main(self):
        if not self.needs_download or self.canceled:
            return True

        # Some sites return a cookie and redirect us to another URL which
        # expects to receive that cookie. So create a temporary cookie jar.
        cookiejar = cookielib.CookieJar()

        installers_dir = self.get_installers_dir()
        url = self.url
        user_agent = cxurlget.USER_AGENT
        for _count in range(5):
            tmpfileno, tmppath = tempfile.mkstemp(prefix='local.tmp.', dir=installers_dir)
            # fdopen() acquires the fd as its own
            tmpfile = os.fdopen(tmpfileno, "w")
            opener = urllib2.build_opener(urllib2.HTTPCookieProcessor(cookiejar))
            self._getter = cxurlget.UrlGetter(url, tmpfile, user_agent=user_agent, notify_progress=self.urlgetter_progress, notify_failed=self.urlgetter_failed, opener=opener)
            self._getter.fetch() # closes tmpfile for us
            if self.canceled or self.error:
                os.unlink(tmppath)
                return False

            is_html, redirect = validate_installer(tmppath)
            if not is_html:
                break

            # We may have gotten an HTML file instead of the installer:
            # - We may be behind a WiFi hotspot that redirects all URLs to its
            #   login page.
            # - Or the installer may be behind a web page with a <meta> refresh
            #   tag.
            if redirect is None or redirect is False:
                # Some sites (microsoft) return an HTTP 403 error when the
                # user-agent does not look like a real browser, probably to
                # ward off spiders.
                # But when given a browser-like user-agent other sites put
                # up a tracking-cookie notification page (sourceforge)
                # which requires clicking on a button (which we cannot do)
                # to get at the installer.
                # So when trying to get an installer, try first with the
                # browser-like user-agent and, if that fails, try again
                # with the default user-agent.
                if user_agent is not None:
                    # Switch to the default user-agent and try again
                    user_agent = None
                    continue
                break

            if redirect is True:
                # Just reload the url, hoping not to get a redirect again
                continue

            # Follow the web page's http-equiv redirect to make sure the
            # final destination is valid
            url = urlparse.urljoin(self._getter.url, redirect)

        if is_html:
            os.unlink(tmppath)
            self.error = _("Got an HTML file instead of an installer for %s") % self.url
            return False

        installer_file = os.path.join(installers_dir, get_cached_filename(self.url, self._getter.basename))
        try:
            shutil.move(tmppath, installer_file)
            os.chmod(installer_file, 0777 & ~cxutils.UMASK)
        except OSError, ose:
            self.error = ose.strerror
            return False

        self.set_installer_file(installer_file)
        return True

    def getSuggestedFilename_(self, string):
        # This is a convenience method used by the Mac code.
        basename = cxurlget.url_to_sanitized_basename(string)
        return get_cached_filename(self.url, basename)


#####
#
# Scan Media
#
#####

class AIEScanMedia(cxaiebase.AIETask):
    """Scans the media looking for extra installers for dependencies.

    This helps us avoid downloads.
    """

    def __init__(self, aiecore):
        cxaiebase.AIETask.__init__(self, aiecore.scheduler, "")
        self.aiecore = aiecore


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


    def main(self):
        install_source = self.aiecore.insert_media.install_source
        if not os.path.isdir(install_source):
            # Not something we can scan
            return True

        best_rank = {}
        globber = globtree.FileGlobTree()
        for download in self.parents:
            if not isinstance(download, AIEDownload):
                continue
            if not download.needs_download:
                continue
            installer = download.aiecore.installer
            size = len(installer.local_installer_file_globs)
            best_rank[download] = size
            for i in range(size):
                globber.add_glob(installer.local_installer_file_globs[i],
                                 (download, i))

        best_matches = {}
        for match, data in globber.matches(install_source):
            download, rank = data
            if rank < best_rank[download]:
                best_matches[download] = [match]
            elif rank == best_rank[download]:
                best_matches[download].append(match)

        for download, matches in best_matches.iteritems():
            # Reject ties
            if len(matches) > 1:
                cxlog.log("got a tie for %s: %s" % (cxlog.to_str(download.aiecore.installer.local_installer_file_globs[best_rank[download]]), cxlog.debug_str(matches)))
                continue

            # Make sure the media will be present when the installer is run
            download.aiecore.add_dependency(self.aiecore.insert_media)
            self.aiecore.release_media.add_dependency(download.aiecore)
            download.aiecore.state['install_source'] = matches[0]
            download.aiecore.state['silentinstall'] = True
        return True


    def done(self):
        to_delete = []
        self.scheduler.lock()
        try:
            for download in self.parents:
                if not isinstance(download, AIEDownload):
                    continue
                if self.aiecore in download.aiecore.parent_media and self.aiecore.insert_media.status == cxaiebase.TASK_DONE:
                    # We must remove the download's dependencies on all
                    # AIEScanMedia objects to avoid so it does not prevent its
                    # AIECore from installing, thus causing a deadlock.
                    # Perform the deletion in two steps so we don't change the
                    # self.parents set while iterating it.
                    cxlog.log_("aie", "  clearing the %s dependencies" % cxlog.to_str(download))
                    to_delete.append(download)
        finally:
            self.scheduler.unlock()

        for download in to_delete:
            download.clear_dependencies()
        cxaiebase.AIETask.done(self)