2 Chooses a set of implementations based on a policy.
4 @deprecated: see L{solver}
7 # Copyright (C) 2007, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
12 from logging
import info
, debug
, warn
17 from namespaces
import *
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
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."""
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
),
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
52 assert step
not in self
.streams
53 self
.streams
[step
] = stream
54 del self
.downloads
[step
]
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
)
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
)
75 # If unpacking fails, remove the temporary directory
76 if tmpdir
is not None:
77 from zeroinstall
import support
78 support
.ro_rmtree(tmpdir
)
81 """Chooses a set of implementations based on a policy.
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
98 @ivar ready: whether L{implementation} is complete enough to run the program
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
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.
131 self
.freshness
= 60 * 60 * 24 * 30
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')
153 config
= ConfigParser
.ConfigParser()
155 self
.solver
.help_with_testing
= config
.getboolean('global',
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
)
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))
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
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
):
203 iface
= iface_cache
.get_interface(pending
.url
)
204 # Note: this may call recalculate, but it shouldn't do any harm
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...
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()
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.
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}
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
:
260 debug("Skipping '%s'; unsupported architecture %s-%s",
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
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
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
)
282 if self
._warned
_offline
:
283 debug("Nothing known about interface, but we are off-line.")
286 info("Nothing known about interface '%s' and off-line. Trying feeds only.", uri
)
288 warn("Nothing known about interface '%s', but we are in off-line mode "
289 "(so not fetching).", uri
)
290 self
._warned
_offline
= True
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
))
301 debug("Adding %s to stale set", iface
)
302 self
.stale_feeds
.add(iface
)
303 #else: debug("Local interface, so not checking staleness.")
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('/'):
314 debug("Need to download")
315 dl
= self
.handler
.get_download(interface
.uri
, force
= force
)
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."""
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
)
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
355 mime_type
= unpack
.type_from_url(download_source
.url
)
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
)
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')
375 source
= icon
.getAttribute('href')
378 warn('Missing "href" attribute on <icon> in %s', interface
)
380 info('No PNG icons found in %s', interface
)
383 dl
= self
.handler
.get_download(source
, force
= force
)
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
)
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
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.
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('/'):
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
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
)
423 return self
.implementation
[interface
]
425 if interface
.implementations
:
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
))
433 def get_cached(self
, impl
):
434 """Check whether an implementation is available locally.
435 @type impl: model.Implementation
438 if isinstance(impl
, DistributionImplementation
):
439 return impl
.installed
440 if impl
.id.startswith('/'):
441 return os
.path
.exists(impl
.id)
444 path
= self
.get_implementation_path(impl
)
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)]"""
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
))
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
)
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." %
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
503 @return: The cached icon's path, or None if no icon is currently available.
505 path
= iface_cache
.get_icon_path(iface
)
509 if self
.network_use
== network_offline
:
510 info("No icon present for %s, but off-line so not downloading", iface
)
513 self
.begin_icon_download(iface
)
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]