Ensure that begin_iface_download isn't called when the feed is already being downloaded.
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / policy.py
blobd07698a38f34474824e8d5a7a34f220708cdde9c
1 """
2 Chooses a set of implementations based on a policy.
4 @deprecated: see L{solver}
5 """
7 # Copyright (C) 2007, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
10 import time
11 import sys, os, sets
12 from logging import info, debug, warn
13 import arch
15 from model import *
16 import basedir
17 from namespaces import *
18 import ConfigParser
19 from zeroinstall import NeedDownload
20 from zeroinstall.injector.iface_cache import iface_cache, PendingFeed
21 from zeroinstall.injector.trust import trust_db
23 # If we started a check within this period, don't start another one:
24 FAILED_CHECK_DELAY = 60 * 60 # 1 Hour
26 class _Cook:
27 """A Cook follows a Recipe."""
28 # Maybe we're taking this metaphor too far?
30 def __init__(self, policy, required_digest, recipe, force = False):
31 """Start downloading all the ingredients."""
32 self.recipe = recipe
33 self.required_digest = required_digest
34 self.downloads = {} # Downloads that are not yet successful
35 self.streams = {} # Streams collected from successful downloads
37 # Start a download for each ingredient
38 for step in recipe.steps:
39 dl = policy.begin_archive_download(step, success_callback =
40 lambda stream, step=step: self.ingredient_ready(step, stream),
41 force = force)
42 self.downloads[step] = dl
43 self.test_done() # Needed for empty recipes
45 # Note: the only references to us are held by the on_success callback
46 # in each Download. On error this is removed, which will cause us
47 # to be destoryed, which will release all the temporary files we hold.
49 def ingredient_ready(self, step, stream):
50 # Called when one archive has been fetched. Store it until the other
51 # archives arrive.
52 assert step not in self.streams
53 self.streams[step] = stream
54 del self.downloads[step]
55 self.test_done()
57 def test_done(self):
58 # On success, a download is removed from here. If empty, it means that
59 # all archives have successfully been downloaded.
60 if self.downloads: return
62 from zeroinstall.zerostore import unpack
64 # Create an empty directory for the new implementation
65 store = iface_cache.stores.stores[0]
66 tmpdir = store.get_tmp_dir_for(self.required_digest)
67 try:
68 # Unpack each of the downloaded archives into it in turn
69 for step in self.recipe.steps:
70 unpack.unpack_archive_over(step.url, self.streams[step], tmpdir, step.extract)
71 # Check that the result is correct and store it in the cache
72 store.check_manifest_and_rename(self.required_digest, tmpdir)
73 tmpdir = None
74 finally:
75 # If unpacking fails, remove the temporary directory
76 if tmpdir is not None:
77 from zeroinstall import support
78 support.ro_rmtree(tmpdir)
80 class Policy(object):
81 """Chooses a set of implementations based on a policy.
82 Typical use:
83 1. Create a Policy object, giving it the URI of the program to be run and a handler.
84 2. Call L{recalculate}. If more information is needed, the handler will be used to download it.
85 3. When all downloads are complete, the L{implementation} map contains the chosen versions.
86 4. Use L{get_uncached_implementations} to find where to get these versions and download them
87 using L{begin_impl_download}.
89 @ivar root: URI of the root interface
90 @ivar implementation: chosen implementations
91 @type implementation: {model.Interface: model.Implementation or None}
92 @ivar watchers: callbacks to invoke after recalculating
93 @ivar help_with_testing: default stability policy
94 @type help_with_testing: bool
95 @ivar network_use: one of the model.network_* values
96 @ivar freshness: seconds allowed since last update
97 @type freshness: int
98 @ivar ready: whether L{implementation} is complete enough to run the program
99 @type ready: bool
100 @ivar handler: handler for main-loop integration
101 @type handler: L{handler.Handler}
102 @ivar restrictions: Currently known restrictions for each interface.
103 @type restrictions: {model.Interface -> [model.Restriction]}
104 @ivar src: whether we are looking for source code
105 @type src: bool
106 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
107 @type stale_feeds: set
109 __slots__ = ['root', 'implementation', 'watchers',
110 'freshness', 'ready', 'handler', '_warned_offline',
111 'restrictions', 'src', 'stale_feeds', 'solver']
113 help_with_testing = property(lambda self: self.solver.help_with_testing,
114 lambda self, value: setattr(self.solver, 'help_with_testing', value))
116 network_use = property(lambda self: self.solver.network_use,
117 lambda self, value: setattr(self.solver, 'network_use', value))
119 root_restrictions = property(lambda self: self.solver.root_restrictions,
120 lambda self, value: setattr(self.solver, 'root_restrictions', value))
122 def __init__(self, root, handler = None, src = False):
124 @param root: The URI of the root interface (the program we want to run).
125 @param handler: A handler for main-loop integration.
126 @type handler: L{zeroinstall.injector.handler.Handler}
127 @param src: Whether we are looking for source code.
128 @type src: bool
130 self.watchers = []
131 self.freshness = 60 * 60 * 24 * 30
132 self.ready = False
133 self.src = src # Root impl must be a "src" machine type
134 self.restrictions = {}
135 self.stale_feeds = sets.Set()
137 from zeroinstall.injector.solver import DefaultSolver
138 self.solver = DefaultSolver(network_full, iface_cache, iface_cache.stores, root_restrictions = [])
140 # If we need to download something but can't because we are offline,
141 # warn the user. But only the first time.
142 self._warned_offline = False
144 # (allow self for backwards compat)
145 self.handler = handler or self
147 debug("Supported systems: '%s'", arch.os_ranks)
148 debug("Supported processors: '%s'", arch.machine_ranks)
150 path = basedir.load_first_config(config_site, config_prog, 'global')
151 if path:
152 try:
153 config = ConfigParser.ConfigParser()
154 config.read(path)
155 self.solver.help_with_testing = config.getboolean('global',
156 'help_with_testing')
157 self.solver.network_use = config.get('global', 'network_use')
158 self.freshness = int(config.get('global', 'freshness'))
159 assert self.solver.network_use in network_levels
160 except Exception, ex:
161 warn("Error loading config: %s", ex)
163 self.set_root(root)
165 # Probably need weakrefs here...
166 iface_cache.add_watcher(self)
167 trust_db.watchers.append(self.process_pending)
169 def set_root(self, root):
170 """Change the root interface URI."""
171 assert isinstance(root, (str, unicode))
172 self.root = root
173 self.implementation = {} # Interface -> [Implementation | None]
175 def save_config(self):
176 """Write global settings."""
177 config = ConfigParser.ConfigParser()
178 config.add_section('global')
180 config.set('global', 'help_with_testing', self.help_with_testing)
181 config.set('global', 'network_use', self.network_use)
182 config.set('global', 'freshness', self.freshness)
184 path = basedir.save_config_path(config_site, config_prog)
185 path = os.path.join(path, 'global')
186 config.write(file(path + '.new', 'w'))
187 os.rename(path + '.new', path)
189 def process_pending(self):
190 """For each pending feed, either import it fully (if we now
191 trust one of the signatures) or start performing whatever action
192 is needed next (either downloading a key or confirming a
193 fingerprint).
194 @since: 0.25
196 # process_pending must never be called from recalculate
198 for pending in iface_cache.pending.values():
199 pending.begin_key_downloads(self.handler, lambda pending = pending: self._keys_ready(pending))
201 def _keys_ready(self, pending):
202 try:
203 iface = iface_cache.get_interface(pending.url)
204 # Note: this may call recalculate, but it shouldn't do any harm
205 # (just a bit slow)
206 updated = iface_cache.update_interface_if_trusted(iface, pending.sigs, pending.new_xml)
207 except SafeException, ex:
208 self.handler.report_error(ex)
209 # Ignore the problematic new version and continue...
210 else:
211 if not updated:
212 self.handler.confirm_trust_keys(iface, pending.sigs, pending.new_xml)
214 def recalculate(self, fetch_stale_interfaces = True):
215 """Try to choose a set of implementations.
216 This may start downloading more interfaces, but will return immediately.
217 @param fetch_stale_interfaces: whether to begin downloading interfaces which are present but haven't
218 been checked within the L{freshness} period
219 @type fetch_stale_interfaces: bool
220 @postcondition: L{ready} indicates whether a possible set of implementations was chosen
221 @note: A policy may be ready before all feeds have been downloaded. As new feeds
222 arrive, the chosen versions may change.
225 self.stale_feeds = sets.Set()
226 self.restrictions = {}
227 self.implementation = {}
229 host_arch = arch.get_host_architecture()
230 if self.src:
231 host_arch = arch.SourceArchitecture(host_arch)
232 self.ready = self.solver.solve(self.root, host_arch)
234 for f in self.solver.feeds_used:
235 iface = self.get_interface(f) # May start a download
237 self.implementation = self.solver.selections.copy()
239 if fetch_stale_interfaces and self.network_use != network_offline:
240 for stale in self.stale_feeds:
241 info("Checking for updates to stale feed %s", stale)
242 self.begin_iface_download(stale, False)
244 for w in self.watchers: w()
246 def usable_feeds(self, iface):
247 """Generator for C{iface.feeds} that are valid for our architecture.
248 @rtype: generator
249 @see: L{arch}"""
250 if self.src and iface.uri == self.root:
251 # Note: when feeds are recursive, we'll need a better test for root here
252 machine_ranks = {'src': 1}
253 else:
254 machine_ranks = arch.machine_ranks
256 for f in iface.feeds:
257 if f.os in arch.os_ranks and f.machine in machine_ranks:
258 yield f
259 else:
260 debug("Skipping '%s'; unsupported architecture %s-%s",
261 f, f.os, f.machine)
263 def get_interface(self, uri):
264 """Get an interface from the L{iface_cache}. If it is missing start a new download.
265 If it is present but stale, add it to L{stale_feeds}. This should only be called
266 from L{recalculate}.
267 @see: iface_cache.iface_cache.get_interface
268 @rtype: L{model.Interface}"""
269 iface = iface_cache.get_interface(uri)
271 if uri in iface_cache.pending:
272 # Don't start another download while one is pending
273 # TODO: unless the pending version is very old
274 return iface
276 if not uri.startswith('/'):
277 if iface.last_modified is None:
278 if self.network_use != network_offline:
279 debug("Interface not cached and not off-line. Downloading...")
280 self.begin_iface_download(iface)
281 else:
282 if self._warned_offline:
283 debug("Nothing known about interface, but we are off-line.")
284 else:
285 if iface.feeds:
286 info("Nothing known about interface '%s' and off-line. Trying feeds only.", uri)
287 else:
288 warn("Nothing known about interface '%s', but we are in off-line mode "
289 "(so not fetching).", uri)
290 self._warned_offline = True
291 else:
292 now = time.time()
293 staleness = now - (iface.last_checked or 0)
294 debug("Staleness for %s is %.2f hours", iface, staleness / 3600.0)
296 if self.freshness > 0 and staleness > self.freshness:
297 last_check_attempt = iface_cache.get_last_check_attempt(iface.uri)
298 if last_check_attempt and last_check_attempt > now - FAILED_CHECK_DELAY:
299 debug("Stale, but tried to check recently (%s) so not rechecking now.", time.ctime(last_check_attempt))
300 else:
301 debug("Adding %s to stale set", iface)
302 self.stale_feeds.add(iface)
303 #else: debug("Local interface, so not checking staleness.")
305 return iface
307 def begin_iface_download(self, interface, force = False):
308 """Start downloading the interface, and add a callback to process it when
309 done. If it is already being downloaded, do nothing."""
311 debug("begin_iface_download %s (force = %d)", interface, force)
312 if interface.uri.startswith('/'):
313 return
314 debug("Need to download")
315 dl = self.handler.get_download(interface.uri, force = force)
316 if dl.on_success:
317 # Make sure we don't get called twice
318 raise Exception("Already have a handler for %s; not adding another" % interface)
320 def feed_downloaded(stream):
321 pending = PendingFeed(interface.uri, stream)
322 iface_cache.add_pending(pending)
323 # This will trigger any required confirmations
324 self.process_pending()
326 dl.on_success.append(feed_downloaded)
328 def begin_impl_download(self, impl, retrieval_method, force = False):
329 """Start fetching impl, using retrieval_method. Each download started
330 will call monitor_download."""
331 assert impl
332 assert retrieval_method
334 from zeroinstall.zerostore import manifest
335 alg = impl.id.split('=', 1)[0]
336 if alg not in manifest.algorithms:
337 raise SafeException("Unknown digest algorithm '%s' for '%s' version %s" %
338 (alg, impl.feed.get_name(), impl.get_version()))
340 if isinstance(retrieval_method, DownloadSource):
341 def archive_ready(stream):
342 iface_cache.add_to_cache(retrieval_method, stream)
343 self.begin_archive_download(retrieval_method, success_callback = archive_ready, force = force)
344 elif isinstance(retrieval_method, Recipe):
345 _Cook(self, impl.id, retrieval_method)
346 else:
347 raise Exception("Unknown download type for '%s'" % retrieval_method)
349 def begin_archive_download(self, download_source, success_callback, force = False):
350 """Start fetching an archive. You should normally call L{begin_impl_download}
351 instead, since it handles other kinds of retrieval method too."""
352 from zeroinstall.zerostore import unpack
353 mime_type = download_source.type
354 if not mime_type:
355 mime_type = unpack.type_from_url(download_source.url)
356 if not mime_type:
357 raise SafeException("No 'type' attribute on archive, and I can't guess from the name (%s)" % download_source.url)
358 unpack.check_type_ok(mime_type)
359 dl = self.handler.get_download(download_source.url, force = force)
360 dl.expected_size = download_source.size + (download_source.start_offset or 0)
361 dl.on_success.append(success_callback)
362 return dl
364 def begin_icon_download(self, interface, force = False):
365 """Start downloading an icon for this interface. On success, add it to the
366 icon cache. If the interface has no icon, do nothing."""
367 debug("begin_icon_download %s (force = %d)", interface, force)
369 # Find a suitable icon to download
370 for icon in interface.get_metadata(XMLNS_IFACE, 'icon'):
371 type = icon.getAttribute('type')
372 if type != 'image/png':
373 debug('Skipping non-PNG icon')
374 continue
375 source = icon.getAttribute('href')
376 if source:
377 break
378 warn('Missing "href" attribute on <icon> in %s', interface)
379 else:
380 info('No PNG icons found in %s', interface)
381 return
383 dl = self.handler.get_download(source, force = force)
384 if dl.on_success:
385 # Possibly we should handle this better, but it's unlikely anyone will need
386 # to use an icon as an interface or implementation as well, and some of the code
387 # may assume it's OK keep asking for the same icon to be downloaded.
388 info("Already have a handler for %s; not adding another", source)
389 return
390 dl.on_success.append(lambda stream: self.store_icon(interface, stream))
392 def store_icon(self, interface, stream):
393 """Called when an icon has been successfully downloaded.
394 Subclasses may wish to wrap this to repaint the display."""
395 from zeroinstall.injector import basedir
396 import shutil
397 icons_cache = basedir.save_cache_path(config_site, 'interface_icons')
398 icon_file = file(os.path.join(icons_cache, escape(interface.uri)), 'w')
399 shutil.copyfileobj(stream, icon_file)
401 def get_implementation_path(self, impl):
402 """Return the local path of impl.
403 @rtype: str
404 @raise zeroinstall.zerostore.NotStored: if it needs to be added to the cache first."""
405 assert isinstance(impl, Implementation)
406 if impl.id.startswith('/'):
407 return impl.id
408 return iface_cache.stores.lookup(impl.id)
410 def get_implementation(self, interface):
411 """Get the chosen implementation.
412 @type interface: Interface
413 @rtype: L{model.Implementation}
414 @raise SafeException: if interface has not been fetched or no implementation could be
415 chosen."""
416 assert isinstance(interface, Interface)
418 if not interface.name and not interface.feeds:
419 raise SafeException("We don't have enough information to "
420 "run this program yet. "
421 "Need to download:\n%s" % interface.uri)
422 try:
423 return self.implementation[interface]
424 except KeyError, ex:
425 if interface.implementations:
426 offline = ""
427 if self.network_use == network_offline:
428 offline = "\nThis may be because 'Network Use' is set to Off-line."
429 raise SafeException("No usable implementation found for '%s'.%s" %
430 (interface.name, offline))
431 raise ex
433 def get_cached(self, impl):
434 """Check whether an implementation is available locally.
435 @type impl: model.Implementation
436 @rtype: bool
438 if isinstance(impl, DistributionImplementation):
439 return impl.installed
440 if impl.id.startswith('/'):
441 return os.path.exists(impl.id)
442 else:
443 try:
444 path = self.get_implementation_path(impl)
445 assert path
446 return True
447 except:
448 pass # OK
449 return False
451 def add_to_cache(self, source, data):
452 """Wrapper for L{iface_cache.IfaceCache.add_to_cache}."""
453 iface_cache.add_to_cache(source, data)
455 def get_uncached_implementations(self):
456 """List all chosen implementations which aren't yet available locally.
457 @rtype: [(str, model.Implementation)]"""
458 uncached = []
459 for iface in self.implementation:
460 impl = self.implementation[iface]
461 assert impl, self.implementation
462 if not self.get_cached(impl):
463 uncached.append((iface, impl))
464 return uncached
466 def refresh_all(self, force = True):
467 """Start downloading all feeds for all selected interfaces.
468 @param force: Whether to restart existing downloads."""
469 for x in self.implementation:
470 self.begin_iface_download(x, force)
471 for f in self.usable_feeds(x):
472 feed_iface = iface_cache.get_interface(f.uri)
473 self.begin_iface_download(feed_iface, force)
475 def interface_changed(self, interface):
476 """Callback used by L{iface_cache.IfaceCache.update_interface_from_network}."""
477 debug("interface_changed(%s): recalculating", interface)
478 self.recalculate()
480 def get_feed_targets(self, feed_iface_uri):
481 """Return a list of Interfaces for which feed_iface can be a feed.
482 This is used by B{0launch --feed}.
483 @rtype: [model.Interface]
484 @raise SafeException: If there are no known feeds."""
485 # TODO: what if it isn't cached yet?
486 feed_iface = iface_cache.get_interface(feed_iface_uri)
487 if not feed_iface.feed_for:
488 if not feed_iface.name:
489 raise SafeException("Can't get feed targets for '%s'; failed to load interface." %
490 feed_iface_uri)
491 raise SafeException("Missing <feed-for> element in '%s'; "
492 "this interface can't be used as a feed." % feed_iface_uri)
493 feed_targets = feed_iface.feed_for
494 debug("Feed targets: %s", feed_targets)
495 if not feed_iface.name:
496 warn("Warning: unknown interface '%s'" % feed_iface_uri)
497 return [iface_cache.get_interface(uri) for uri in feed_targets]
499 def get_icon_path(self, iface):
500 """Get an icon for this interface. If the icon is in the cache, use that.
501 If not, start a download. If we already started a download (successful or
502 not) do nothing.
503 @return: The cached icon's path, or None if no icon is currently available.
504 @rtype: str"""
505 path = iface_cache.get_icon_path(iface)
506 if path:
507 return path
509 if self.network_use == network_offline:
510 info("No icon present for %s, but off-line so not downloading", iface)
511 return None
513 self.begin_icon_download(iface)
514 return None
516 def get_best_source(self, impl):
517 """Return the best download source for this implementation.
518 @rtype: L{model.RetrievalMethod}"""
519 if impl.download_sources:
520 return impl.download_sources[0]
521 return None