Start development series 0.42.1-post
[zeroinstall/zeroinstall-rsl.git] / zeroinstall / injector / handler.py
blob8f93461e7a963ef1d3768250944bda21944a2a75
1 """
2 Integrates download callbacks with an external mainloop.
3 While things are being downloaded, Zero Install returns control to your program.
4 Your mainloop is responsible for monitoring the state of the downloads and notifying
5 Zero Install when they are complete.
7 To do this, you supply a L{Handler} to the L{policy}.
8 """
10 # Copyright (C) 2009, Thomas Leonard
11 # See the README file for details, or visit http://0install.net.
13 from zeroinstall import _
14 import sys
15 from logging import debug, warn
17 from zeroinstall import NeedDownload, SafeException
18 from zeroinstall.support import tasks
19 from zeroinstall.injector import download
21 class NoTrustedKeys(SafeException):
22 """Thrown by L{Handler.confirm_trust_keys} on failure."""
23 pass
25 class Handler(object):
26 """
27 This implementation uses the GLib mainloop. Note that QT4 can use the GLib mainloop too.
29 @ivar monitored_downloads: dict of downloads in progress
30 @type monitored_downloads: {URL: L{download.Download}}
31 @ivar n_completed_downloads: number of downloads which have finished for GUIs, etc (can be reset as desired).
32 @type n_completed_downloads: int
33 @ivar total_bytes_downloaded: informational counter for GUIs, etc (can be reset as desired). Updated when download finishes.
34 @type total_bytes_downloaded: int
35 """
37 __slots__ = ['monitored_downloads', '_loop', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads', '_current_confirm']
39 def __init__(self, mainloop = None, dry_run = False):
40 self.monitored_downloads = {}
41 self._loop = None
42 self.dry_run = dry_run
43 self.n_completed_downloads = 0
44 self.total_bytes_downloaded = 0
45 self._current_confirm = None
47 def monitor_download(self, dl):
48 """Called when a new L{download} is started.
49 This is mainly used by the GUI to display the progress bar."""
50 dl.start()
51 self.monitored_downloads[dl.url] = dl
52 self.downloads_changed()
54 @tasks.async
55 def download_done_stats():
56 yield dl.downloaded
57 # NB: we don't check for exceptions here; someone else should be doing that
58 try:
59 self.n_completed_downloads += 1
60 self.total_bytes_downloaded += dl.get_bytes_downloaded_so_far()
61 del self.monitored_downloads[dl.url]
62 self.downloads_changed()
63 except Exception, ex:
64 self.report_error(ex)
65 download_done_stats()
67 def impl_added_to_store(self, impl):
68 """Called by the L{fetch.Fetcher} when adding an implementation.
69 The GUI uses this to update its display.
70 @param impl: the implementation which has been added
71 @type impl: L{model.Implementation}
72 """
73 pass
75 def downloads_changed(self):
76 """This is just for the GUI to override to update its display."""
77 pass
79 def wait_for_blocker(self, blocker):
80 """Run a recursive mainloop until blocker is triggered.
81 @param blocker: event to wait on
82 @type blocker: L{tasks.Blocker}"""
83 if not blocker.happened:
84 import gobject
86 def quitter():
87 yield blocker
88 self._loop.quit()
89 quit = tasks.Task(quitter(), "quitter")
91 assert self._loop is None # Avoid recursion
92 self._loop = gobject.MainLoop(gobject.main_context_default())
93 try:
94 debug(_("Entering mainloop, waiting for %s"), blocker)
95 self._loop.run()
96 finally:
97 self._loop = None
99 assert blocker.happened, "Someone quit the main loop!"
101 tasks.check(blocker)
103 def get_download(self, url, force = False, hint = None):
104 """Return the Download object currently downloading 'url'.
105 If no download for this URL has been started, start one now (and
106 start monitoring it).
107 If the download failed and force is False, return it anyway.
108 If force is True, abort any current or failed download and start
109 a new one.
110 @rtype: L{download.Download}
112 if self.dry_run:
113 raise NeedDownload(url)
115 try:
116 dl = self.monitored_downloads[url]
117 if dl and force:
118 dl.abort()
119 raise KeyError
120 except KeyError:
121 dl = download.Download(url, hint)
122 self.monitor_download(dl)
123 return dl
125 def confirm_keys(self, pending, fetch_key_info):
126 """We don't trust any of the signatures yet. Ask the user.
127 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
128 This method just calls L{confirm_import_feed} if the handler (self) is
129 new-style, or L{confirm_trust_keys} for older classes. A class
130 is considered old-style if it overrides confirm_trust_keys and
131 not confirm_import_feed.
132 @since: 0.42
133 @arg pending: an object holding details of the updated feed
134 @type pending: L{PendingFeed}
135 @arg fetch_key_info: a function which can be used to fetch information about a key fingerprint
136 @type fetch_key_info: str -> L{Blocker}
137 @return: A blocker that triggers when the user has chosen, or None if already done.
138 @rtype: None | L{Blocker}"""
140 assert pending.sigs
142 if hasattr(self.confirm_trust_keys, 'original') or not hasattr(self.confirm_import_feed, 'original'):
143 # new-style class
144 from zeroinstall.injector import gpg
145 valid_sigs = [s for s in pending.sigs if isinstance(s, gpg.ValidSig)]
146 if not valid_sigs:
147 raise SafeException(_('No valid signatures found on "%(url)s". Signatures:%(signatures)s') %
148 {'url': pending.url, 'signatures': ''.join(['\n- ' + str(s) for s in pending.sigs])})
150 # Start downloading information about the keys...
151 kfs = {}
152 for sig in valid_sigs:
153 kfs[sig] = fetch_key_info(sig.fingerprint)
155 return self._queue_confirm_import_feed(pending, kfs)
156 else:
157 # old-style class
158 from zeroinstall.injector import iface_cache
159 import warnings
160 warnings.warn(_("Should override confirm_import_feed(); using old confirm_trust_keys() for now"), DeprecationWarning, stacklevel = 2)
162 iface = iface_cache.iface_cache.get_interface(pending.url)
163 return self.confirm_trust_keys(iface, pending.sigs, pending.new_xml)
165 @tasks.async
166 def _queue_confirm_import_feed(self, pending, valid_sigs):
167 # If we're already confirming something else, wait for that to finish...
168 while self._current_confirm is not None:
169 yield self._current_confirm
171 # Check whether we still need to confirm. The user may have
172 # already approved one of the keys while dealing with another
173 # feed.
174 from zeroinstall.injector import trust
175 domain = trust.domain_from_url(pending.url)
176 for sig in valid_sigs:
177 is_trusted = trust.trust_db.is_trusted(sig.fingerprint, domain)
178 if is_trusted:
179 return
181 # Take the lock and confirm this feed
182 self._current_confirm = lock = tasks.Blocker('confirm key lock')
183 try:
184 done = self.confirm_import_feed(pending, valid_sigs)
185 yield done
186 tasks.check(done)
187 finally:
188 self._current_confirm = None
189 lock.trigger()
191 @tasks.async
192 def confirm_import_feed(self, pending, valid_sigs):
193 """Sub-classes should override this method to interact with the user about new feeds.
194 If multiple feeds need confirmation, L{confirm_keys} will only invoke one instance of this
195 method at a time.
196 @param pending: the new feed to be imported
197 @type pending: L{PendingFeed}
198 @param valid_sigs: maps signatures to a list of fetchers collecting information about the key
199 @type valid_sigs: {L{gpg.ValidSig} : L{fetch.KeyInfoFetcher}}
200 @since: 0.42
201 @see: L{confirm_keys}"""
202 from zeroinstall.injector import trust
204 assert valid_sigs
206 domain = trust.domain_from_url(pending.url)
208 # Ask on stderr, because we may be writing XML to stdout
209 print >>sys.stderr, _("Feed: %s"), pending.url
210 print >>sys.stderr, _("The feed is correctly signed with the following keys:")
211 for x in valid_sigs:
212 print >>sys.stderr, "-", x
214 def text(parent):
215 text = ""
216 for node in parent.childNodes:
217 if node.nodeType == node.TEXT_NODE:
218 text = text + node.data
219 return text
221 shown = set()
222 key_info_fetchers = valid_sigs.values()
223 while key_info_fetchers:
224 old_kfs = key_info_fetchers
225 key_info_fetchers = []
226 for kf in old_kfs:
227 infos = set(kf.info) - shown
228 if infos:
229 if len(valid_sigs) > 1:
230 print "%s: " % kf.fingerprint
231 for info in infos:
232 print >>sys.stderr, "-", text(info)
233 shown.add(info)
234 if kf.blocker:
235 key_info_fetchers.append(kf)
236 if key_info_fetchers:
237 for kf in key_info_fetchers: print >>sys.stderr, kf.status
238 stdin = tasks.InputBlocker(0, 'console')
239 blockers = [kf.blocker for kf in key_info_fetchers] + [stdin]
240 yield blockers
241 for b in blockers:
242 try:
243 tasks.check(b)
244 except Exception, ex:
245 warn(_("Failed to get key info: %s"), ex)
246 if stdin.happened:
247 print >>sys.stderr, _("Skipping remaining key lookups due to input from user")
248 break
250 if len(valid_sigs) == 1:
251 print >>sys.stderr, _("Do you want to trust this key to sign feeds from '%s'?") % domain
252 else:
253 print >>sys.stderr, _("Do you want to trust all of these keys to sign feeds from '%s'?") % domain
254 while True:
255 print >>sys.stderr, _("Trust [Y/N] "),
256 i = raw_input()
257 if not i: continue
258 if i in 'Nn':
259 raise NoTrustedKeys(_('Not signed with a trusted key'))
260 if i in 'Yy':
261 break
262 for key in valid_sigs:
263 print >>sys.stderr, _("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key.fingerprint, 'domain': domain}
264 trust.trust_db.trust_key(key.fingerprint, domain)
266 confirm_import_feed.original = True
268 def confirm_trust_keys(self, interface, sigs, iface_xml):
269 """We don't trust any of the signatures yet. Ask the user.
270 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
271 @deprecated: see L{confirm_keys}
272 @arg interface: the interface being updated
273 @arg sigs: a list of signatures (from L{gpg.check_stream})
274 @arg iface_xml: the downloaded data (not yet trusted)
275 @return: a blocker, if confirmation will happen asynchronously, or None
276 @rtype: L{tasks.Blocker}"""
277 import warnings
278 warnings.warn(_("Use confirm_keys, not confirm_trust_keys"), DeprecationWarning, stacklevel = 2)
279 from zeroinstall.injector import trust, gpg
280 assert sigs
281 valid_sigs = [s for s in sigs if isinstance(s, gpg.ValidSig)]
282 if not valid_sigs:
283 raise SafeException('No valid signatures found on "%s". Signatures:%s' %
284 (interface.uri, ''.join(['\n- ' + str(s) for s in sigs])))
286 domain = trust.domain_from_url(interface.uri)
288 # Ask on stderr, because we may be writing XML to stdout
289 print >>sys.stderr, "\nInterface:", interface.uri
290 print >>sys.stderr, "The interface is correctly signed with the following keys:"
291 for x in valid_sigs:
292 print >>sys.stderr, "-", x
294 if len(valid_sigs) == 1:
295 print >>sys.stderr, "Do you want to trust this key to sign feeds from '%s'?" % domain
296 else:
297 print >>sys.stderr, "Do you want to trust all of these keys to sign feeds from '%s'?" % domain
298 while True:
299 print >>sys.stderr, "Trust [Y/N] ",
300 i = raw_input()
301 if not i: continue
302 if i in 'Nn':
303 raise NoTrustedKeys(_('Not signed with a trusted key'))
304 if i in 'Yy':
305 break
306 for key in valid_sigs:
307 print >>sys.stderr, "Trusting", key.fingerprint, "for", domain
308 trust.trust_db.trust_key(key.fingerprint, domain)
310 trust.trust_db.notify()
312 confirm_trust_keys.original = True # Detect if someone overrides it
314 def report_error(self, exception, tb = None):
315 """Report an exception to the user.
316 @param exception: the exception to report
317 @type exception: L{SafeException}
318 @param tb: optional traceback
319 @since: 0.25"""
320 warn("%s", str(exception) or type(exception))
321 #import traceback
322 #traceback.print_exception(exception, None, tb)