2 Chooses a set of implementations based on a policy.
5 # Copyright (C) 2007, Thomas Leonard
6 # See the README file for details, or visit http://0install.net.
10 from logging
import info
, debug
, warn
15 from namespaces
import *
18 from zeroinstall
import NeedDownload
19 from zeroinstall
.injector
.iface_cache
import iface_cache
, PendingFeed
20 from zeroinstall
.injector
.trust
import trust_db
22 # If we started a check within this period, don't start another one:
23 FAILED_CHECK_DELAY
= 60 * 60 # 1 Hour
26 """A Cook follows a Recipe."""
27 # Maybe we're taking this metaphor too far?
29 def __init__(self
, policy
, required_digest
, recipe
, force
= False):
30 """Start downloading all the ingredients."""
32 self
.required_digest
= required_digest
33 self
.downloads
= {} # Downloads that are not yet successful
34 self
.streams
= {} # Streams collected from successful downloads
36 # Start a download for each ingredient
37 for step
in recipe
.steps
:
38 dl
= policy
.begin_archive_download(step
, success_callback
=
39 lambda stream
, step
=step
: self
.ingredient_ready(step
, stream
),
41 self
.downloads
[step
] = dl
42 self
.test_done() # Needed for empty recipes
44 # Note: the only references to us are held by the on_success callback
45 # in each Download. On error this is removed, which will cause us
46 # to be destoryed, which will release all the temporary files we hold.
48 def ingredient_ready(self
, step
, stream
):
49 # Called when one archive has been fetched. Store it until the other
51 assert step
not in self
.streams
52 self
.streams
[step
] = stream
53 del self
.downloads
[step
]
57 # On success, a download is removed from here. If empty, it means that
58 # all archives have successfully been downloaded.
59 if self
.downloads
: return
61 from zeroinstall
.zerostore
import unpack
63 # Create an empty directory for the new implementation
64 store
= iface_cache
.stores
.stores
[0]
65 tmpdir
= store
.get_tmp_dir_for(self
.required_digest
)
67 # Unpack each of the downloaded archives into it in turn
68 for step
in self
.recipe
.steps
:
69 unpack
.unpack_archive_over(step
.url
, self
.streams
[step
], tmpdir
, step
.extract
)
70 # Check that the result is correct and store it in the cache
71 store
.check_manifest_and_rename(self
.required_digest
, tmpdir
)
74 # If unpacking fails, remove the temporary directory
75 if tmpdir
is not None:
76 from zeroinstall
import support
77 support
.ro_rmtree(tmpdir
)
80 """Chooses a set of implementations based on a policy.
82 1. Create a Policy object, giving it the URI of the program to be run and a handler.
83 2. Call L{recalculate}. If more information is needed, the handler will be used to download it.
84 3. When all downloads are complete, the L{implementation} map contains the chosen versions.
85 4. Use L{get_uncached_implementations} to find where to get these versions and download them
86 using L{begin_impl_download}.
88 @ivar root: URI of the root interface
89 @ivar implementation: chosen implementations
90 @type implementation: {model.Interface: model.Implementation or None}
91 @ivar watchers: callbacks to invoke after recalculating
92 @ivar help_with_testing: default stability policy
93 @type help_with_testing: bool
94 @ivar network_use: one of the model.network_* values
95 @ivar freshness: seconds allowed since last update
97 @ivar ready: whether L{implementation} is complete enough to run the program
99 @ivar handler: handler for main-loop integration
100 @type handler: L{handler.Handler}
101 @ivar restrictions: Currently known restrictions for each interface.
102 @type restrictions: {model.Interface -> [model.Restriction]}
103 @ivar src: whether we are looking for source code
105 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
106 @type stale_feeds: set
108 __slots__
= ['root', 'implementation', 'watchers',
109 'help_with_testing', 'network_use',
110 'freshness', 'ready', 'handler', '_warned_offline',
111 'restrictions', 'src', 'root_restrictions', 'stale_feeds']
113 def __init__(self
, root
, handler
= None, src
= False):
115 @param root: The URI of the root interface (the program we want to run).
116 @param handler: A handler for main-loop integration.
117 @type handler: L{zeroinstall.injector.handler.Handler}
118 @param src: Whether we are looking for source code.
122 self
.help_with_testing
= False
123 self
.network_use
= network_full
124 self
.freshness
= 60 * 60 * 24 * 30
126 self
.src
= src
# Root impl must be a "src" machine type
127 self
.restrictions
= {}
128 self
.stale_feeds
= sets
.Set()
130 # This is used in is_unusable() to check whether the impl is
131 # for the root interface when looking for source. It is also
132 # used to add restrictions to the root (e.g. --before and --not-before)
133 self
.root_restrictions
= []
135 # If we need to download something but can't because we are offline,
136 # warn the user. But only the first time.
137 self
._warned
_offline
= False
139 # (allow self for backwards compat)
140 self
.handler
= handler
or self
142 debug("Supported systems: '%s'", arch
.os_ranks
)
143 debug("Supported processors: '%s'", arch
.machine_ranks
)
145 path
= basedir
.load_first_config(config_site
, config_prog
, 'global')
148 config
= ConfigParser
.ConfigParser()
150 self
.help_with_testing
= config
.getboolean('global',
152 self
.network_use
= config
.get('global', 'network_use')
153 self
.freshness
= int(config
.get('global', 'freshness'))
154 assert self
.network_use
in network_levels
155 except Exception, ex
:
156 warn("Error loading config: %s", ex
)
160 # Probably need weakrefs here...
161 iface_cache
.add_watcher(self
)
162 trust_db
.watchers
.append(self
.process_pending
)
164 def set_root(self
, root
):
165 """Change the root interface URI."""
166 assert isinstance(root
, (str, unicode))
168 self
.implementation
= {} # Interface -> [Implementation | None]
170 def save_config(self
):
171 """Write global settings."""
172 config
= ConfigParser
.ConfigParser()
173 config
.add_section('global')
175 config
.set('global', 'help_with_testing', self
.help_with_testing
)
176 config
.set('global', 'network_use', self
.network_use
)
177 config
.set('global', 'freshness', self
.freshness
)
179 path
= basedir
.save_config_path(config_site
, config_prog
)
180 path
= os
.path
.join(path
, 'global')
181 config
.write(file(path
+ '.new', 'w'))
182 os
.rename(path
+ '.new', path
)
184 def process_pending(self
):
185 """For each pending feed, either import it fully (if we now
186 trust one of the signatures) or start performing whatever action
187 is needed next (either downloading a key or confirming a
191 # process_pending must never be called from recalculate
193 for pending
in iface_cache
.pending
.values():
194 pending
.begin_key_downloads(self
.handler
, lambda pending
= pending
: self
._keys
_ready
(pending
))
196 def _keys_ready(self
, pending
):
198 iface
= iface_cache
.get_interface(pending
.url
)
199 # Note: this may call recalculate, but it shouldn't do any harm
201 updated
= iface_cache
.update_interface_if_trusted(iface
, pending
.sigs
, pending
.new_xml
)
202 except SafeException
, ex
:
203 self
.handler
.report_error(ex
)
204 # Ignore the problematic new version and continue...
207 self
.handler
.confirm_trust_keys(iface
, pending
.sigs
, pending
.new_xml
)
209 def recalculate(self
, fetch_stale_interfaces
= True):
210 """Try to choose a set of implementations.
211 This may start downloading more interfaces, but will return immediately.
212 @param fetch_stale_interfaces: whether to begin downloading interfaces which are present but haven't
213 been checked within the L{freshness} period
214 @type fetch_stale_interfaces: bool
215 @postcondition: L{ready} indicates whether a possible set of implementations was chosen
216 @note: A policy may be ready before all feeds have been downloaded. As new feeds
217 arrive, the chosen versions may change.
220 self
.stale_feeds
= sets
.Set()
221 self
.restrictions
= {}
222 self
.implementation
= {}
224 debug("Recalculate! root = %s", self
.root
)
226 iface
= self
.get_interface(dep
.interface
)
227 if iface
in self
.implementation
:
228 debug("Interface requested twice; skipping second %s", iface
)
230 warn("Interface requested twice; I've already chosen an implementation "
231 "of '%s' but there are more restrictions! Ignoring the second set.", iface
)
233 self
.implementation
[iface
] = None # Avoid cycles
235 assert iface
not in self
.restrictions
236 self
.restrictions
[iface
] = dep
.restrictions
238 impl
= self
._get
_best
_implementation
(iface
)
240 debug("Will use implementation %s (version %s)", impl
, impl
.get_version())
241 self
.implementation
[iface
] = impl
242 for d
in impl
.requires
:
243 debug("Considering dependency %s", d
)
246 debug("No implementation chould be chosen yet");
248 process(InterfaceDependency(self
.root
, restrictions
= self
.root_restrictions
))
250 if fetch_stale_interfaces
and self
.network_use
!= network_offline
:
251 for stale
in self
.stale_feeds
:
252 info("Checking for updates to stale feed %s", stale
)
253 self
.begin_iface_download(stale
, False)
255 for w
in self
.watchers
: w()
257 # Only to be called from recalculate, as it is quite slow.
258 # Use the results stored in self.implementation instead.
259 def _get_best_implementation(self
, iface
):
260 impls
= iface
.implementations
.values()
261 for f
in self
.usable_feeds(iface
):
262 debug("Processing feed %s", f
)
264 feed_iface
= self
.get_interface(f
.uri
)
265 if feed_iface
.name
and iface
.uri
not in feed_iface
.feed_for
:
266 warn("Missing <feed-for> for '%s' in '%s'",
268 if feed_iface
.implementations
:
269 impls
.extend(feed_iface
.implementations
.values())
270 except NeedDownload
, ex
:
272 except Exception, ex
:
273 warn("Failed to load feed %s for %s: %s",
276 debug("get_best_implementation(%s), with feeds: %s", iface
, iface
.feeds
)
279 info("Interface %s has no implementations!", iface
)
283 if self
.compare(iface
, x
, best
) < 0:
285 unusable
= self
.get_unusable_reason(best
, self
.restrictions
.get(iface
, []))
287 info("Best implementation of %s is %s, but unusable (%s)", iface
, best
, unusable
)
291 def compare(self
, interface
, b
, a
):
292 """Compare a and b to see which would be chosen first.
293 @param interface: The interface we are trying to resolve, which may
294 not be the interface of a or b if they are from feeds.
296 restrictions
= self
.restrictions
.get(interface
, [])
298 a_stab
= a
.get_stability()
299 b_stab
= b
.get_stability()
301 # Usable ones come first
302 r
= cmp(self
.is_unusable(b
, restrictions
), self
.is_unusable(a
, restrictions
))
305 # Preferred versions come first
306 r
= cmp(a_stab
== preferred
, b_stab
== preferred
)
309 if self
.network_use
!= network_full
:
310 r
= cmp(self
.get_cached(a
), self
.get_cached(b
))
314 stab_policy
= interface
.stability_policy
316 if self
.help_with_testing
: stab_policy
= testing
317 else: stab_policy
= stable
319 if a_stab
>= stab_policy
: a_stab
= preferred
320 if b_stab
>= stab_policy
: b_stab
= preferred
322 r
= cmp(a_stab
, b_stab
)
325 # Newer versions come before older ones
326 r
= cmp(a
.version
, b
.version
)
330 r
= cmp(arch
.os_ranks
.get(a
.os
, None),
331 arch
.os_ranks
.get(b
.os
, None))
335 r
= cmp(arch
.machine_ranks
.get(a
.machine
, None),
336 arch
.machine_ranks
.get(b
.machine
, None))
339 # Slightly prefer cached versions
340 if self
.network_use
== network_full
:
341 r
= cmp(self
.get_cached(a
), self
.get_cached(b
))
344 return cmp(a
.id, b
.id)
346 def usable_feeds(self
, iface
):
347 """Generator for C{iface.feeds} that are valid for our architecture.
350 if self
.src
and iface
.uri
== self
.root
:
351 # Note: when feeds are recursive, we'll need a better test for root here
352 machine_ranks
= {'src': 1}
354 machine_ranks
= arch
.machine_ranks
356 for f
in iface
.feeds
:
357 if f
.os
in arch
.os_ranks
and f
.machine
in machine_ranks
:
360 debug("Skipping '%s'; unsupported architecture %s-%s",
363 def get_ranked_implementations(self
, iface
):
364 """Get all implementations from all feeds, in order.
365 @type iface: Interface
366 @return: a sorted list of implementations.
367 @rtype: [model.Implementation]"""
368 impls
= iface
.implementations
.values()
369 for f
in self
.usable_feeds(iface
):
370 feed_iface
= iface_cache
.get_interface(f
.uri
)
371 if feed_iface
.implementations
:
372 impls
.extend(feed_iface
.implementations
.values())
373 impls
.sort(lambda a
, b
: self
.compare(iface
, a
, b
))
376 def is_unusable(self
, impl
, restrictions
= []):
377 """@return: whether this implementation is unusable.
379 return self
.get_unusable_reason(impl
, restrictions
) != None
381 def get_unusable_reason(self
, impl
, restrictions
= []):
383 @param impl: Implementation to test.
384 @type restrictions: [L{model.Restriction}]
385 @return: The reason why this impl is unusable, or None if it's OK.
387 @note: The restrictions are for the interface being requested, not the interface
388 of the implementation; they may be different when feeds are being used."""
389 for r
in restrictions
:
390 if not r
.meets_restriction(impl
):
391 return "Incompatible with another selected implementation"
392 stability
= impl
.get_stability()
393 if stability
<= buggy
:
394 return stability
.name
395 if self
.network_use
== network_offline
and not self
.get_cached(impl
):
396 return "Not cached and we are off-line"
397 if impl
.os
not in arch
.os_ranks
:
398 return "Unsupported OS"
399 # When looking for source code, we need to known if we're
400 # looking at an implementation of the root interface, even if
401 # it's from a feed, hence the sneaky restrictions identity check.
402 if self
.src
and restrictions
is self
.root_restrictions
:
403 if impl
.machine
!= 'src':
404 return "Not source code"
406 if impl
.machine
not in arch
.machine_ranks
:
407 if impl
.machine
== 'src':
409 return "Unsupported machine type"
412 def get_interface(self
, uri
):
413 """Get an interface from the L{iface_cache}. If it is missing start a new download.
414 If it is present but stale, add it to L{stale_feeds}. This should only be called
416 @see: iface_cache.iface_cache.get_interface
417 @rtype: L{model.Interface}"""
418 iface
= iface_cache
.get_interface(uri
)
420 if uri
in iface_cache
.pending
:
421 # Don't start another download while one is pending
422 # TODO: unless the pending version is very old
425 if iface
.last_modified
is None:
426 if self
.network_use
!= network_offline
:
427 debug("Interface not cached and not off-line. Downloading...")
428 self
.begin_iface_download(iface
)
430 if self
._warned
_offline
:
431 debug("Nothing known about interface, but we are off-line.")
434 info("Nothing known about interface '%s' and off-line. Trying feeds only.", uri
)
436 warn("Nothing known about interface '%s', but we are in off-line mode "
437 "(so not fetching).", uri
)
438 self
._warned
_offline
= True
439 elif not uri
.startswith('/'):
441 staleness
= now
- (iface
.last_checked
or 0)
442 debug("Staleness for %s is %.2f hours", iface
, staleness
/ 3600.0)
444 if self
.freshness
> 0 and staleness
> self
.freshness
:
445 if iface
.last_check_attempt
and iface
.last_check_attempt
> now
- FAILED_CHECK_DELAY
:
446 debug("Stale, but tried to check recently (%s) so not rechecking now.", time
.ctime(iface
.last_check_attempt
))
448 debug("Adding %s to stale set", iface
)
449 self
.stale_feeds
.add(iface
)
450 #else: debug("Local interface, so not checking staleness.")
454 def begin_iface_download(self
, interface
, force
= False):
455 """Start downloading the interface, and add a callback to process it when
456 done. If it is already being downloaded, do nothing."""
458 debug("begin_iface_download %s (force = %d)", interface
, force
)
459 if interface
.uri
.startswith('/'):
461 debug("Need to download")
462 dl
= self
.handler
.get_download(interface
.uri
, force
= force
)
464 # Possibly we should handle this better, but it's unlikely anyone will need
465 # to use an interface as an icon or implementation as well, and some of the code
466 # assumes it's OK keep asking for the same interface to be downloaded.
467 debug("Already have a handler for %s; not adding another", interface
)
470 def feed_downloaded(stream
):
471 pending
= PendingFeed(interface
.uri
, stream
)
472 iface_cache
.add_pending(pending
)
473 # This will trigger any required confirmations
474 self
.process_pending()
476 dl
.on_success
.append(feed_downloaded
)
478 def begin_impl_download(self
, impl
, retrieval_method
, force
= False):
479 """Start fetching impl, using retrieval_method. Each download started
480 will call monitor_download."""
482 assert retrieval_method
484 from zeroinstall
.zerostore
import manifest
485 alg
= impl
.id.split('=', 1)[0]
486 if alg
not in manifest
.algorithms
:
487 raise SafeException("Unknown digest algorithm '%s' for '%s' version %s" %
488 (alg
, impl
.interface
.get_name(), impl
.get_version()))
490 if isinstance(retrieval_method
, DownloadSource
):
491 def archive_ready(stream
):
492 iface_cache
.add_to_cache(retrieval_method
, stream
)
493 self
.begin_archive_download(retrieval_method
, success_callback
= archive_ready
, force
= force
)
494 elif isinstance(retrieval_method
, Recipe
):
495 _Cook(self
, impl
.id, retrieval_method
)
497 raise Exception("Unknown download type for '%s'" % retrieval_method
)
499 def begin_archive_download(self
, download_source
, success_callback
, force
= False):
500 """Start fetching an archive. You should normally call L{begin_impl_download}
501 instead, since it handles other kinds of retrieval method too."""
502 from zeroinstall
.zerostore
import unpack
503 mime_type
= download_source
.type
505 mime_type
= unpack
.type_from_url(download_source
.url
)
507 raise SafeException("No 'type' attribute on archive, and I can't guess from the name (%s)" % download_source
.url
)
508 unpack
.check_type_ok(mime_type
)
509 dl
= self
.handler
.get_download(download_source
.url
, force
= force
)
510 dl
.expected_size
= download_source
.size
+ (download_source
.start_offset
or 0)
511 dl
.on_success
.append(success_callback
)
514 def begin_icon_download(self
, interface
, force
= False):
515 """Start downloading an icon for this interface. On success, add it to the
516 icon cache. If the interface has no icon, do nothing."""
517 debug("begin_icon_download %s (force = %d)", interface
, force
)
519 # Find a suitable icon to download
520 for icon
in interface
.get_metadata(XMLNS_IFACE
, 'icon'):
521 type = icon
.getAttribute('type')
522 if type != 'image/png':
523 debug('Skipping non-PNG icon')
525 source
= icon
.getAttribute('href')
528 warn('Missing "href" attribute on <icon> in %s', interface
)
530 info('No PNG icons found in %s', interface
)
533 dl
= self
.handler
.get_download(source
, force
= force
)
535 # Possibly we should handle this better, but it's unlikely anyone will need
536 # to use an icon as an interface or implementation as well, and some of the code
537 # may assume it's OK keep asking for the same icon to be downloaded.
538 info("Already have a handler for %s; not adding another", source
)
540 dl
.on_success
.append(lambda stream
: self
.store_icon(interface
, stream
))
542 def store_icon(self
, interface
, stream
):
543 """Called when an icon has been successfully downloaded.
544 Subclasses may wish to wrap this to repaint the display."""
545 from zeroinstall
.injector
import basedir
547 icons_cache
= basedir
.save_cache_path(config_site
, 'interface_icons')
548 icon_file
= file(os
.path
.join(icons_cache
, escape(interface
.uri
)), 'w')
549 shutil
.copyfileobj(stream
, icon_file
)
551 def get_implementation_path(self
, impl
):
552 """Return the local path of impl.
554 @raise zeroinstall.zerostore.NotStored: if it needs to be added to the cache first."""
555 assert isinstance(impl
, Implementation
)
556 if impl
.id.startswith('/'):
558 return iface_cache
.stores
.lookup(impl
.id)
560 def get_implementation(self
, interface
):
561 """Get the chosen implementation.
562 @type interface: Interface
563 @rtype: L{model.Implementation}
564 @raise SafeException: if interface has not been fetched or no implementation could be
566 assert isinstance(interface
, Interface
)
568 if not interface
.name
and not interface
.feeds
:
569 raise SafeException("We don't have enough information to "
570 "run this program yet. "
571 "Need to download:\n%s" % interface
.uri
)
573 return self
.implementation
[interface
]
575 if interface
.implementations
:
577 if self
.network_use
== network_offline
:
578 offline
= "\nThis may be because 'Network Use' is set to Off-line."
579 raise SafeException("No usable implementation found for '%s'.%s" %
580 (interface
.name
, offline
))
583 def walk_interfaces(self
):
584 """@deprecated: use L{implementation} instead"""
585 return iter(self
.implementation
)
587 def get_cached(self
, impl
):
588 """Check whether an implementation is available locally.
589 @type impl: model.Implementation
592 if isinstance(impl
, DistributionImplementation
):
593 return impl
.installed
594 if impl
.id.startswith('/'):
595 return os
.path
.exists(impl
.id)
598 path
= self
.get_implementation_path(impl
)
605 def add_to_cache(self
, source
, data
):
606 """Wrapper for L{iface_cache.IfaceCache.add_to_cache}."""
607 iface_cache
.add_to_cache(source
, data
)
609 def get_uncached_implementations(self
):
610 """List all chosen implementations which aren't yet available locally.
611 @rtype: [(str, model.Implementation)]"""
613 for iface
in self
.implementation
:
614 impl
= self
.implementation
[iface
]
616 if not self
.get_cached(impl
):
617 uncached
.append((iface
, impl
))
620 def refresh_all(self
, force
= True):
621 """Start downloading all feeds for all selected interfaces.
622 @param force: Whether to restart existing downloads."""
623 for x
in self
.implementation
:
624 self
.begin_iface_download(x
, force
)
625 for f
in self
.usable_feeds(x
):
626 feed_iface
= iface_cache
.get_interface(f
.uri
)
627 self
.begin_iface_download(feed_iface
, force
)
629 def interface_changed(self
, interface
):
630 """Callback used by L{iface_cache.IfaceCache.update_interface_from_network}."""
631 debug("interface_changed(%s): recalculating", interface
)
634 def get_feed_targets(self
, feed_iface_uri
):
635 """Return a list of Interfaces for which feed_iface can be a feed.
636 This is used by B{0launch --feed}.
637 @rtype: [model.Interface]
638 @raise SafeException: If there are no known feeds."""
639 # TODO: what if it isn't cached yet?
640 feed_iface
= iface_cache
.get_interface(feed_iface_uri
)
641 if not feed_iface
.feed_for
:
642 if not feed_iface
.name
:
643 raise SafeException("Can't get feed targets for '%s'; failed to load interface." %
645 raise SafeException("Missing <feed-for> element in '%s'; "
646 "this interface can't be used as a feed." % feed_iface_uri
)
647 feed_targets
= feed_iface
.feed_for
648 debug("Feed targets: %s", feed_targets
)
649 if not feed_iface
.name
:
650 warn("Warning: unknown interface '%s'" % feed_iface_uri
)
651 return [iface_cache
.get_interface(uri
) for uri
in feed_targets
]
653 def get_icon_path(self
, iface
):
654 """Get an icon for this interface. If the icon is in the cache, use that.
655 If not, start a download. If we already started a download (successful or
657 @return: The cached icon's path, or None if no icon is currently available.
659 path
= iface_cache
.get_icon_path(iface
)
663 if self
.network_use
== network_offline
:
664 info("No icon present for %s, but off-line so not downloading", iface
)
667 self
.begin_icon_download(iface
)
670 def get_best_source(self
, impl
):
671 """Return the best download source for this implementation.
672 @rtype: L{model.RetrievalMethod}"""
673 if impl
.download_sources
:
674 return impl
.download_sources
[0]