Update year to 2009 in various places
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / policy.py
blob2d31933560e64dd750b2a87b8ef910d2f0770831
1 """
2 This class brings together a L{solve.Solver} to choose a set of implmentations, a
3 L{fetch.Fetcher} to download additional components, and the user's configuration
4 settings.
5 """
7 # Copyright (C) 2009, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
10 import time
11 import os
12 from logging import info, debug, warn
13 import ConfigParser
15 from zeroinstall import SafeException
16 from zeroinstall.injector import arch
17 from zeroinstall.injector.model import Interface, Implementation, network_levels, network_offline, DistributionImplementation, network_full
18 from zeroinstall.injector.namespaces import config_site, config_prog, injector_gui_uri
19 from zeroinstall.support import tasks, basedir
20 from zeroinstall.injector.iface_cache import iface_cache
22 # If we started a check within this period, don't start another one:
23 FAILED_CHECK_DELAY = 60 * 60 # 1 Hour
25 class Policy(object):
26 """Chooses a set of implementations based on a policy.
27 Typical use:
28 1. Create a Policy object, giving it the URI of the program to be run and a handler.
29 2. Call L{solve_with_downloads}. If more information is needed, a L{fetch.Fetcher} will be used to download it.
30 3. When all downloads are complete, the L{solver} contains the chosen versions.
31 4. Use L{get_uncached_implementations} to find where to get these versions and download them
32 using L{download_uncached_implementations}.
34 @ivar target_arch: target architecture for binaries
35 @type target_arch: L{arch.Architecture}
36 @ivar root: URI of the root interface
37 @ivar solver: solver used to choose a set of implementations
38 @type solver: L{solve.Solver}
39 @ivar watchers: callbacks to invoke after recalculating
40 @ivar help_with_testing: default stability policy
41 @type help_with_testing: bool
42 @ivar network_use: one of the model.network_* values
43 @ivar freshness: seconds allowed since last update
44 @type freshness: int
45 @ivar handler: handler for main-loop integration
46 @type handler: L{handler.Handler}
47 @ivar src: whether we are looking for source code
48 @type src: bool
49 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
50 @type stale_feeds: set
51 """
52 __slots__ = ['root', 'watchers',
53 'freshness', 'handler', '_warned_offline',
54 'target_arch', 'src', 'stale_feeds', 'solver', '_fetcher']
56 help_with_testing = property(lambda self: self.solver.help_with_testing,
57 lambda self, value: setattr(self.solver, 'help_with_testing', value))
59 network_use = property(lambda self: self.solver.network_use,
60 lambda self, value: setattr(self.solver, 'network_use', value))
62 implementation = property(lambda self: self.solver.selections)
64 ready = property(lambda self: self.solver.ready)
66 def __init__(self, root, handler = None, src = False):
67 """
68 @param root: The URI of the root interface (the program we want to run).
69 @param handler: A handler for main-loop integration.
70 @type handler: L{zeroinstall.injector.handler.Handler}
71 @param src: Whether we are looking for source code.
72 @type src: bool
73 """
74 self.watchers = []
75 self.freshness = 60 * 60 * 24 * 30
76 self.src = src # Root impl must be a "src" machine type
77 self.stale_feeds = set()
79 from zeroinstall.injector.solver import DefaultSolver
80 self.solver = DefaultSolver(network_full, iface_cache, iface_cache.stores)
82 # If we need to download something but can't because we are offline,
83 # warn the user. But only the first time.
84 self._warned_offline = False
85 self._fetcher = None
87 # (allow self for backwards compat)
88 self.handler = handler or self
90 debug("Supported systems: '%s'", arch.os_ranks)
91 debug("Supported processors: '%s'", arch.machine_ranks)
93 path = basedir.load_first_config(config_site, config_prog, 'global')
94 if path:
95 try:
96 config = ConfigParser.ConfigParser()
97 config.read(path)
98 self.solver.help_with_testing = config.getboolean('global',
99 'help_with_testing')
100 self.solver.network_use = config.get('global', 'network_use')
101 self.freshness = int(config.get('global', 'freshness'))
102 assert self.solver.network_use in network_levels
103 except Exception, ex:
104 warn("Error loading config: %s", ex)
106 self.set_root(root)
108 self.target_arch = arch.get_host_architecture()
110 @property
111 def fetcher(self):
112 if not self._fetcher:
113 import fetch
114 self._fetcher = fetch.Fetcher(self.handler)
115 return self._fetcher
117 def set_root(self, root):
118 """Change the root interface URI."""
119 assert isinstance(root, (str, unicode))
120 self.root = root
121 for w in self.watchers: w()
123 def save_config(self):
124 """Write global settings."""
125 config = ConfigParser.ConfigParser()
126 config.add_section('global')
128 config.set('global', 'help_with_testing', self.help_with_testing)
129 config.set('global', 'network_use', self.network_use)
130 config.set('global', 'freshness', self.freshness)
132 path = basedir.save_config_path(config_site, config_prog)
133 path = os.path.join(path, 'global')
134 config.write(file(path + '.new', 'w'))
135 os.rename(path + '.new', path)
137 def recalculate(self, fetch_stale_interfaces = True):
138 """@deprecated: see L{solve_with_downloads} """
139 self.stale_feeds = set()
141 host_arch = self.target_arch
142 if self.src:
143 host_arch = arch.SourceArchitecture(host_arch)
144 self.solver.solve(self.root, host_arch)
146 if self.network_use == network_offline:
147 fetch_stale_interfaces = False
149 blockers = []
150 for f in self.solver.feeds_used:
151 if f.startswith('/'): continue
152 feed = iface_cache.get_feed(f)
153 if feed is None or feed.last_modified is None:
154 self.download_and_import_feed_if_online(f) # Will start a download
155 elif self.is_stale(feed):
156 debug("Adding %s to stale set", f)
157 self.stale_feeds.add(iface_cache.get_interface(f)) # Legacy API
158 if fetch_stale_interfaces:
159 self.download_and_import_feed_if_online(f) # Will start a download
161 for w in self.watchers: w()
163 return blockers
165 def usable_feeds(self, iface):
166 """Generator for C{iface.feeds} that are valid for our architecture.
167 @rtype: generator
168 @see: L{arch}"""
169 if self.src and iface.uri == self.root:
170 # Note: when feeds are recursive, we'll need a better test for root here
171 machine_ranks = {'src': 1}
172 else:
173 machine_ranks = arch.machine_ranks
175 for f in iface.feeds:
176 if f.os in arch.os_ranks and f.machine in machine_ranks:
177 yield f
178 else:
179 debug("Skipping '%s'; unsupported architecture %s-%s",
180 f, f.os, f.machine)
182 def is_stale(self, feed):
183 """Check whether feed needs updating, based on the configured L{freshness}.
184 None is considered to be stale.
185 @return: true if feed is stale or missing."""
186 if feed is None:
187 return True
188 if feed.url.startswith('/'):
189 return False # Local feeds are never stale
190 if feed.last_modified is None:
191 return True # Don't even have it yet
192 now = time.time()
193 staleness = now - (feed.last_checked or 0)
194 debug("Staleness for %s is %.2f hours", feed, staleness / 3600.0)
196 if self.freshness == 0 or staleness < self.freshness:
197 return False # Fresh enough for us
199 last_check_attempt = iface_cache.get_last_check_attempt(feed.url)
200 if last_check_attempt and last_check_attempt > now - FAILED_CHECK_DELAY:
201 debug("Stale, but tried to check recently (%s) so not rechecking now.", time.ctime(last_check_attempt))
202 return False
204 return True
206 def download_and_import_feed_if_online(self, feed_url):
207 """If we're online, call L{fetch.Fetcher.download_and_import_feed}. Otherwise, log a suitable warning."""
208 if self.network_use != network_offline:
209 debug("Feed %s not cached and not off-line. Downloading...", feed_url)
210 return self.fetcher.download_and_import_feed(feed_url, iface_cache)
211 else:
212 if self._warned_offline:
213 debug("Not downloading feed '%s' because we are off-line.", feed_url)
214 elif feed_url == injector_gui_uri:
215 # Don't print a warning, because we always switch to off-line mode to
216 # run the GUI the first time.
217 info("Not downloading GUI feed '%s' because we are in off-line mode.", feed_url)
218 else:
219 warn("Not downloading feed '%s' because we are in off-line mode.", feed_url)
220 self._warned_offline = True
222 def get_implementation_path(self, impl):
223 """Return the local path of impl.
224 @rtype: str
225 @raise zeroinstall.zerostore.NotStored: if it needs to be added to the cache first."""
226 assert isinstance(impl, Implementation)
227 if impl.id.startswith('/'):
228 return impl.id
229 return iface_cache.stores.lookup(impl.id)
231 def get_implementation(self, interface):
232 """Get the chosen implementation.
233 @type interface: Interface
234 @rtype: L{model.Implementation}
235 @raise SafeException: if interface has not been fetched or no implementation could be
236 chosen."""
237 assert isinstance(interface, Interface)
239 if not interface.name and not interface.feeds:
240 raise SafeException("We don't have enough information to "
241 "run this program yet. "
242 "Need to download:\n%s" % interface.uri)
243 try:
244 return self.implementation[interface]
245 except KeyError, ex:
246 if interface.implementations:
247 offline = ""
248 if self.network_use == network_offline:
249 offline = "\nThis may be because 'Network Use' is set to Off-line."
250 raise SafeException("No usable implementation found for '%s'.%s" %
251 (interface.name, offline))
252 raise ex
254 def get_cached(self, impl):
255 """Check whether an implementation is available locally.
256 @type impl: model.Implementation
257 @rtype: bool
259 if isinstance(impl, DistributionImplementation):
260 return impl.installed
261 if impl.id.startswith('/'):
262 return os.path.exists(impl.id)
263 else:
264 try:
265 path = self.get_implementation_path(impl)
266 assert path
267 return True
268 except:
269 pass # OK
270 return False
272 def get_uncached_implementations(self):
273 """List all chosen implementations which aren't yet available locally.
274 @rtype: [(L{model.Interface}, L{model.Implementation})]"""
275 uncached = []
276 for iface in self.solver.selections:
277 impl = self.solver.selections[iface]
278 assert impl, self.solver.selections
279 if not self.get_cached(impl):
280 uncached.append((iface, impl))
281 return uncached
283 def refresh_all(self, force = True):
284 """Start downloading all feeds for all selected interfaces.
285 @param force: Whether to restart existing downloads."""
286 return self.solve_with_downloads(force = True)
288 def get_feed_targets(self, feed_iface_uri):
289 """Return a list of Interfaces for which feed_iface can be a feed.
290 This is used by B{0launch --feed}.
291 @rtype: [model.Interface]
292 @raise SafeException: If there are no known feeds."""
293 # TODO: what if it isn't cached yet?
294 feed_iface = iface_cache.get_interface(feed_iface_uri)
295 if not feed_iface.feed_for:
296 if not feed_iface.name:
297 raise SafeException("Can't get feed targets for '%s'; failed to load interface." %
298 feed_iface_uri)
299 raise SafeException("Missing <feed-for> element in '%s'; "
300 "this interface can't be used as a feed." % feed_iface_uri)
301 feed_targets = feed_iface.feed_for
302 debug("Feed targets: %s", feed_targets)
303 if not feed_iface.name:
304 warn("Warning: unknown interface '%s'" % feed_iface_uri)
305 return [iface_cache.get_interface(uri) for uri in feed_targets]
307 @tasks.async
308 def solve_with_downloads(self, force = False):
309 """Run the solver, then download any feeds that are missing or
310 that need to be updated. Each time a new feed is imported into
311 the cache, the solver is run again, possibly adding new downloads.
312 @param force: whether to download even if we're already ready to run."""
314 downloads_finished = set() # Successful or otherwise
315 downloads_in_progress = {} # URL -> Download
317 host_arch = self.target_arch
318 if self.src:
319 host_arch = arch.SourceArchitecture(host_arch)
321 while True:
322 self.solver.solve(self.root, host_arch)
323 for w in self.watchers: w()
325 if self.solver.ready and not force:
326 break
327 else:
328 if self.network_use == network_offline and not force:
329 info("Can't choose versions and in off-line mode, so aborting")
330 break
331 # Once we've starting downloading some things,
332 # we might as well get them all.
333 force = True
335 for f in self.solver.feeds_used:
336 if f in downloads_finished or f in downloads_in_progress:
337 continue
338 if f.startswith('/'):
339 continue
340 feed = iface_cache.get_interface(f)
341 downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, iface_cache)
343 if not downloads_in_progress:
344 break
346 blockers = downloads_in_progress.values()
347 yield blockers
348 tasks.check(blockers, self.handler.report_error)
350 for f in downloads_in_progress.keys():
351 if downloads_in_progress[f].happened:
352 del downloads_in_progress[f]
353 downloads_finished.add(f)
355 def need_download(self):
356 """Decide whether we need to download anything (but don't do it!)
357 @return: true if we MUST download something (feeds or implementations)
358 @rtype: bool"""
359 host_arch = self.target_arch
360 if self.src:
361 host_arch = arch.SourceArchitecture(host_arch)
362 self.solver.solve(self.root, host_arch)
363 for w in self.watchers: w()
365 if not self.solver.ready:
366 return True # Maybe a newer version will work?
368 if self.get_uncached_implementations():
369 return True
371 return False
373 def download_uncached_implementations(self):
374 """Download all implementations chosen by the solver that are missing from the cache."""
375 assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections
376 return self.fetcher.download_impls([impl for impl in self.solver.selections.values() if not self.get_cached(impl)],
377 iface_cache.stores)
379 def download_icon(self, interface, force = False):
380 """Download an icon for this interface and add it to the
381 icon cache. If the interface has no icon or we are offline, do nothing.
382 @return: the task doing the import, or None
383 @rtype: L{tasks.Task}"""
384 debug("download_icon %s (force = %d)", interface, force)
386 if self.network_use == network_offline:
387 info("No icon present for %s, but off-line so not downloading", interface)
388 return
390 return self.fetcher.download_icon(interface, force)
392 def get_interface(self, uri):
393 """@deprecated: use L{iface_cache.IfaceCache.get_interface} instead"""
394 import warnings
395 warnings.warn("Policy.get_interface is deprecated!", DeprecationWarning, stacklevel = 2)
396 return iface_cache.get_interface(uri)