# (c) Copyright 2009-2011. CodeWeavers, Inc.
import os
import re
import distversion
import cxutils
import bottlequery
import c4profilesmanager
import cxglob
import cxlog
import cxobjc
def _get_uninstall_info(bottlename):
result = []
for parent_key in ('HKEY_LOCAL_MACHINE\\Software',
'HKEY_LOCAL_MACHINE\\Software\\Wow6432Node',
'HKEY_CURRENT_USER\\Software'):
uninstaller_key = '%s\\Microsoft\\Windows\\CurrentVersion\\Uninstall' % parent_key
try:
subkeys, _values = bottlequery.get_registry_key(bottlename, uninstaller_key)
except bottlequery.NotFoundError:
continue
for keyname in subkeys:
_subkeys, values = bottlequery.get_registry_key(bottlename, '%s\\%s' % (uninstaller_key, keyname))
result.append((keyname, values))
return result
class InstalledApplication(cxobjc.Proxy):
"""Describes an application installed in a specific bottle.
Each such object has an appid property which uniquely identifies it in a
bottle.
If there is a corresponding C4 profile then this is the profile's id. This
makes it easy to check if the application for a given profile is installed
or not.
If there is no corresponding C4 profile, then it's a string of the form
'.local.<keyname>' (since only applications that have a proper Uninstall
key can be detected without a profile. Since this id starts with a dot it
cannot collide with a profile id.
This class inherits from cxobjc.Proxy so that we can bind to it properly from
within ObjC. That means that it must always be created via the special
platformconstructor.
"""
# Name of the Uninstall registry key, if any
keyname = None
# Display name from the registry, if any
displayname = None
# True if the application is a system component
systemcomponent = False
# Command line for "Repair or Remove", if any
uninstallcommand = None
# C4 application profile, if any
_profile = None
def __init__(self, bottlename, profile, keyname):
cxobjc.Proxy.__init__(self)
self.bottlename = bottlename
self.keyname = keyname
if profile:
self._profile = profile
self.appid = profile.appid
else:
self.appid = ".local." + keyname
# This is a special initializer for objc. It must always be called
# explicitly on the mac.
def initWithBottleName_profile_andKeyName_(self, bottlename, profile, keyname):
self = cxobjc.Proxy.nsobject_init(self)
if self is not None:
self.__init__(bottlename, profile, keyname)
return self
# This is the constructor that should /always/ be used to create
# a new object. It will create the object as appropriate for
# the platform.
@classmethod
def platformconstructor(cls, bottlename, profile=None, keyname=None):
if distversion.IS_MACOSX:
# There's no alloc() method on non-Mac platforms...
# pylint: disable=E1101
return cls.alloc().initWithBottleName_profile_andKeyName_(bottlename, profile, keyname)
return cls(bottlename, profile, keyname)
def _getprofile(self):
return self._profile
def _setprofile(self, profile):
"""Associates a profile and updates the appid property accordingly."""
self._profile = profile
self.appid = profile.appid
profile = property(_getprofile, _setprofile)
def _getname(self):
"""Returns the installed application name.
This is the name of the C4 profile if there is one, or the display
name if there is one, and the registry key as a last resort.
"""
if self._profile:
return self._profile.name
if self.displayname:
return self.displayname
return self.keyname
name = property(_getname)
def _get_uninstall_applications(bottlename):
result = []
winebin = os.path.join(cxutils.CX_ROOT, "bin", "wine")
for keyname, values in _get_uninstall_info(bottlename):
app = InstalledApplication.platformconstructor(bottlename, keyname=keyname)
app.displayname = values.get('displayname')
app.systemcomponent = values.get('systemcomponent', 0) == 1
if 'uninstallstring' in values:
# we can't reliably call a windows command so just use uninstaller.exe
app.uninstallcommand = [winebin, '--bottle', bottlename, '--wl-app', 'uninstaller', '--remove', keyname]
result.append(app)
return result
class AppDetector(cxobjc.Proxy):
pass
def get_installed_applications(bottlename, profiles):
installed_apps = {}
# search for installed profiles
file_glob_tree = cxglob.FileGlobTree()
uninstall_apps = _get_uninstall_applications(bottlename)
known_uninstall_apps = set()
for profile in profiles.values():
app_profile = profile.app_profile
if app_profile is None:
continue
found = False
if app_profile.installed_key_pattern is not None:
try:
regex = re.compile(app_profile.installed_key_pattern, re.IGNORECASE)
except re.error:
cxlog.warn("installed key pattern %s in profile for %s is an invalid regular expression" % (cxlog.debug_str(app_profile.installed_key_pattern), cxlog.debug_str(profile.appid)))
else:
for app in uninstall_apps:
if regex.match(app.keyname.decode('utf8')):
installed_apps[profile.appid] = InstalledApplication.platformconstructor(bottlename, profile, app.keyname)
installed_apps[profile.appid].uninstallcommand = app.uninstallcommand
known_uninstall_apps.add(app.keyname)
found = True
break
if found:
continue
if app_profile.steamid:
# A special case. For a given steam appid there's a ready-made
# key pattern to check.
steamidstring = "Steam App %s" % app_profile.steamid
for app in uninstall_apps:
if steamidstring.lower() == app.keyname.decode('utf8'):
installed_apps[profile.appid] = InstalledApplication.platformconstructor(bottlename, profile, app.keyname)
installed_apps[profile.appid].uninstallcommand = app.uninstallcommand
known_uninstall_apps.add(app.keyname)
found = True
break
if found:
continue
if app_profile.installed_display_pattern is not None:
try:
regex = re.compile(app_profile.installed_display_pattern, re.IGNORECASE)
except re.error:
cxlog.warn("installed display pattern %s in profile for %s is an invalid regular expression" % (cxlog.debug_str(app_profile.installed_display_pattern), cxlog.debug_str(profile.appid)))
else:
for app in uninstall_apps:
if app.displayname is not None and regex.match(app.displayname.decode('utf8')):
installed_apps[profile.appid] = InstalledApplication.platformconstructor(bottlename, profile, app.keyname)
installed_apps[profile.appid].uninstallcommand = app.uninstallcommand
known_uninstall_apps.add(app.keyname)
found = True
break
if found:
continue
file_glob_tree.add_profile(profile, bottlename)
# scan the filesystem in one step
for _filename, profile in file_glob_tree.matches('', bottlename):
if profile.appid in installed_apps:
# don't make an extra InstalledApplication if we have one already
continue
app = InstalledApplication.platformconstructor(bottlename, profile)
installed_apps[app.appid] = app
# scan the registry in one step
registry_glob_tree = cxglob.RegistryGlobTree.get(profiles)
for key, (profile, glob) in registry_glob_tree.matches('', bottlename):
if profile.appid in installed_apps:
# don't make an extra InstalledApplication if we have one already
continue
elif glob.value_pattern != '':
# test the default value
key = key.replace('/', '\\')
if not key.endswith('\\'):
key = '%s\\' % key
_subkeys, values = bottlequery.get_registry_key(bottlename, key)
try:
data_regex = re.compile(glob.data_pattern, re.IGNORECASE)
except re.error:
cxlog.warn("registry data pattern %s in profile for %s is an invalid regular expression" % (cxlog.debug_str(glob.data_pattern), cxlog.debug_str(profile.appid)))
break
if glob.value_pattern == '@':
if '' not in values:
continue
if not data_regex.match(values['']):
continue
else:
try:
value_regex = re.compile(glob.value_pattern, re.IGNORECASE)
except re.error:
cxlog.warn("registry value pattern %s in profile for %s is an invalid regular expression" % (cxlog.debug_str(glob.value_pattern), cxlog.debug_str(profile.appid)))
break
for value in values:
if value_regex.match(value) and data_regex.match(str(values[value])):
break
else:
# no matching value found
continue
keyparts = key.split('\\')
# Note that key now contains an extra '\\' at the end
if len(keyparts) == 8 and \
(keyparts[0] == 'hkey_local_machine' or keyparts[0] == 'hkey_current_user') and \
keyparts[1] == 'software' and \
keyparts[2] == 'microsoft' and \
keyparts[3] == 'windows' and \
keyparts[4] == 'currentversion' and \
keyparts[5] == 'uninstall':
# Mark this 'uninstall' entry as known to avoid duplicates
known_uninstall_apps.add(keyparts[6])
app = InstalledApplication.platformconstructor(bottlename, profile)
installed_apps[app.appid] = app
# add any remaining applications with display names to the list
for app in uninstall_apps:
if app.displayname and not app.systemcomponent and \
app.keyname not in known_uninstall_apps:
installed_apps[app.appid] = app
return installed_apps
def fast_get_installed_applications(bottlename, profiles):
"""return a list of InstalledApplication objects in a bottle, using the C4
cache to work quickly"""
# FIXME: not implemented
return get_installed_applications(bottlename, profiles)
@cxobjc.method(AppDetector, 'fastGetInstalledApplicationsInBottle_')
def _mac_get_installed_apps(bottlename):
# FIXME: This method should take a C4ProfilesSet parameter and be merged
# with fast_get_installed_applications()
profiles = c4profilesmanager.C4ProfilesSet.all_profiles()
return fast_get_installed_applications(bottlename, profiles)
def is_profile_installed(bottlename, profile):
"""returns an InstalledApplication if the profile is installed, None if not"""
profiles = c4profilesmanager.C4ProfilesSet()
profiles[profile.appid] = profile
applications = get_installed_applications(bottlename, profiles)
if profile.appid in applications:
return applications[profile.appid]
return None