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 / c4profilesmanager.py

# (c) Copyright 2009, 2012-2014. CodeWeavers, Inc.

"""Handles refreshing and reading the C4 profile information."""

import os
import threading
import time
import UserDict

import cxobjc
import cxlog
import cxutils
import cxproduct
import c4profiles
import fileupdate

import c4parser
import shutil


UNKNOWN = "com.codeweavers.unknown"
# This is the application id of the unknown application profile


class _C4PSource(object):
    """This is a helper class for C4ProfilesSet.

    It keeps track of a couple of extra data about a C4Pfile object: its old
    list of profiles and its last modification time.

    The 'trusted' flag marks profiles that are official
    CodeWeavers profiles. Untrusted profiles will produce a
    user warning upon selection. Ideally this will be handled via
    signing but at the moment it is based on where the files came from.

    The 'source' member marks profiles which were added at runtime via
    associations or drag and drop. It's used for ranking when
    there are profile collisions. Possible values are
    builtin, download, dropin
    """

    def __init__(self, c4pfile, trusted=True, source=c4profiles.builtin):
        self.c4pfile = c4pfile
        self.profiles = {}
        self.mtime = 0
        self.trusted = trusted
        self.source = source


class C4Profiles(cxobjc.Proxy):
    pass

@cxobjc.method(C4Profiles, 'profilesSet')
def profilesSet():
    return C4ProfilesSet.all_profiles()

@cxobjc.method(C4Profiles, 'dropinC4PFile_withSig_')
def dropin_C4PFile(c4pfilename, sigfilepath):
    if sigfilepath:
        sigfilepath = sigfilepath.encode('utf8')
    return C4ProfilesSet.add_c4p_file(c4pfilename.encode('utf8'), sigfilepath, c4profiles.dropin)

@cxobjc.method(C4Profiles, 'tasteC4PFile_')
def taste_c4p(sourcetie):
    (trusted, tmptie, tmpsig) = C4ProfilesSet.taste_new_c4p_file(sourcetie)
    if trusted:
        os.remove(tmptie)
        os.remove(tmpsig)
    return trusted

@cxobjc.method(C4Profiles, 'prepareC4PFile_')
def prepare_c4p(c4pfilename):
    (tmpfile, tmpsig) = fileupdate.prepare(c4pfilename)
    if tmpfile is None:
        return (None, None)
    return fileupdate.install(c4pfilename, tmpfile, tmpsig)

@cxobjc.method(C4Profiles, 'prepareSingleUseC4PFile_')
def prepare_temp_c4p(c4pfilename):
    return fileupdate.prepare(c4pfilename)

