More refactoring of downloads. injector-auto can now download in parallel.
[zeroinstall.git] / policy.py
blobd43c6aa7ac9e9a9aecef687957f1c99edc4450e7
1 from model import *
2 import basedir
3 from namespaces import *
4 import ConfigParser
5 import reader
6 from logging import debug, info
7 import download
9 #from logging import getLogger, DEBUG
10 #getLogger().setLevel(DEBUG)
12 _interfaces = {} # URI -> Interface
14 class Policy(object):
15 __slots__ = ['root', 'implementation', 'watchers',
16 'help_with_testing', 'network_use', 'updates']
18 def __init__(self, root):
19 assert isinstance(root, (str, unicode))
20 self.root = root
21 self.implementation = {} # Interface -> Implementation
22 self.watchers = []
23 self.help_with_testing = False
24 self.network_use = network_full
25 self.updates = []
27 path = basedir.load_first_config(config_site, config_prog, 'global')
28 if path:
29 config = ConfigParser.ConfigParser()
30 config.read(path)
31 self.help_with_testing = config.getboolean('global',
32 'help_with_testing')
33 self.network_use = config.get('global', 'network_use')
34 assert self.network_use in network_levels
36 def save_config(self):
37 config = ConfigParser.ConfigParser()
38 config.add_section('global')
40 config.set('global', 'help_with_testing', self.help_with_testing)
41 config.set('global', 'network_use', self.network_use)
43 path = basedir.save_config_path(config_site, config_prog)
44 path = os.path.join(path, 'global')
45 config.write(file(path + '.new', 'w'))
46 os.rename(path + '.new', path)
48 def recalculate(self):
49 self.implementation = {}
50 self.updates = []
51 def process(iface):
52 impl = self.get_best_implementation(iface)
53 if impl:
54 self.implementation[iface] = impl
55 for d in impl.dependencies.values():
56 process(self.get_interface(d.interface))
57 process(self.get_interface(self.root))
58 for w in self.watchers: w()
60 def get_best_implementation(self, iface):
61 if not iface.implementations:
62 return None
63 impls = iface.implementations.values()
64 best = impls[0]
65 for x in impls[1:]:
66 if self.compare(iface, x, best) < 0:
67 best = x
68 if self.is_unusable(best):
69 return None
70 return best
72 def compare(self, interface, b, a):
73 a_stab = a.get_stability()
74 b_stab = b.get_stability()
76 # Usable ones come first
77 r = cmp(self.is_unusable(b), self.is_unusable(a))
78 if r: return r
80 # Preferred versions come first
81 r = cmp(a_stab == preferred, b_stab == preferred)
82 if r: return r
84 if self.network_use != network_full:
85 r = cmp(a.get_cached(), b.get_cached())
86 if r: return r
88 # Stability
89 stab_policy = interface.stability_policy
90 if not stab_policy:
91 if self.help_with_testing: stab_policy = testing
92 else: stab_policy = stable
94 if a_stab >= stab_policy: a_stab = preferred
95 if b_stab >= stab_policy: b_stab = preferred
97 r = cmp(a_stab, b_stab)
98 if r: return r
100 r = cmp(a.version, b.version)
101 if r: return r
103 if self.network_use != network_full:
104 r = cmp(a.get_cached(), b.get_cached())
105 if r: return r
107 return cmp(a.path, b.path)
109 def get_ranked_implementations(self, iface):
110 impls = iface.implementations.values()
111 impls.sort(lambda a, b: self.compare(iface, a, b))
112 return impls
114 def is_unusable(self, impl):
115 if impl.get_stability() <= buggy:
116 return True
117 if self.network_use == network_offline and not impl.get_cached():
118 return True
119 return False
121 def get_interface(self, uri):
122 """Get the interface for uri. If it's in the cache, read that.
123 If it's not in the cache or network use is full, start downloading
124 the latest version."""
125 if type(uri) == str:
126 uri = unicode(uri)
127 assert isinstance(uri, unicode)
129 if uri not in _interfaces:
130 # Haven't used this interface so far. Initialise from cache.
131 _interfaces[uri] = Interface(uri)
132 self.init_interface(_interfaces[uri])
134 if self.network_use == network_full and not _interfaces[uri].uptodate:
135 self.begin_iface_download(_interfaces[uri])
137 return _interfaces[uri]
139 def init_interface(self, iface):
140 """We've just created a new Interface. Update from disk cache/network."""
141 debug("Created " + iface.uri)
142 cached = reader.update_from_cache(iface)
143 if not cached:
144 if self.network_use != network_offline:
145 debug("Interface not cached and not off-line. Downloading...")
146 self.begin_iface_download(iface)
147 else:
148 debug("Nothing known about interface, but we are off-line.")
150 def begin_iface_download(self, interface, force = False):
151 dl = download.begin_download(interface, force)
152 if not dl:
153 assert not force
154 return # Already in progress
156 # Calls update_interface_from_network eventually on success
157 self.monitor_download(dl)
159 def monitor_download(self, dl):
160 raise NotImplementedError("Abstract method")
162 def update_interface_from_network(self, interface, stream):
163 """stream is the new XML (after the signature has been checked and
164 removed)."""
165 debug("Updating '%s' from network" % (interface.name or interface.uri))
166 assert interface.uri.startswith('/')
168 upstream_dir = basedir.save_config_path(config_site, config_prog, 'interfaces')
169 cached = os.path.join(upstream_dir, escape(interface.uri))
171 new_xml = stream.read()
173 if os.path.exists(cached):
174 old_xml = file(cached).read()
175 if old_xml == new_xml:
176 debug("No change")
177 else:
178 self.confirm_diff(old_xml, new_xml, interface.uri)
180 stream = file(cached + '.new', 'w')
181 stream.write(new_xml)
182 stream.close()
183 reader.update(interface, cached + '.new')
184 os.rename(cached + '.new', cached)
185 debug("Saved as " + cached)
187 interface.uptodate = True
189 reader.update_user_overrides(interface)
190 self.recalculate()
192 def get_implementation(self, interface):
193 if not interface.name:
194 raise SafeException("We don't have enough information to "
195 "run this program yet. "
196 "Need to download:\n%s" % interface.uri)
197 try:
198 return self.implementation[interface]
199 except KeyError, ex:
200 if interface.implementations:
201 offline = ""
202 if self.network_use == network_offline:
203 offline = "\nThis may be because 'Network Use' is set to Off-line."
204 raise SafeException("No usable implementation found for '%s'.%s" %
205 (interface.name, offline))
206 raise ex
208 def walk_interfaces(self):
209 def walk(iface):
210 yield iface
211 impl = self.get_best_implementation(iface)
212 if impl:
213 for d in impl.dependencies.values():
214 for idep in walk(self.get_interface(d.interface)):
215 yield idep
216 return walk(self.get_interface(self.root))
218 def check_signed_data(self, download, signed_data):
219 """Downloaded data is a GPG-signed message. Check that the signature is trusted
220 and call self.update_interface_from_network() when done."""
221 import gpg
222 data = gpg.check_stream(signed_data)
223 self.update_interface_from_network(download.interface, data)
225 def confirm_diff(self, old, new, uri):
226 import difflib
227 diff = difflib.unified_diff(old.split('\n'), new.split('\n'), uri, "",
228 "", "", 2, "")
229 print "Updates:"
230 for line in diff:
231 print line