From 5bdbb2e8c79e9c6e85c90f4b1e886cf4b32b3a2d Mon Sep 17 00:00:00 2001 From: Thomas Leonard Date: Thu, 24 Feb 2011 21:26:30 +0000 Subject: [PATCH] WIP: Added Driver and Settings objects to clean up API --- tests/basetest.py | 3 +- tests/testinstall.py | 1 + tests/testsat.py | 5 +- tests/testsolver.py | 17 +-- zeroinstall/injector/driver.py | 218 ++++++++++++++++++++++++++++++++++++++ zeroinstall/injector/policy.py | 29 ++--- zeroinstall/injector/settings.py | 60 +++++++++++ zeroinstall/injector/solver.py | 16 ++- zeroinstall/zerostore/__init__.py | 17 +++ 9 files changed, 327 insertions(+), 39 deletions(-) create mode 100644 zeroinstall/injector/driver.py create mode 100644 zeroinstall/injector/settings.py diff --git a/tests/basetest.py b/tests/basetest.py index e30618b..eb3a74e 100755 --- a/tests/basetest.py +++ b/tests/basetest.py @@ -11,7 +11,7 @@ os.environ['HOME'] = '/home/idontexist' os.environ['LANGUAGE'] = 'C' sys.path.insert(0, '..') -from zeroinstall.injector import qdom +from zeroinstall.injector import qdom, driver from zeroinstall.injector import iface_cache, download, distro, model, handler, policy, reader from zeroinstall.zerostore import NotStored, Store, Stores; Store._add_with_helper = lambda *unused: False from zeroinstall import support @@ -148,6 +148,7 @@ class TestConfig: self.handler = DummyHandler() self.stores = Stores() self.fetcher = TestFetcher(self) + self.driver_factory = driver.DriverFactory(settings = self, iface_cache = self.iface_cache, stores = self.stores, user_interface = self.handler) class BaseTest(unittest.TestCase): def setUp(self): diff --git a/tests/testinstall.py b/tests/testinstall.py index 714ccb4..d9b841c 100755 --- a/tests/testinstall.py +++ b/tests/testinstall.py @@ -145,6 +145,7 @@ class TestInstall(BaseTest): # Using a remote feed for the first time self.config.stores = TestStores() + self.config.driver_factory.stores = self.config.stores binary_feed = reader.load_feed('Binary.xml') self.config.fetcher.allow_download('sha1=123') self.config.fetcher.allow_feed_download('http://foo/Binary.xml', binary_feed) diff --git a/tests/testsat.py b/tests/testsat.py index 579654f..e778820 100755 --- a/tests/testsat.py +++ b/tests/testsat.py @@ -134,10 +134,7 @@ def assertSelection(expected, repo): class TestConfig: help_with_testing = False network_use = model.network_offline - stores = stores - iface_cache = cache - - s = Solver(TestConfig()) + s = Solver(TestConfig(), stores, cache) s.solve(root, arch.get_architecture('Linux', 'x86_64')) if expected[0][1] == 'FAIL': diff --git a/tests/testsolver.py b/tests/testsolver.py index ad962eb..415984f 100755 --- a/tests/testsolver.py +++ b/tests/testsolver.py @@ -11,9 +11,13 @@ logger = logging.getLogger() #logger.setLevel(logging.DEBUG) class TestSolver(BaseTest): + def setUp(self): + BaseTest.setUp(self) + self.solver = solver.SATSolver(self.config, self.config.stores, self.config.iface_cache) + def testSimple(self): iface_cache = self.config.iface_cache - s = solver.DefaultSolver(self.config) + s = self.solver foo = iface_cache.get_interface('http://foo/Binary.xml') self.import_feed(foo.uri, 'Binary.xml') @@ -43,7 +47,7 @@ class TestSolver(BaseTest): def testDetails(self): iface_cache = self.config.iface_cache - s = solver.DefaultSolver(self.config) + s = self.solver foo_binary_uri = 'http://foo/Binary.xml' foo = iface_cache.get_interface(foo_binary_uri) @@ -70,7 +74,8 @@ class TestSolver(BaseTest): def testRecursive(self): iface_cache = self.config.iface_cache - s = solver.DefaultSolver(self.config) + s = self.solver + foo = iface_cache.get_interface('http://foo/Recursive.xml') self.import_feed(foo.uri, 'Recursive.xml') @@ -87,7 +92,7 @@ class TestSolver(BaseTest): def testMultiArch(self): iface_cache = self.config.iface_cache - s = solver.DefaultSolver(self.config) + s = self.solver foo = iface_cache.get_interface('http://foo/MultiArch.xml') self.import_feed(foo.uri, 'MultiArch.xml') @@ -128,7 +133,7 @@ class TestSolver(BaseTest): def testRanking(self): iface_cache = self.config.iface_cache - s = solver.DefaultSolver(self.config) + s = self.solver ranking = os.path.join(os.path.abspath(os.path.dirname(__file__)), 'Ranking.xml') iface = iface_cache.get_interface(ranking) @@ -152,7 +157,7 @@ class TestSolver(BaseTest): try: locale.setlocale(locale.LC_ALL, 'en_US.UTF-8') - s = solver.DefaultSolver(self.config) + s = self.solver iface = iface_cache.get_interface('http://foo/Langs.xml') self.import_feed(iface.uri, 'Langs.xml') diff --git a/zeroinstall/injector/driver.py b/zeroinstall/injector/driver.py new file mode 100644 index 0000000..0966e72 --- /dev/null +++ b/zeroinstall/injector/driver.py @@ -0,0 +1,218 @@ +""" +A driver manages the process of iteratively solving and downloading extra feeds, and +then downloading the implementations. +settings. +""" + +# Copyright (C) 2011, Thomas Leonard +# See the README file for details, or visit http://0install.net. + +from zeroinstall import _ +import time +import os +from logging import info, debug, warn +import ConfigParser + +from zeroinstall import zerostore, SafeException +from zeroinstall.injector import arch, model +from zeroinstall.injector.model import Interface, Implementation, network_levels, network_offline, DistributionImplementation, network_full +from zeroinstall.injector.handler import Handler +from zeroinstall.injector.namespaces import config_site, config_prog +from zeroinstall.support import tasks, basedir + +# If we started a check within this period, don't start another one: +FAILED_CHECK_DELAY = 60 * 60 # 1 Hour + +class Driver: + """Manages the process of downloading feeds, solving, and downloading implementations. + Typical use: + 1. Create a Driver object using a DriverFactory, giving it the Requirements about the program to be run. + 2. Call L{solve_with_downloads}. If more information is needed, a L{fetch.Fetcher} will be used to download it. + 3. When all downloads are complete, the L{solver} contains the chosen versions. + 4. Use L{get_uncached_implementations} to find where to get these versions and download them + using L{download_uncached_implementations}. + @ivar solver: solver used to choose a set of implementations + @type solver: L{solve.Solver} + @ivar watchers: callbacks to invoke after recalculating + @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time + @type stale_feeds: set + """ + __slots__ = ['watchers', 'requirements', '_warned_offline', 'stale_feeds', 'solver'] + + def __init__(self, requirements = None, solver = None): + """ + @param requirements: Details about the program we want to run + @type requirements: L{requirements.Requirements} + """ + self.watchers = [] + self.target_arch = arch.get_architecture(requirements.os, requirements.cpu) + self.requirements = requirements + self.solver = solver + + self.stale_feeds = set() + + # If we need to download something but can't because we are offline, + # warn the user. But only the first time. + self._warned_offline = False + + def download_and_import_feed_if_online(self, feed_url): + """If we're online, call L{fetch.Fetcher.download_and_import_feed}. Otherwise, log a suitable warning.""" + if self.network_use != network_offline: + debug(_("Feed %s not cached and not off-line. Downloading..."), feed_url) + return self.fetcher.download_and_import_feed(feed_url, self.iface_cache) + else: + if self._warned_offline: + debug(_("Not downloading feed '%s' because we are off-line."), feed_url) + else: + warn(_("Not downloading feed '%s' because we are in off-line mode."), feed_url) + self._warned_offline = True + + def get_uncached_implementations(self): + """List all chosen implementations which aren't yet available locally. + @rtype: [(L{model.Interface}, L{model.Implementation})]""" + iface_cache = self.iface_cache + uncached = [] + for uri, selection in self.solver.selections.selections.iteritems(): + impl = selection.impl + assert impl, self.solver.selections + if not self.stores.is_available(impl): + uncached.append((iface_cache.get_interface(uri), impl)) + return uncached + + @tasks.async + def solve_with_downloads(self, force = False, update_local = False): + """Run the solver, then download any feeds that are missing or + that need to be updated. Each time a new feed is imported into + the cache, the solver is run again, possibly adding new downloads. + @param force: whether to download even if we're already ready to run. + @param update_local: fetch PackageKit feeds even if we're ready to run.""" + + downloads_finished = set() # Successful or otherwise + downloads_in_progress = {} # URL -> Download + + host_arch = self.target_arch + if self.requirements.source: + host_arch = arch.SourceArchitecture(host_arch) + + # There are three cases: + # 1. We want to run immediately if possible. If not, download all the information we can. + # (force = False, update_local = False) + # 2. We're in no hurry, but don't want to use the network unnecessarily. + # We should still update local information (from PackageKit). + # (force = False, update_local = True) + # 3. The user explicitly asked us to refresh everything. + # (force = True) + + try_quick_exit = not (force or update_local) + + while True: + self.solver.solve(self.root, host_arch, command_name = self.command) + for w in self.watchers: w() + + if try_quick_exit and self.solver.ready: + break + try_quick_exit = False + + if not self.solver.ready: + force = True + + for f in self.solver.feeds_used: + if f in downloads_finished or f in downloads_in_progress: + continue + if os.path.isabs(f): + if force: + self.iface_cache.get_feed(f, force = True) + downloads_in_progress[f] = tasks.IdleBlocker('Refresh local feed') + continue + elif f.startswith('distribution:'): + if force or update_local: + downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.iface_cache) + elif force and self.network_use != network_offline: + downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.iface_cache) + # Once we've starting downloading some things, + # we might as well get them all. + force = True + + if not downloads_in_progress: + if self.network_use == network_offline: + info(_("Can't choose versions and in off-line mode, so aborting")) + break + + # Wait for at least one download to finish + blockers = downloads_in_progress.values() + yield blockers + tasks.check(blockers, self.handler.report_error) + + for f in downloads_in_progress.keys(): + if f in downloads_in_progress and downloads_in_progress[f].happened: + del downloads_in_progress[f] + downloads_finished.add(f) + + # Need to refetch any "distribution" feed that + # depends on this one + distro_feed_url = 'distribution:' + f + if distro_feed_url in downloads_finished: + downloads_finished.remove(distro_feed_url) + if distro_feed_url in downloads_in_progress: + del downloads_in_progress[distro_feed_url] + + @tasks.async + def solve_and_download_impls(self, refresh = False, select_only = False): + """Run L{solve_with_downloads} and then get the selected implementations too. + @raise SafeException: if we couldn't select a set of implementations + @since: 0.40""" + refreshed = self.solve_with_downloads(refresh) + if refreshed: + yield refreshed + tasks.check(refreshed) + + if not self.solver.ready: + raise self.solver.get_failure_reason() + + if not select_only: + downloaded = self.download_uncached_implementations() + if downloaded: + yield downloaded + tasks.check(downloaded) + + def need_download(self): + """Decide whether we need to download anything (but don't do it!) + @return: true if we MUST download something (feeds or implementations) + @rtype: bool""" + host_arch = self.target_arch + if self.requirements.source: + host_arch = arch.SourceArchitecture(host_arch) + self.solver.solve(self.root, host_arch, command_name = self.command) + for w in self.watchers: w() + + if not self.solver.ready: + return True # Maybe a newer version will work? + + if self.get_uncached_implementations(): + return True + + return False + + def download_uncached_implementations(self): + """Download all implementations chosen by the solver that are missing from the cache.""" + assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections + return self.fetcher.download_impls([impl for impl in self.solver.selections.values() if not self.stores.is_available(impl)], + self.stores) + +class DriverFactory: + def __init__(self, settings, iface_cache, stores, user_interface): + self.settings = settings + self.iface_cache = iface_cache + self.stores = stores + self.user_interface = user_interface + + def make_driver(self, requirements): + from zeroinstall.injector.solver import DefaultSolver + solver = DefaultSolver(self.settings, self.stores, self.iface_cache) + + if requirements.before or requirements.not_before: + solver.extra_restrictions[self.iface_cache.get_interface(requirements.interface_uri)] = [ + model.VersionRangeRestriction(model.parse_version(requirements.before), + model.parse_version(requirements.not_before))] + + return Driver(requirements, solver = solver) diff --git a/zeroinstall/injector/policy.py b/zeroinstall/injector/policy.py index b0956f0..f9f272d 100644 --- a/zeroinstall/injector/policy.py +++ b/zeroinstall/injector/policy.py @@ -14,7 +14,7 @@ from logging import info, debug, warn import ConfigParser from zeroinstall import zerostore, SafeException -from zeroinstall.injector import arch, model +from zeroinstall.injector import arch, model, driver from zeroinstall.injector.model import Interface, Implementation, network_levels, network_offline, DistributionImplementation, network_full from zeroinstall.injector.handler import Handler from zeroinstall.injector.namespaces import config_site, config_prog @@ -29,7 +29,7 @@ class Config(object): @type handler: L{handler.Handler} """ - __slots__ = ['help_with_testing', 'freshness', 'network_use', '_fetcher', '_stores', '_iface_cache', 'handler'] + __slots__ = ['help_with_testing', 'freshness', 'network_use', '_fetcher', '_stores', '_iface_cache', 'handler', 'driver_factory'] def __init__(self, handler): assert handler is not None self.help_with_testing = False @@ -37,6 +37,7 @@ class Config(object): self.network_use = network_full self.handler = handler self._fetcher = self._stores = self._iface_cache = None + self.driver_factory = driver.DriverFactory(settings = self, iface_cache = self.iface_cache, stores = self.stores, user_interface = handler) @property def stores(self): @@ -120,8 +121,8 @@ class Policy(object): @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time @type stale_feeds: set """ - __slots__ = ['root', 'watchers', 'requirements', 'config', '_warned_offline', - 'command', 'target_arch', + __slots__ = ['root', 'watchers', 'config', '_warned_offline', + 'target_arch', 'driver', 'stale_feeds', 'solver'] help_with_testing = property(lambda self: self.config.help_with_testing, @@ -135,6 +136,10 @@ class Policy(object): implementation = property(lambda self: self.solver.selections) + requirements = property(lambda self: self.driver.requirements) + + solver = property(lambda self: self.driver.solver) + ready = property(lambda self: self.solver.ready) # (was used by 0test) @@ -166,7 +171,6 @@ class Policy(object): assert root == src == None assert command == -1 self.target_arch = arch.get_architecture(requirements.os, requirements.cpu) - self.requirements = requirements self.stale_feeds = set() @@ -176,20 +180,7 @@ class Policy(object): assert handler is None, "can't pass a handler and a config" self.config = config - from zeroinstall.injector.solver import DefaultSolver - self.solver = DefaultSolver(self.config) - - # If we need to download something but can't because we are offline, - # warn the user. But only the first time. - self._warned_offline = False - - debug(_("Supported systems: '%s'"), arch.os_ranks) - debug(_("Supported processors: '%s'"), arch.machine_ranks) - - if requirements.before or requirements.not_before: - self.solver.extra_restrictions[config.iface_cache.get_interface(requirements.interface_uri)] = [ - model.VersionRangeRestriction(model.parse_version(requirements.before), - model.parse_version(requirements.not_before))] + self.driver = config.driver_factory.make_driver(requirements) @property def fetcher(self): diff --git a/zeroinstall/injector/settings.py b/zeroinstall/injector/settings.py new file mode 100644 index 0000000..b3765b3 --- /dev/null +++ b/zeroinstall/injector/settings.py @@ -0,0 +1,60 @@ +""" +Holds the user's preferences and settings. +""" + +# Copyright (C) 2011, Thomas Leonard +# See the README file for details, or visit http://0install.net. + +from zeroinstall import _ +import os +from logging import info, debug, warn +import ConfigParser + +from zeroinstall.injector.namespaces import config_site, config_prog +from zeroinstall.support import basedir + +class Settings(object): + __slots__ = ['help_with_testing', 'freshness', 'network_use'] + + def __init__(self): + self.help_with_testing = False + self.freshness = 60 * 60 * 24 * 30 + self.network_use = model.network_full + + def save_globals(self): + """Write global settings.""" + parser = ConfigParser.ConfigParser() + parser.add_section('global') + + parser.set('global', 'help_with_testing', self.help_with_testing) + parser.set('global', 'network_use', self.network_use) + parser.set('global', 'freshness', self.freshness) + + path = basedir.save_config_path(config_site, config_prog) + path = os.path.join(path, 'global') + parser.write(file(path + '.new', 'w')) + os.rename(path + '.new', path) + +def load_config(): + config = Config() + parser = ConfigParser.RawConfigParser() + parser.add_section('global') + parser.set('global', 'help_with_testing', 'False') + parser.set('global', 'freshness', str(60 * 60 * 24 * 30)) # One month + parser.set('global', 'network_use', 'full') + + path = basedir.load_first_config(config_site, config_prog, 'global') + if path: + info("Loading configuration from %s", path) + try: + parser.read(path) + except Exception, ex: + warn(_("Error loading config: %s"), str(ex) or repr(ex)) + + config.help_with_testing = parser.getboolean('global', 'help_with_testing') + config.network_use = parser.get('global', 'network_use') + config.freshness = int(parser.get('global', 'freshness')) + + assert config.network_use in model.network_levels, config.network_use + + return config diff --git a/zeroinstall/injector/solver.py b/zeroinstall/injector/solver.py index b2e8e4d..e325ae3 100644 --- a/zeroinstall/injector/solver.py +++ b/zeroinstall/injector/solver.py @@ -119,16 +119,12 @@ class Solver(object): raise NotImplementedError("Abstract") class SATSolver(Solver): - __slots__ = ['_failure_reason', 'config', 'extra_restrictions', 'langs'] - - @property - def iface_cache(self): - return self.config.iface_cache # (deprecated; used by 0compile) + __slots__ = ['_failure_reason', 'config', 'stores', 'iface_cache', 'extra_restrictions', 'langs'] """Converts the problem to a set of pseudo-boolean constraints and uses a PB solver to solve them. @ivar langs: the preferred languages (e.g. ["es_ES", "en"]). Initialised to the current locale. @type langs: str""" - def __init__(self, config, extra_restrictions = None): + def __init__(self, config, stores, iface_cache, extra_restrictions = None): """ @param network_use: how much use to make of the network @type network_use: L{model.network_levels} @@ -140,6 +136,8 @@ class SATSolver(Solver): Solver.__init__(self) assert not isinstance(config, str), "API change!" self.config = config + self.stores = stores + self.iface_cache = iface_cache self.extra_restrictions = extra_restrictions or {} self.langs = [locale.getlocale()[0] or 'en'] @@ -166,7 +164,7 @@ class SATSolver(Solver): r = cmp(a_stab == model.preferred, b_stab == model.preferred) if r: return r - stores = self.config.stores + stores = self.stores if self.config.network_use != model.network_full: r = cmp(_get_cached(stores, a), _get_cached(stores, b)) if r: return r @@ -231,7 +229,7 @@ class SATSolver(Solver): # this is probably too much. We could insert a dummy optimial # implementation in stale/uncached feeds and see whether it # selects that. - iface_cache = self.config.iface_cache + iface_cache = self.iface_cache problem = sat.SATProblem() @@ -305,7 +303,7 @@ class SATSolver(Solver): stability = impl.get_stability() if stability <= model.buggy: return stability.name - if (self.config.network_use == model.network_offline or not impl.download_sources) and not _get_cached(self.config.stores, impl): + if (self.config.network_use == model.network_offline or not impl.download_sources) and not _get_cached(self.stores, impl): if not impl.download_sources: return _("No retrieval methods") return _("Not cached and we are off-line") diff --git a/zeroinstall/zerostore/__init__.py b/zeroinstall/zerostore/__init__.py index 053a977..3a11e3d 100644 --- a/zeroinstall/zerostore/__init__.py +++ b/zeroinstall/zerostore/__init__.py @@ -249,6 +249,23 @@ class Stores(object): debug(_("Added system store '%s'"), directory) self.stores.append(Store(directory)) + def is_available(self, impl): + """Check whether an implementation is available locally. + @type impl: model.Implementation or selections.Selection + @rtype: bool + """ + if hasattr(impl, 'installed'): + return impl.installed + elif impl.local_path: + return os.path.exists(impl.local_path) + else: + try: + path = self.lookup_any(impl.digests) + assert path + return True + except NotStored: + return False + def lookup(self, digest): return self.lookup_any([digest]) -- 2.11.4.GIT