WIP: Added Driver and Settings objects to clean up API
[zeroinstall.git] / zeroinstall / injector / driver.py
blob0966e72c4dc0068f51868a8d0921aa535fe3d273
1 """
2 A driver manages the process of iteratively solving and downloading extra feeds, and
3 then downloading the implementations.
4 settings.
5 """
7 # Copyright (C) 2011, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
10 from zeroinstall import _
11 import time
12 import os
13 from logging import info, debug, warn
14 import ConfigParser
16 from zeroinstall import zerostore, SafeException
17 from zeroinstall.injector import arch, model
18 from zeroinstall.injector.model import Interface, Implementation, network_levels, network_offline, DistributionImplementation, network_full
19 from zeroinstall.injector.handler import Handler
20 from zeroinstall.injector.namespaces import config_site, config_prog
21 from zeroinstall.support import tasks, basedir
23 # If we started a check within this period, don't start another one:
24 FAILED_CHECK_DELAY = 60 * 60 # 1 Hour
26 class Driver:
27 """Manages the process of downloading feeds, solving, and downloading implementations.
28 Typical use:
29 1. Create a Driver object using a DriverFactory, giving it the Requirements about the program to be run.
30 2. Call L{solve_with_downloads}. If more information is needed, a L{fetch.Fetcher} will be used to download it.
31 3. When all downloads are complete, the L{solver} contains the chosen versions.
32 4. Use L{get_uncached_implementations} to find where to get these versions and download them
33 using L{download_uncached_implementations}.
34 @ivar solver: solver used to choose a set of implementations
35 @type solver: L{solve.Solver}
36 @ivar watchers: callbacks to invoke after recalculating
37 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
38 @type stale_feeds: set
39 """
40 __slots__ = ['watchers', 'requirements', '_warned_offline', 'stale_feeds', 'solver']
42 def __init__(self, requirements = None, solver = None):
43 """
44 @param requirements: Details about the program we want to run
45 @type requirements: L{requirements.Requirements}
46 """
47 self.watchers = []
48 self.target_arch = arch.get_architecture(requirements.os, requirements.cpu)
49 self.requirements = requirements
50 self.solver = solver
52 self.stale_feeds = set()
54 # If we need to download something but can't because we are offline,
55 # warn the user. But only the first time.
56 self._warned_offline = False
58 def download_and_import_feed_if_online(self, feed_url):
59 """If we're online, call L{fetch.Fetcher.download_and_import_feed}. Otherwise, log a suitable warning."""
60 if self.network_use != network_offline:
61 debug(_("Feed %s not cached and not off-line. Downloading..."), feed_url)
62 return self.fetcher.download_and_import_feed(feed_url, self.iface_cache)
63 else:
64 if self._warned_offline:
65 debug(_("Not downloading feed '%s' because we are off-line."), feed_url)
66 else:
67 warn(_("Not downloading feed '%s' because we are in off-line mode."), feed_url)
68 self._warned_offline = True
70 def get_uncached_implementations(self):
71 """List all chosen implementations which aren't yet available locally.
72 @rtype: [(L{model.Interface}, L{model.Implementation})]"""
73 iface_cache = self.iface_cache
74 uncached = []
75 for uri, selection in self.solver.selections.selections.iteritems():
76 impl = selection.impl
77 assert impl, self.solver.selections
78 if not self.stores.is_available(impl):
79 uncached.append((iface_cache.get_interface(uri), impl))
80 return uncached
82 @tasks.async
83 def solve_with_downloads(self, force = False, update_local = False):
84 """Run the solver, then download any feeds that are missing or
85 that need to be updated. Each time a new feed is imported into
86 the cache, the solver is run again, possibly adding new downloads.
87 @param force: whether to download even if we're already ready to run.
88 @param update_local: fetch PackageKit feeds even if we're ready to run."""
90 downloads_finished = set() # Successful or otherwise
91 downloads_in_progress = {} # URL -> Download
93 host_arch = self.target_arch
94 if self.requirements.source:
95 host_arch = arch.SourceArchitecture(host_arch)
97 # There are three cases:
98 # 1. We want to run immediately if possible. If not, download all the information we can.
99 # (force = False, update_local = False)
100 # 2. We're in no hurry, but don't want to use the network unnecessarily.
101 # We should still update local information (from PackageKit).
102 # (force = False, update_local = True)
103 # 3. The user explicitly asked us to refresh everything.
104 # (force = True)
106 try_quick_exit = not (force or update_local)
108 while True:
109 self.solver.solve(self.root, host_arch, command_name = self.command)
110 for w in self.watchers: w()
112 if try_quick_exit and self.solver.ready:
113 break
114 try_quick_exit = False
116 if not self.solver.ready:
117 force = True
119 for f in self.solver.feeds_used:
120 if f in downloads_finished or f in downloads_in_progress:
121 continue
122 if os.path.isabs(f):
123 if force:
124 self.iface_cache.get_feed(f, force = True)
125 downloads_in_progress[f] = tasks.IdleBlocker('Refresh local feed')
126 continue
127 elif f.startswith('distribution:'):
128 if force or update_local:
129 downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.iface_cache)
130 elif force and self.network_use != network_offline:
131 downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, self.iface_cache)
132 # Once we've starting downloading some things,
133 # we might as well get them all.
134 force = True
136 if not downloads_in_progress:
137 if self.network_use == network_offline:
138 info(_("Can't choose versions and in off-line mode, so aborting"))
139 break
141 # Wait for at least one download to finish
142 blockers = downloads_in_progress.values()
143 yield blockers
144 tasks.check(blockers, self.handler.report_error)
146 for f in downloads_in_progress.keys():
147 if f in downloads_in_progress and downloads_in_progress[f].happened:
148 del downloads_in_progress[f]
149 downloads_finished.add(f)
151 # Need to refetch any "distribution" feed that
152 # depends on this one
153 distro_feed_url = 'distribution:' + f
154 if distro_feed_url in downloads_finished:
155 downloads_finished.remove(distro_feed_url)
156 if distro_feed_url in downloads_in_progress:
157 del downloads_in_progress[distro_feed_url]
159 @tasks.async
160 def solve_and_download_impls(self, refresh = False, select_only = False):
161 """Run L{solve_with_downloads} and then get the selected implementations too.
162 @raise SafeException: if we couldn't select a set of implementations
163 @since: 0.40"""
164 refreshed = self.solve_with_downloads(refresh)
165 if refreshed:
166 yield refreshed
167 tasks.check(refreshed)
169 if not self.solver.ready:
170 raise self.solver.get_failure_reason()
172 if not select_only:
173 downloaded = self.download_uncached_implementations()
174 if downloaded:
175 yield downloaded
176 tasks.check(downloaded)
178 def need_download(self):
179 """Decide whether we need to download anything (but don't do it!)
180 @return: true if we MUST download something (feeds or implementations)
181 @rtype: bool"""
182 host_arch = self.target_arch
183 if self.requirements.source:
184 host_arch = arch.SourceArchitecture(host_arch)
185 self.solver.solve(self.root, host_arch, command_name = self.command)
186 for w in self.watchers: w()
188 if not self.solver.ready:
189 return True # Maybe a newer version will work?
191 if self.get_uncached_implementations():
192 return True
194 return False
196 def download_uncached_implementations(self):
197 """Download all implementations chosen by the solver that are missing from the cache."""
198 assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections
199 return self.fetcher.download_impls([impl for impl in self.solver.selections.values() if not self.stores.is_available(impl)],
200 self.stores)
202 class DriverFactory:
203 def __init__(self, settings, iface_cache, stores, user_interface):
204 self.settings = settings
205 self.iface_cache = iface_cache
206 self.stores = stores
207 self.user_interface = user_interface
209 def make_driver(self, requirements):
210 from zeroinstall.injector.solver import DefaultSolver
211 solver = DefaultSolver(self.settings, self.stores, self.iface_cache)
213 if requirements.before or requirements.not_before:
214 solver.extra_restrictions[self.iface_cache.get_interface(requirements.interface_uri)] = [
215 model.VersionRangeRestriction(model.parse_version(requirements.before),
216 model.parse_version(requirements.not_before))]
218 return Driver(requirements, solver = solver)