2 Chooses a set of implementations based on a policy.
4 @deprecated: see L{solver}
7 # Copyright (C) 2008, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
12 from logging
import info
, debug
, warn
16 from namespaces
import *
18 from zeroinstall
.support
import tasks
, basedir
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 """Chooses a set of implementations based on a policy.
28 1. Create a Policy object, giving it the URI of the program to be run and a handler.
29 2. Call L{recalculate}. If more information is needed, the handler will be used to download it.
30 3. When all downloads are complete, the L{implementation} map contains the chosen versions.
31 4. Use L{get_uncached_implementations} to find where to get these versions and download them
32 using L{begin_impl_download}.
34 @ivar root: URI of the root interface
35 @ivar implementation: chosen implementations
36 @type implementation: {model.Interface: model.Implementation or None}
37 @ivar watchers: callbacks to invoke after recalculating
38 @ivar help_with_testing: default stability policy
39 @type help_with_testing: bool
40 @ivar network_use: one of the model.network_* values
41 @ivar freshness: seconds allowed since last update
43 @ivar ready: whether L{implementation} is complete enough to run the program
45 @ivar handler: handler for main-loop integration
46 @type handler: L{handler.Handler}
47 @ivar src: whether we are looking for source code
49 @ivar stale_feeds: set of feeds which are present but haven't been checked for a long time
50 @type stale_feeds: set
52 __slots__
= ['root', 'watchers',
53 'freshness', 'handler', '_warned_offline',
54 '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 root_restrictions
= property(lambda self
: self
.solver
.root_restrictions
,
63 lambda self
, value
: setattr(self
.solver
, 'root_restrictions', value
))
65 implementation
= property(lambda self
: self
.solver
.selections
)
67 ready
= property(lambda self
: self
.solver
.ready
)
69 def __init__(self
, root
, handler
= None, src
= False):
71 @param root: The URI of the root interface (the program we want to run).
72 @param handler: A handler for main-loop integration.
73 @type handler: L{zeroinstall.injector.handler.Handler}
74 @param src: Whether we are looking for source code.
78 self
.freshness
= 60 * 60 * 24 * 30
79 self
.src
= src
# Root impl must be a "src" machine type
80 self
.stale_feeds
= sets
.Set()
82 from zeroinstall
.injector
.solver
import DefaultSolver
83 self
.solver
= DefaultSolver(network_full
, iface_cache
, iface_cache
.stores
, root_restrictions
= [])
85 # If we need to download something but can't because we are offline,
86 # warn the user. But only the first time.
87 self
._warned
_offline
= False
90 # (allow self for backwards compat)
91 self
.handler
= handler
or self
93 debug("Supported systems: '%s'", arch
.os_ranks
)
94 debug("Supported processors: '%s'", arch
.machine_ranks
)
96 path
= basedir
.load_first_config(config_site
, config_prog
, 'global')
99 config
= ConfigParser
.ConfigParser()
101 self
.solver
.help_with_testing
= config
.getboolean('global',
103 self
.solver
.network_use
= config
.get('global', 'network_use')
104 self
.freshness
= int(config
.get('global', 'freshness'))
105 assert self
.solver
.network_use
in network_levels
106 except Exception, ex
:
107 warn("Error loading config: %s", ex
)
111 # Probably need weakrefs here...
112 iface_cache
.add_watcher(self
)
116 if not self
._fetcher
:
118 self
._fetcher
= fetch
.Fetcher(self
.handler
)
121 def set_root(self
, root
):
122 """Change the root interface URI."""
123 assert isinstance(root
, (str, unicode))
125 for w
in self
.watchers
: w()
127 def save_config(self
):
128 """Write global settings."""
129 config
= ConfigParser
.ConfigParser()
130 config
.add_section('global')
132 config
.set('global', 'help_with_testing', self
.help_with_testing
)
133 config
.set('global', 'network_use', self
.network_use
)
134 config
.set('global', 'freshness', self
.freshness
)
136 path
= basedir
.save_config_path(config_site
, config_prog
)
137 path
= os
.path
.join(path
, 'global')
138 config
.write(file(path
+ '.new', 'w'))
139 os
.rename(path
+ '.new', path
)
141 def recalculate(self
, fetch_stale_interfaces
= True):
143 @see: L{solve_with_downloads}
146 self
.stale_feeds
= sets
.Set()
148 host_arch
= arch
.get_host_architecture()
150 host_arch
= arch
.SourceArchitecture(host_arch
)
151 self
.solver
.solve(self
.root
, host_arch
)
153 if self
.network_use
== network_offline
:
154 fetch_stale_interfaces
= False
157 for f
in self
.solver
.feeds_used
:
158 if f
.startswith('/'): continue
159 feed
= iface_cache
.get_feed(f
)
160 if feed
is None or feed
.last_modified
is None:
161 self
.download_and_import_feed_if_online(f
) # Will start a download
162 elif self
.is_stale(feed
):
163 debug("Adding %s to stale set", f
)
164 self
.stale_feeds
.add(iface_cache
.get_interface(f
)) # Legacy API
165 if fetch_stale_interfaces
:
166 self
.download_and_import_feed_if_online(f
) # Will start a download
168 for w
in self
.watchers
: w()
172 def usable_feeds(self
, iface
):
173 """Generator for C{iface.feeds} that are valid for our architecture.
176 if self
.src
and iface
.uri
== self
.root
:
177 # Note: when feeds are recursive, we'll need a better test for root here
178 machine_ranks
= {'src': 1}
180 machine_ranks
= arch
.machine_ranks
182 for f
in iface
.feeds
:
183 if f
.os
in arch
.os_ranks
and f
.machine
in machine_ranks
:
186 debug("Skipping '%s'; unsupported architecture %s-%s",
189 def is_stale(self
, feed
):
190 """Check whether feed needs updating, based on the configured L{freshness}.
191 None is considered to be stale.
192 @return: true if feed is stale or missing."""
195 if feed
.url
.startswith('/'):
196 return False # Local feeds are never stale
197 if feed
.last_modified
is None:
198 return True # Don't even have it yet
200 staleness
= now
- (feed
.last_checked
or 0)
201 debug("Staleness for %s is %.2f hours", feed
, staleness
/ 3600.0)
203 if self
.freshness
== 0 or staleness
< self
.freshness
:
204 return False # Fresh enough for us
206 last_check_attempt
= iface_cache
.get_last_check_attempt(feed
.url
)
207 if last_check_attempt
and last_check_attempt
> now
- FAILED_CHECK_DELAY
:
208 debug("Stale, but tried to check recently (%s) so not rechecking now.", time
.ctime(last_check_attempt
))
213 def download_and_import_feed_if_online(self
, feed_url
):
214 """If we're online, call L{download_and_import_feed}. Otherwise, log a suitable warning."""
215 if self
.network_use
!= network_offline
:
216 debug("Feed %s not cached and not off-line. Downloading...", feed_url
)
217 return self
.fetcher
.download_and_import_feed(feed_url
, iface_cache
)
219 if self
._warned
_offline
:
220 debug("Not downloading feed '%s' because we are off-line.", feed_url
)
221 elif feed_url
== injector_gui_uri
:
222 # Don't print a warning, because we always switch to off-line mode to
223 # run the GUI the first time.
224 info("Not downloading GUI feed '%s' because we are in off-line mode.", feed_url
)
226 warn("Not downloading feed '%s' because we are in off-line mode.", feed_url
)
227 self
._warned
_offline
= True
229 def get_implementation_path(self
, impl
):
230 """Return the local path of impl.
232 @raise zeroinstall.zerostore.NotStored: if it needs to be added to the cache first."""
233 assert isinstance(impl
, Implementation
)
234 if impl
.id.startswith('/'):
236 return iface_cache
.stores
.lookup(impl
.id)
238 def get_implementation(self
, interface
):
239 """Get the chosen implementation.
240 @type interface: Interface
241 @rtype: L{model.Implementation}
242 @raise SafeException: if interface has not been fetched or no implementation could be
244 assert isinstance(interface
, Interface
)
246 if not interface
.name
and not interface
.feeds
:
247 raise SafeException("We don't have enough information to "
248 "run this program yet. "
249 "Need to download:\n%s" % interface
.uri
)
251 return self
.implementation
[interface
]
253 if interface
.implementations
:
255 if self
.network_use
== network_offline
:
256 offline
= "\nThis may be because 'Network Use' is set to Off-line."
257 raise SafeException("No usable implementation found for '%s'.%s" %
258 (interface
.name
, offline
))
261 def get_cached(self
, impl
):
262 """Check whether an implementation is available locally.
263 @type impl: model.Implementation
266 if isinstance(impl
, DistributionImplementation
):
267 return impl
.installed
268 if impl
.id.startswith('/'):
269 return os
.path
.exists(impl
.id)
272 path
= self
.get_implementation_path(impl
)
279 def add_to_cache(self
, source
, data
):
280 """Wrapper for L{iface_cache.IfaceCache.add_to_cache}."""
281 iface_cache
.add_to_cache(source
, data
)
283 def get_uncached_implementations(self
):
284 """List all chosen implementations which aren't yet available locally.
285 @rtype: [(str, model.Implementation)]"""
287 for iface
in self
.solver
.selections
:
288 impl
= self
.solver
.selections
[iface
]
289 assert impl
, self
.solver
.selections
290 if not self
.get_cached(impl
):
291 uncached
.append((iface
, impl
))
294 def refresh_all(self
, force
= True):
295 """Start downloading all feeds for all selected interfaces.
296 @param force: Whether to restart existing downloads."""
297 return self
.solve_with_downloads(force
= True)
299 def get_feed_targets(self
, feed_iface_uri
):
300 """Return a list of Interfaces for which feed_iface can be a feed.
301 This is used by B{0launch --feed}.
302 @rtype: [model.Interface]
303 @raise SafeException: If there are no known feeds."""
304 # TODO: what if it isn't cached yet?
305 feed_iface
= iface_cache
.get_interface(feed_iface_uri
)
306 if not feed_iface
.feed_for
:
307 if not feed_iface
.name
:
308 raise SafeException("Can't get feed targets for '%s'; failed to load interface." %
310 raise SafeException("Missing <feed-for> element in '%s'; "
311 "this interface can't be used as a feed." % feed_iface_uri
)
312 feed_targets
= feed_iface
.feed_for
313 debug("Feed targets: %s", feed_targets
)
314 if not feed_iface
.name
:
315 warn("Warning: unknown interface '%s'" % feed_iface_uri
)
316 return [iface_cache
.get_interface(uri
) for uri
in feed_targets
]
319 def solve_with_downloads(self
, force
= False):
320 """Run the solver, then download any feeds that are missing or
321 that need to be updated. Each time a new feed is imported into
322 the cache, the solver is run again, possibly adding new downloads.
323 @param force: whether to download even if we're already ready to run."""
325 downloads_finished
= set() # Successful or otherwise
326 downloads_in_progress
= {} # URL -> Download
328 host_arch
= arch
.get_host_architecture()
330 host_arch
= arch
.SourceArchitecture(host_arch
)
333 self
.solver
.solve(self
.root
, host_arch
)
334 for w
in self
.watchers
: w()
336 if self
.solver
.ready
and not force
:
339 # Once we've starting downloading some things,
340 # we might as well get them all.
343 if not self
.network_use
== network_offline
:
344 for f
in self
.solver
.feeds_used
:
345 if f
in downloads_finished
or f
in downloads_in_progress
:
347 if f
.startswith('/'):
349 feed
= iface_cache
.get_interface(f
)
350 downloads_in_progress
[f
] = self
.fetcher
.download_and_import_feed(f
, iface_cache
)
352 if not downloads_in_progress
:
355 blockers
= downloads_in_progress
.values()
357 tasks
.check(blockers
)
359 for f
in downloads_in_progress
.keys():
360 if downloads_in_progress
[f
].happened
:
361 del downloads_in_progress
[f
]
362 downloads_finished
.add(f
)
364 def need_download(self
):
365 """Decide whether we need to download anything (but don't do it!)
366 @return: true if we MUST download something (feeds or implementations)
368 host_arch
= arch
.get_host_architecture()
370 host_arch
= arch
.SourceArchitecture(host_arch
)
371 self
.solver
.solve(self
.root
, host_arch
)
372 for w
in self
.watchers
: w()
374 if not self
.solver
.ready
:
375 return True # Maybe a newer version will work?
377 if self
.get_uncached_implementations():
382 def download_uncached_implementations(self
):
383 """Download all implementations chosen by the solver that are missing from the cache."""
384 assert self
.solver
.ready
, "Solver is not ready!\n%s" % self
.solver
.selections
385 return self
.fetcher
.download_impls([impl
for impl
in self
.solver
.selections
.values() if not self
.get_cached(impl
)],
388 def download_icon(self
, interface
, force
= False):
389 """Download an icon for this interface and add it to the
390 icon cache. If the interface has no icon or we are offline, do nothing.
391 @return: the task doing the import, or None
392 @rtype: L{tasks.Task}"""
393 debug("download_icon %s (force = %d)", interface
, force
)
395 if self
.network_use
== network_offline
:
396 info("No icon present for %s, but off-line so not downloading", interface
)
399 return self
.fetcher
.download_icon(interface
, force
)