class C4ProfilesSet(UserDict.IterableUserDict):
    """A mapping of application ids to the corresponding C4Profile objects."""

    #####
    #
    # Instance methods
    #
    #####

    def __init__(self):
        """Adds a data mapping attribute that third parties can use to store
        or cache data related to this profiles set.
        """
        UserDict.IterableUserDict.__init__(self)
        self.userdata = {}


    def installer_profiles(self, appid):
        """Returns the application's installer profiles if any, and the
        unknown application installer profiles otherwise.
        """
        installers = None
        if appid in self:
            profile = self[appid]
            if profile.app_profile and profile.app_profile.installer_id:
                try:
                    profile = self[profile.app_profile.installer_id]
                except KeyError:
                    profile = None
            if profile:
                installers = profile.installer_profiles
        if not installers:
            installers = self[UNKNOWN].installer_profiles
        return installers

    installers_ = installer_profiles

    def unknown_installer(self):
        return self[UNKNOWN].installer_profiles[0]

    def unknown_profile(self):
        return self[UNKNOWN]

    def contentsDict(self):
        return self.data


    #####
    #
    # Class methods and data
    #
    #####

    _lock = threading.Lock()
    # To avoid races while updating _sources, should not be held while blocking.

    _update_lock = threading.Lock()
    # Held while reading crossties.

    _updating = False
    # True if a thread is updating crossties, protected by _lock

    _retry_update = False
    # True if sources were added while updating crossties, protected by _lock

    _sources = {}
    # Keep track of the files the profiles come from. This is so we know when
    # to refresh the profiles set, and also so we know to delete a given
    # profile if the file goes away. This is a mapping so each file is present
    # only once (and for now order does not matter).
    # Protected by _lock

    _last_checked = 0
    # The timestamp of the last time the freshness of the _profiles data
    # was checked
    # Protected by _lock

    _profiles = None
    # The cached profiles set. This variable is updated atomically, so this
    # set can be returned at any time without locking.
    # Protected by _lock

    _revoked_licenses = set()
    # The set of revoked licenses. This is intentionally static (no need to
    # track it separately for each set) and is updated whenever profiles are
    # refreshed.

    _mtimes = {}
    # We keep track of the mtimes of each file in order to
    # update our list when needed. The keys are source files.
    # Protected by _lock

    tiedir = os.path.join(cxproduct.get_user_dir(), "tie")

    _tie_files_loaded = False
    # True if _sources contains the standard builtin crosstie files.
    # Protected by _lock

    @classmethod
    def add_c4p(cls, c4pfile):
        """Adds the specified CrossTie profile to the global database.

        The CrossTie file is then watched for updates and re-read if it is
        modified.
        """
        with cls._lock:
            cls._sources[c4pfile.filename] = _C4PSource(c4pfile, c4pfile.trusted, c4pfile.source)
            cls._last_checked = 0
            if cls._updating:
                cls._retry_update = True
        return c4pfile

    @classmethod
    def add_c4p_file(cls, c4pfilename, sigfile=None, source=c4profiles.builtin):
        """Reads the specified c4p file and add its profiles to the global
        database.

        The c4p file is then watched for updates and re-read if it is modified.
        """
        trusted = cls.is_c4p_file_trusted(c4pfilename, sigfile)
        if not trusted and source == c4profiles.download:
            if os.path.exists(c4pfilename + ".new"):
                # We were interrupted in fileupdate.install()
                shutil.move(c4pfilename + ".new", c4pfilename)
                trusted = cls.is_c4p_file_trusted(c4pfilename, sigfile)
            if not trusted:
                cxlog.err("Deleting '%s' because it lacks a valid signature." % c4pfilename)
                os.remove(c4pfilename)
                return None
        c4pfile = c4parser.C4PFile(c4pfilename, trusted, source)
        return cls.add_c4p(c4pfile)

    @classmethod
    def is_c4p_file_trusted(cls, filepath, sigfilepath=None):
        """Evaluate the trustworthiness of a c4p. Typically the
        signature filename (if it exists) can be derived from
        the filepath. When using temp files (e.g. via taste_new_c4p_file)
        the sigfile may have been uniquely generated in which case
        it needs to be specified here.
        """

        if not filepath:
            return False

        # Files that originate from within the CrossOver package
        #  get a free pass.
        if filepath.startswith(cxutils.CX_ROOT):
            return True

        if fileupdate.is_signed(filepath, sigfilepath):
            return True
        return False

    @classmethod
    def taste_new_c4p_file(cls, sourcetie):
        """For GUI purposes it is useful to be able to detect if a file will be
        trusted or untrusted before we actually load it into any CrossOver
        dirs. This function prepares and tests the signature of a CrossTie file
        using disposable temp files.

        Returns a (trusted, tmptie, tmpsig) triplet where trusted is True if
        the file is trusted and False otherwise, and tmptie and tmpsig are the
        temporary filenames if the file is trusted.
        """
        (tmptie, tmpsig) = fileupdate.prepare(sourcetie)
        if not tmptie:
            return (False, None, None)

        if cls.is_c4p_file_trusted(tmptie, tmpsig):
            return (True, tmptie, tmpsig)

        os.remove(tmptie)
        if tmpsig:
            os.remove(tmpsig)
        return (False, None, None)

    @classmethod
    def _rebuild_profiles(cls, sources):
        profiles = C4ProfilesSet()
        profile_lists = []
        result = None
        for source in sources.itervalues():
            try:
                # Also refreshes that source's profile list
                profile_list = source.c4pfile.profiles
                if profile_list is None:
                    # If the file does not exist, then we get
                    # None with no exception
                    continue
            except Exception, exception: # pylint: disable=W0703
                cxlog.err("an error occurred while reading %s:\n%s" % (cxlog.debug_str(source), cxlog.debug_str(exception)))
                continue

            # First check all files for revoke profiles, so we know
            # new revoke profiles are applied everywhere
            if source.trusted and source.c4pfile.revokelist:
                revokelist_file = c4profiles.get_revokelist_file()

                revokelist_file.lock_file()
                try:
                    for revoke_profile in source.c4pfile.revokelist:
                        revoke_string = '%s..%s' % (
                            revoke_profile.first and revoke_profile.first.encode('utf8') or '',
                            revoke_profile.last and revoke_profile.last.encode('utf8') or '')
                        section = revokelist_file[revoke_profile.appid.encode('utf8')]

                        if revoke_string not in section.values():
                            section['Range%s' % len(section)] = revoke_string
                finally:
                    revokelist_file.save_and_unlock_file()

            profile_lists.append(profile_list)

        for profile_list in profile_lists:
            for profile in profile_list:
                if profile.is_revoked:
                    continue

                appid = profile.appid
                if appid not in profiles:
                    profiles[appid] = profile
                elif profile.score < profiles[appid].score:
                    profiles[appid] = profile

        # Atomically update cls._profiles
        with cls._lock:
            if not cls._retry_update:
                cls._profiles = result = profiles
                cls._last_checked = time.time()
            cls._updating = False

        return result

    @classmethod
    def all_profiles(cls):
        """Returns a mapping of application ids to the corresponding C4Profile
        objects.

        This automatically reads or re-reads the relevant c4p files if needed
        but will always return a consistent view of the underlying c4p profile
        database.
        """
        if not cls._tie_files_loaded:
            with cls._update_lock:
                if not cls._tie_files_loaded:
                    cls.load_tie_files()
                    cls._tie_files_loaded = True

        result = None

        # The consistency guarantee means that even if we split the local C4
        # database across many different c4p files, and these are in various
        # stages of being updated, the returned profiles will not mix old data
        # with new data.
        while result is None:
            other_thread_updating = False
            needs_update = False

            with cls._lock:
                if cls._updating:
                    other_thread_updating = True
                elif cls._profiles is None or cls._last_checked + 2 < time.time():
                    needs_update = True
                    cls._updating = True
                    cls._retry_update = False
                    sources = cls._sources.copy()
                else:
                    result = cls._profiles

            if other_thread_updating:
                with cls._update_lock:
                    pass

            elif needs_update:
                with cls._update_lock:
                    # Check if one of the source files changed
                    changes = False

                    for source in sources.itervalues():
                        latestmtime = source.c4pfile.update()
                        if latestmtime and ((source not in cls._mtimes) or (latestmtime > cls._mtimes[source])):
                            cls._mtimes[source] = latestmtime
                            if source.trusted:
                                cls._revoked_licenses.update(source.c4pfile.disabled_licenses)
                            changes = True

                    if changes:
                        # If a profile has changed for a given appid in one of the
                        # source files, then we need to reevaluate whether it's the
                        # profile that best matches the current product for that
                        # appid, or whether a profile from another source file
                        # should be used instead.
                        # The simplest way to do so, and possibly fastest too, is
                        # to rebuild the list from scratch.
                        result = cls._rebuild_profiles(sources)
                    else:
                        with cls._lock:
                            if not cls._retry_update:
                                result = cls._profiles
                                cls._last_checked = time.time()
                            cls._updating = False

        return result

    @classmethod
    def load_tie_files(cls):
        for basename in ("crossover.tie", "unknown.tie"):
            filename = os.path.join(cxutils.CX_ROOT, "share/crossover/data", basename)
            cls.add_c4p_file(filename, None, c4profiles.builtin)

        if os.path.exists(cls.tiedir):
            for c4pfile in os.listdir(cls.tiedir):
                if os.path.splitext(c4pfile)[1] == ".c4p" or os.path.splitext(c4pfile)[1] == ".tie":
                    if c4pfile == os.path.basename(online_file):
                        cls.add_c4p_file(os.path.join(cls.tiedir, c4pfile), None, c4profiles.download)
                    else:
                        cls.add_c4p_file(os.path.join(cls.tiedir, c4pfile), None, c4profiles.dropin)

    @classmethod
    def get_revoked_licenses(cls):
        """Returns a set of revoked license id's."""
        cls.all_profiles() # reload ties as needed
        return cls._revoked_licenses

    def copy(self):
        # We have to override this because IterableUserDict wrongly copies userdata.
        result = C4ProfilesSet()
        result.data.update(self.data)
        return result


online_file = os.path.join(C4ProfilesSet.tiedir, "crossover.tie")

@cxobjc.method(C4Profiles, 'getOnlineFilePath')
def getOnlineFilePath():
    # This is used by the Mac GUI to display the mtime of the file.
    return online_file

# Return True if anything is updated.
@cxobjc.method(C4Profiles, 'updateOnlineProfilesFromURL_')
def update_online_profiles(url):
    add_profiles = not os.path.exists(online_file)
    if not fileupdate.update(online_file, url, True):
        return False

    if add_profiles:
        c4pfile = c4parser.C4PFile(online_file, True, c4profiles.download)
        C4ProfilesSet.add_c4p(c4pfile)

    return True