Clarified copyrights.
[zeroinstall.git] / zeroinstall / injector / policy.py
blob47860b47721e6a734c2ed03ebf067ef3ec9090eb
1 # Copyright (C) 2006, Thomas Leonard
2 # See the README file for details, or visit http://0install.net.
4 import time
5 import sys
6 from logging import info, debug, warn
7 from cStringIO import StringIO
8 import arch
10 from model import *
11 import basedir
12 from namespaces import *
13 import ConfigParser
14 import reader
15 import download
16 from iface_cache import iface_cache
17 from zeroinstall import NeedDownload
19 class Policy(object):
20 __slots__ = ['root', 'implementation', 'watchers',
21 'help_with_testing', 'network_use',
22 'freshness', 'ready', 'handler', 'warned_offline']
24 def __init__(self, root, handler = None):
25 self.watchers = []
26 self.help_with_testing = False
27 self.network_use = network_full
28 self.freshness = 60 * 60 * 24 * 30 # Seconds allowed since last update (1 month)
29 self.ready = False
31 # If we need to download something but can't because we are offline,
32 # warn the user. But only the first time.
33 self.warned_offline = False
35 # (allow self for backwards compat)
36 self.handler = handler or self
38 debug("Supported systems: '%s'", arch.os_ranks)
39 debug("Supported processors: '%s'", arch.machine_ranks)
41 path = basedir.load_first_config(config_site, config_prog, 'global')
42 if path:
43 try:
44 config = ConfigParser.ConfigParser()
45 config.read(path)
46 self.help_with_testing = config.getboolean('global',
47 'help_with_testing')
48 self.network_use = config.get('global', 'network_use')
49 self.freshness = int(config.get('global', 'freshness'))
50 assert self.network_use in network_levels
51 except Exception, ex:
52 print >>sys.stderr, "Error loading config:", ex
54 self.set_root(root)
56 iface_cache.add_watcher(self)
58 def set_root(self, root):
59 assert isinstance(root, (str, unicode))
60 self.root = root
61 self.implementation = {} # Interface -> [Implementation | None]
63 def save_config(self):
64 config = ConfigParser.ConfigParser()
65 config.add_section('global')
67 config.set('global', 'help_with_testing', self.help_with_testing)
68 config.set('global', 'network_use', self.network_use)
69 config.set('global', 'freshness', self.freshness)
71 path = basedir.save_config_path(config_site, config_prog)
72 path = os.path.join(path, 'global')
73 config.write(file(path + '.new', 'w'))
74 os.rename(path + '.new', path)
76 def recalculate(self):
77 self.implementation = {}
78 self.ready = True
79 debug("Recalculate! root = %s", self.root)
80 def process(iface):
81 if iface in self.implementation:
82 debug("cycle; skipping second %s", iface)
83 return
84 self.implementation[iface] = None # Avoid cycles
86 impl = self._get_best_implementation(iface)
87 if impl:
88 debug("Will use implementation %s (version %s)", impl, impl.get_version())
89 self.implementation[iface] = impl
90 for d in impl.dependencies.values():
91 debug("Considering dependency %s", d)
92 process(self.get_interface(d.interface))
93 else:
94 debug("No implementation chould be chosen yet");
95 self.ready = False
96 process(self.get_interface(self.root))
97 for w in self.watchers: w()
99 # Only to be called from recalculate, as it is quite slow.
100 # Use the results stored in self.implementation instead.
101 def _get_best_implementation(self, iface):
102 impls = iface.implementations.values()
103 for f in self.usable_feeds(iface):
104 debug("Processing feed %s", f)
105 try:
106 feed_iface = self.get_interface(f.uri)
107 if feed_iface.name and iface.uri not in feed_iface.feed_for:
108 warn("Missing <feed-for> for '%s' in '%s'",
109 iface.uri, f.uri)
110 if feed_iface.implementations:
111 impls.extend(feed_iface.implementations.values())
112 except NeedDownload, ex:
113 raise ex
114 except Exception, ex:
115 warn("Failed to load feed %s for %s: %s",
116 f, iface, str(ex))
118 debug("get_best_implementation(%s), with feeds: %s", iface, iface.feeds)
120 if not impls:
121 info("Interface %s has no implementations!", iface)
122 return None
123 best = impls[0]
124 for x in impls[1:]:
125 if self.compare(iface, x, best) < 0:
126 best = x
127 if self.is_unusable(best):
128 info("Best implementation of %s is %s, but unusable (%s)", iface, best,
129 self.get_unusable_reason(best))
130 return None
131 return best
133 def compare(self, interface, b, a):
134 a_stab = a.get_stability()
135 b_stab = b.get_stability()
137 # Usable ones come first
138 r = cmp(self.is_unusable(b), self.is_unusable(a))
139 if r: return r
141 # Preferred versions come first
142 r = cmp(a_stab == preferred, b_stab == preferred)
143 if r: return r
145 if self.network_use != network_full:
146 r = cmp(self.get_cached(a), self.get_cached(b))
147 if r: return r
149 # Stability
150 stab_policy = interface.stability_policy
151 if not stab_policy:
152 if self.help_with_testing: stab_policy = testing
153 else: stab_policy = stable
155 if a_stab >= stab_policy: a_stab = preferred
156 if b_stab >= stab_policy: b_stab = preferred
158 r = cmp(a_stab, b_stab)
159 if r: return r
161 # Newer versions come before older ones
162 r = cmp(a.version, b.version)
163 if r: return r
165 # Get best OS
166 r = cmp(arch.os_ranks.get(a.os, None),
167 arch.os_ranks.get(b.os, None))
168 if r: return r
170 # Get best machine
171 r = cmp(arch.machine_ranks.get(a.machine, None),
172 arch.machine_ranks.get(b.machine, None))
173 if r: return r
175 # Slightly prefer cached versions
176 if self.network_use == network_full:
177 r = cmp(self.get_cached(a), self.get_cached(b))
178 if r: return r
180 return cmp(a.id, b.id)
182 def usable_feeds(self, iface):
183 """Generator for iface.feeds that are valid for our architecture."""
184 for f in iface.feeds:
185 if f.os in arch.os_ranks and f.machine in arch.machine_ranks:
186 yield f
187 else:
188 debug("Skipping '%s'; unsupported architecture %s-%s",
189 f, f.os, f.machine)
191 def get_ranked_implementations(self, iface):
192 impls = iface.implementations.values()
193 for f in self.usable_feeds(iface):
194 feed_iface = self.get_interface(f.uri)
195 if feed_iface.implementations:
196 impls.extend(feed_iface.implementations.values())
197 impls.sort(lambda a, b: self.compare(iface, a, b))
198 return impls
200 def is_unusable(self, impl):
201 return self.get_unusable_reason(impl) != None
203 def get_unusable_reason(self, impl):
204 """Returns the reason why this impl is unusable, or None if it's OK"""
205 stability = impl.get_stability()
206 if stability <= buggy:
207 return stability.name
208 if self.network_use == network_offline and not self.get_cached(impl):
209 return "Not cached and we are off-line"
210 if impl.os not in arch.os_ranks:
211 return "Unsupported OS"
212 if impl.machine not in arch.machine_ranks:
213 return "Unsupported machine type"
214 return None
216 def get_interface(self, uri):
217 iface = iface_cache.get_interface(uri)
219 if iface.last_modified is None:
220 if self.network_use != network_offline:
221 debug("Interface not cached and not off-line. Downloading...")
222 self.begin_iface_download(iface)
223 else:
224 if self.warned_offline:
225 debug("Nothing known about interface, but we are off-line.")
226 else:
227 warn("Nothing known about interface '%s', but we are in off-line mode "
228 "(so not fetching)." % uri)
229 self.warned_offline = True
230 elif not uri.startswith('/'):
231 staleness = time.time() - (iface.last_checked or 0)
232 debug("Staleness for %s is %.2f hours", iface, staleness / 3600.0)
234 if self.network_use != network_offline and self.freshness > 0 and staleness > self.freshness:
235 debug("Updating %s", iface)
236 self.begin_iface_download(iface, False)
237 #else: debug("Local interface, so not checking staleness.")
239 return iface
241 def begin_iface_download(self, interface, force = False):
242 debug("begin_iface_download %s (force = %d)", interface, force)
243 if interface.uri.startswith('/'):
244 return
245 dl = download.begin_iface_download(interface, force)
246 if not dl:
247 assert not force
248 debug("Already in progress")
249 return
251 debug("Need to download")
252 # Calls update_interface_from_network eventually on success
253 self.handler.monitor_download(dl)
255 def get_implementation_path(self, impl):
256 assert isinstance(impl, Implementation)
257 if impl.id.startswith('/'):
258 return impl.id
259 return iface_cache.stores.lookup(impl.id)
261 def get_implementation(self, interface):
262 assert isinstance(interface, Interface)
264 if not interface.name:
265 raise SafeException("We don't have enough information to "
266 "run this program yet. "
267 "Need to download:\n%s" % interface.uri)
268 try:
269 return self.implementation[interface]
270 except KeyError, ex:
271 if interface.implementations:
272 offline = ""
273 if self.network_use == network_offline:
274 offline = "\nThis may be because 'Network Use' is set to Off-line."
275 raise SafeException("No usable implementation found for '%s'.%s" %
276 (interface.name, offline))
277 raise ex
279 def walk_interfaces(self):
280 # Deprecated
281 return iter(self.implementation)
283 def check_signed_data(self, download, signed_data):
284 iface_cache.check_signed_data(download.interface, signed_data, self.handler)
286 def get_cached(self, impl):
287 if impl.id.startswith('/'):
288 return os.path.exists(impl.id)
289 else:
290 try:
291 path = self.get_implementation_path(impl)
292 assert path
293 return True
294 except:
295 pass # OK
296 return False
298 def add_to_cache(self, source, data):
299 iface_cache.add_to_cache(source, data)
301 def get_uncached_implementations(self):
302 uncached = []
303 for iface in self.implementation:
304 impl = self.implementation[iface]
305 assert impl
306 if not self.get_cached(impl):
307 uncached.append((iface, impl))
308 return uncached
310 def refresh_all(self, force = True):
311 for x in self.walk_interfaces():
312 self.begin_iface_download(x, force)
313 for f in self.usable_feeds(x):
314 feed_iface = self.get_interface(f.uri)
315 self.begin_iface_download(feed_iface, force)
317 def interface_changed(self, interface):
318 debug("interface_changed(%s): recalculating", interface)
319 self.recalculate()
321 def get_feed_targets(self, feed_iface_uri):
322 """Return a list of Interfaces for which feed_iface can be a feed.
323 This is used by --feed. If there are no interfaces, raises SafeException."""
324 feed_iface = self.get_interface(feed_iface_uri)
325 if not feed_iface.feed_for:
326 if not feed_iface.name:
327 raise SafeException("Can't get feed targets for '%s'; failed to load interface." %
328 feed_iface_uri)
329 raise SafeException("Missing <feed-for> element in '%s'; "
330 "this interface can't be used as a feed." % feed_iface_uri)
331 feed_targets = feed_iface.feed_for
332 if not feed_iface.name:
333 warn("Warning: unknown interface '%s'" % feed_iface_uri)
334 return [self.get_interface(uri) for uri in feed_targets]