Don't create .desktop filenames with spaces; xdg-desktop-menu gets confused
[zeroinstall/zeroinstall-mseaborn.git] / zeroinstall / injector / policy.py
blobe91f937b3803240b3a285277c4719b25dbaa37f5
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) 2008, Thomas Leonard
8 # See the README file for details, or visit http://0install.net.
10 import time
11 import sys, os
12 from logging import info, debug, warn
13 import arch
15 from model import *
16 from namespaces import *
17 import ConfigParser
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
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: [(str, 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 # Once we've starting downloading some things,
329 # we might as well get them all.
330 force = True
332 if not self.network_use == network_offline:
333 for f in self.solver.feeds_used:
334 if f in downloads_finished or f in downloads_in_progress:
335 continue
336 if f.startswith('/'):
337 continue
338 feed = iface_cache.get_interface(f)
339 downloads_in_progress[f] = self.fetcher.download_and_import_feed(f, iface_cache)
341 if not downloads_in_progress:
342 break
344 blockers = downloads_in_progress.values()
345 yield blockers
346 tasks.check(blockers, self.handler.report_error)
348 for f in downloads_in_progress.keys():
349 if downloads_in_progress[f].happened:
350 del downloads_in_progress[f]
351 downloads_finished.add(f)
353 def need_download(self):
354 """Decide whether we need to download anything (but don't do it!)
355 @return: true if we MUST download something (feeds or implementations)
356 @rtype: bool"""
357 host_arch = self.target_arch
358 if self.src:
359 host_arch = arch.SourceArchitecture(host_arch)
360 self.solver.solve(self.root, host_arch)
361 for w in self.watchers: w()
363 if not self.solver.ready:
364 return True # Maybe a newer version will work?
366 if self.get_uncached_implementations():
367 return True
369 return False
371 def download_uncached_implementations(self):
372 """Download all implementations chosen by the solver that are missing from the cache."""
373 assert self.solver.ready, "Solver is not ready!\n%s" % self.solver.selections
374 return self.fetcher.download_impls([impl for impl in self.solver.selections.values() if not self.get_cached(impl)],
375 iface_cache.stores)
377 def download_icon(self, interface, force = False):
378 """Download an icon for this interface and add it to the
379 icon cache. If the interface has no icon or we are offline, do nothing.
380 @return: the task doing the import, or None
381 @rtype: L{tasks.Task}"""
382 debug("download_icon %s (force = %d)", interface, force)
384 if self.network_use == network_offline:
385 info("No icon present for %s, but off-line so not downloading", interface)
386 return
388 return self.fetcher.download_icon(interface, force)
390 def get_interface(self, uri):
391 """@deprecated: use L{iface_cache.IfaceCache.get_interface} instead"""
392 import warnings
393 warnings.warn("Policy.get_interface is deprecated!", DeprecationWarning, stacklevel = 2)
394 return iface_cache.get_interface(uri)