Added last-modified attribute.
[zeroinstall.git] / policy.py
blobb077fb00dbe920ec86d828bacd8fc6f84d213ec5
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 def pretty_time(t):
15 import time
16 return time.strftime('%Y-%m-%d %H:%M:%S UTC', t)
18 class Policy(object):
19 __slots__ = ['root', 'implementation', 'watchers',
20 'help_with_testing', 'network_use', 'updates']
22 def __init__(self, root):
23 assert isinstance(root, (str, unicode))
24 self.root = root
25 self.implementation = {} # Interface -> Implementation
26 self.watchers = []
27 self.help_with_testing = False
28 self.network_use = network_full
29 self.updates = []
31 path = basedir.load_first_config(config_site, config_prog, 'global')
32 if path:
33 config = ConfigParser.ConfigParser()
34 config.read(path)
35 self.help_with_testing = config.getboolean('global',
36 'help_with_testing')
37 self.network_use = config.get('global', 'network_use')
38 assert self.network_use in network_levels
40 def save_config(self):
41 config = ConfigParser.ConfigParser()
42 config.add_section('global')
44 config.set('global', 'help_with_testing', self.help_with_testing)
45 config.set('global', 'network_use', self.network_use)
47 path = basedir.save_config_path(config_site, config_prog)
48 path = os.path.join(path, 'global')
49 config.write(file(path + '.new', 'w'))
50 os.rename(path + '.new', path)
52 def recalculate(self):
53 self.implementation = {}
54 self.updates = []
55 def process(iface):
56 impl = self.get_best_implementation(iface)
57 if impl:
58 self.implementation[iface] = impl
59 for d in impl.dependencies.values():
60 process(self.get_interface(d.interface))
61 process(self.get_interface(self.root))
62 for w in self.watchers: w()
64 def get_best_implementation(self, iface):
65 if not iface.implementations:
66 return None
67 impls = iface.implementations.values()
68 best = impls[0]
69 for x in impls[1:]:
70 if self.compare(iface, x, best) < 0:
71 best = x
72 if self.is_unusable(best):
73 return None
74 return best
76 def compare(self, interface, b, a):
77 a_stab = a.get_stability()
78 b_stab = b.get_stability()
80 # Usable ones come first
81 r = cmp(self.is_unusable(b), self.is_unusable(a))
82 if r: return r
84 # Preferred versions come first
85 r = cmp(a_stab == preferred, b_stab == preferred)
86 if r: return r
88 if self.network_use != network_full:
89 r = cmp(a.get_cached(), b.get_cached())
90 if r: return r
92 # Stability
93 stab_policy = interface.stability_policy
94 if not stab_policy:
95 if self.help_with_testing: stab_policy = testing
96 else: stab_policy = stable
98 if a_stab >= stab_policy: a_stab = preferred
99 if b_stab >= stab_policy: b_stab = preferred
101 r = cmp(a_stab, b_stab)
102 if r: return r
104 r = cmp(a.version, b.version)
105 if r: return r
107 if self.network_use != network_full:
108 r = cmp(a.get_cached(), b.get_cached())
109 if r: return r
111 return cmp(a.path, b.path)
113 def get_ranked_implementations(self, iface):
114 impls = iface.implementations.values()
115 impls.sort(lambda a, b: self.compare(iface, a, b))
116 return impls
118 def is_unusable(self, impl):
119 if impl.get_stability() <= buggy:
120 return True
121 if self.network_use == network_offline and not impl.get_cached():
122 return True
123 return False
125 def get_interface(self, uri):
126 """Get the interface for uri. If it's in the cache, read that.
127 If it's not in the cache or network use is full, start downloading
128 the latest version."""
129 if type(uri) == str:
130 uri = unicode(uri)
131 assert isinstance(uri, unicode)
133 if uri not in _interfaces:
134 # Haven't used this interface so far. Initialise from cache.
135 _interfaces[uri] = Interface(uri)
136 self.init_interface(_interfaces[uri])
138 if self.network_use == network_full and not _interfaces[uri].uptodate:
139 self.begin_iface_download(_interfaces[uri])
141 return _interfaces[uri]
143 def init_interface(self, iface):
144 """We've just created a new Interface. Update from disk cache/network."""
145 debug("Created " + iface.uri)
146 cached = reader.update_from_cache(iface)
147 if not cached:
148 if self.network_use != network_offline:
149 debug("Interface not cached and not off-line. Downloading...")
150 self.begin_iface_download(iface)
151 else:
152 debug("Nothing known about interface, but we are off-line.")
154 def begin_iface_download(self, interface, force = False):
155 dl = download.begin_download(interface, force)
156 if not dl:
157 assert not force
158 return # Already in progress
160 # Calls update_interface_from_network eventually on success
161 self.monitor_download(dl)
163 def monitor_download(self, dl):
164 raise NotImplementedError("Abstract method")
166 def update_interface_from_network(self, interface, new_xml):
167 """xml is the new XML (after the signature has been checked and
168 removed)."""
169 debug("Updating '%s' from network" % (interface.name or interface.uri))
170 assert interface.uri.startswith('/')
172 upstream_dir = basedir.save_config_path(config_site, config_prog, 'interfaces')
173 cached = os.path.join(upstream_dir, escape(interface.uri))
175 if os.path.exists(cached):
176 old_xml = file(cached).read()
177 if old_xml == new_xml:
178 debug("No change")
179 return
180 else:
181 self.confirm_diff(old_xml, new_xml, interface.uri)
183 stream = file(cached + '.new', 'w')
184 stream.write(new_xml)
185 stream.close()
186 new_mtime = reader.check_readable(interface.uri, cached + '.new')
187 assert new_mtime
188 if interface.last_modified:
189 if new_mtime < interface.last_modified:
190 raise SafeException("New interface's modification time is before old "
191 "version!"
192 "\nOld time: " + pretty_time(interface.last_modified) +
193 "\nNew time: " + pretty_time(new_mtime) +
194 "\nRefusing update (leaving new copy as " +
195 cached + ".new)")
196 if new_mtime == interface.last_modified:
197 raise SafeException("Interface has changed, but modification time "
198 "hasn't! Refusing update.")
199 os.rename(cached + '.new', cached)
200 debug("Saved as " + cached)
202 interface.uptodate = True
203 reader.update_from_cache(interface)
204 self.recalculate()
206 def get_implementation(self, interface):
207 if not interface.name:
208 raise SafeException("We don't have enough information to "
209 "run this program yet. "
210 "Need to download:\n%s" % interface.uri)
211 try:
212 return self.implementation[interface]
213 except KeyError, ex:
214 if interface.implementations:
215 offline = ""
216 if self.network_use == network_offline:
217 offline = "\nThis may be because 'Network Use' is set to Off-line."
218 raise SafeException("No usable implementation found for '%s'.%s" %
219 (interface.name, offline))
220 raise ex
222 def walk_interfaces(self):
223 def walk(iface):
224 yield iface
225 impl = self.get_best_implementation(iface)
226 if impl:
227 for d in impl.dependencies.values():
228 for idep in walk(self.get_interface(d.interface)):
229 yield idep
230 return walk(self.get_interface(self.root))
232 def check_signed_data(self, download, signed_data):
233 """Downloaded data is a GPG-signed message. Check that the signature is trusted
234 and call self.update_interface_from_network() when done."""
235 import gpg
236 from trust import trust_db
237 data, errors, sigs = gpg.check_stream(signed_data)
238 iface_xml = data.read()
239 data.close()
240 if not self.update_interface_if_trusted(download.interface, sigs, iface_xml):
241 self.confirm_trust_keys(download.interface, sigs, iface_xml)
243 def update_interface_if_trusted(self, interface, sigs, xml):
244 for s in sigs:
245 if s.is_trusted():
246 self.update_interface_from_network(interface, xml)
247 return True
248 return False
250 def confirm_trust_keys(self, interface, sigs, iface_xml):
251 """We don't trust any of the signatures yet. Ask the user.
252 When done, call update_interface_if_trusted()."""
253 import gpg
254 valid_sigs = [s for s in sigs if isinstance(s, gpg.ValidSig)]
255 if not valid_sigs:
256 raise SafeException('No valid signatures found')
258 print "\nInterface:", interface.uri
259 print "The interface is correctly signed with the following keys:"
260 for x in valid_sigs:
261 print "-", x
262 print "Do you want to trust all of these keys to sign interfaces?"
263 while True:
264 i = raw_input("Trust all [Y/N] ")
265 if not i: continue
266 if i in 'Nn':
267 raise SafeException('Not signed with a trusted key')
268 if i in 'Yy':
269 break
270 from trust import trust_db
271 for key in valid_sigs:
272 print "Trusting", key.fingerprint
273 trust_db.trust_key(key.fingerprint)
275 if not self.update_interface_if_trusted(interface, sigs, iface_xml):
276 raise Exception('Bug: still not trusted!!')
278 def confirm_diff(self, old, new, uri):
279 import difflib
280 diff = difflib.unified_diff(old.split('\n'), new.split('\n'), uri, "",
281 "", "", 2, "")
282 print "Updates:"
283 for line in diff:
284 print line