Send console trust prompt to stderr, not stdout
[zeroinstall.git] / zeroinstall / injector / handler.py
bloba0124ff6b38add4873ab90f8b851f962d7a1a249
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 import sys
14 from logging import debug, warn
16 from zeroinstall import NeedDownload, SafeException
17 from zeroinstall.support import tasks
18 from zeroinstall.injector import download
20 class NoTrustedKeys(SafeException):
21 """Thrown by L{Handler.confirm_trust_keys} on failure."""
22 pass
24 class Handler(object):
25 """
26 This implementation uses the GLib mainloop. Note that QT4 can use the GLib mainloop too.
28 @ivar monitored_downloads: dict of downloads in progress
29 @type monitored_downloads: {URL: L{download.Download}}
30 @ivar n_completed_downloads: number of downloads which have finished for GUIs, etc (can be reset as desired).
31 @type n_completed_downloads: int
32 @ivar total_bytes_downloaded: informational counter for GUIs, etc (can be reset as desired). Updated when download finishes.
33 @type total_bytes_downloaded: int
34 """
36 __slots__ = ['monitored_downloads', '_loop', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads']
38 def __init__(self, mainloop = None, dry_run = False):
39 self.monitored_downloads = {}
40 self._loop = None
41 self.dry_run = dry_run
42 self.n_completed_downloads = 0
43 self.total_bytes_downloaded = 0
45 def monitor_download(self, dl):
46 """Called when a new L{download} is started.
47 This is mainly used by the GUI to display the progress bar."""
48 dl.start()
49 self.monitored_downloads[dl.url] = dl
50 self.downloads_changed()
52 @tasks.async
53 def download_done_stats():
54 yield dl.downloaded
55 # NB: we don't check for exceptions here; someone else should be doing that
56 try:
57 self.n_completed_downloads += 1
58 self.total_bytes_downloaded += dl.get_bytes_downloaded_so_far()
59 del self.monitored_downloads[dl.url]
60 self.downloads_changed()
61 except Exception, ex:
62 self.report_error(ex)
63 download_done_stats()
65 def impl_added_to_store(self, impl):
66 """Called by the L{fetch.Fetcher} when adding an implementation.
67 The GUI uses this to update its display.
68 @param impl: the implementation which has been added
69 @type impl: L{model.Implementation}
70 """
71 pass
73 def downloads_changed(self):
74 """This is just for the GUI to override to update its display."""
75 pass
77 def wait_for_blocker(self, blocker):
78 """Run a recursive mainloop until blocker is triggered.
79 @param blocker: event to wait on
80 @type blocker: L{tasks.Blocker}"""
81 if not blocker.happened:
82 import gobject
84 def quitter():
85 yield blocker
86 self._loop.quit()
87 quit = tasks.Task(quitter(), "quitter")
89 assert self._loop is None # Avoid recursion
90 self._loop = gobject.MainLoop(gobject.main_context_default())
91 try:
92 debug("Entering mainloop, waiting for %s", blocker)
93 self._loop.run()
94 finally:
95 self._loop = None
97 assert blocker.happened, "Someone quit the main loop!"
99 tasks.check(blocker)
101 def get_download(self, url, force = False, hint = None):
102 """Return the Download object currently downloading 'url'.
103 If no download for this URL has been started, start one now (and
104 start monitoring it).
105 If the download failed and force is False, return it anyway.
106 If force is True, abort any current or failed download and start
107 a new one.
108 @rtype: L{download.Download}
110 if self.dry_run:
111 raise NeedDownload(url)
113 try:
114 dl = self.monitored_downloads[url]
115 if dl and force:
116 dl.abort()
117 raise KeyError
118 except KeyError:
119 dl = download.Download(url, hint)
120 self.monitor_download(dl)
121 return dl
123 def confirm_trust_keys(self, interface, sigs, iface_xml):
124 """We don't trust any of the signatures yet. Ask the user.
125 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
126 @arg interface: the interface being updated
127 @arg sigs: a list of signatures (from L{gpg.check_stream})
128 @arg iface_xml: the downloaded data (not yet trusted)
129 @return: a blocker, if confirmation will happen asynchronously, or None
130 @rtype: L{tasks.Blocker}"""
131 from zeroinstall.injector import trust, gpg
132 assert sigs
133 valid_sigs = [s for s in sigs if isinstance(s, gpg.ValidSig)]
134 if not valid_sigs:
135 raise SafeException('No valid signatures found. Signatures:' +
136 ''.join(['\n- ' + str(s) for s in sigs]))
138 domain = trust.domain_from_url(interface.uri)
140 # Ask on stderr, because we may be writing XML to stdout
141 print >>sys.stderr, "\nInterface:", interface.uri
142 print >>sys.stderr, "The interface is correctly signed with the following keys:"
143 for x in valid_sigs:
144 print >>sys.stderr, "-", x
146 if len(valid_sigs) == 1:
147 print >>sys.stderr, "Do you want to trust this key to sign feeds from '%s'?" % domain
148 else:
149 print >>sys.stderr, "Do you want to trust all of these keys to sign feeds from '%s'?" % domain
150 while True:
151 print >>sys.stderr, "Trust [Y/N] ",
152 i = raw_input()
153 if not i: continue
154 if i in 'Nn':
155 raise NoTrustedKeys('Not signed with a trusted key')
156 if i in 'Yy':
157 break
158 for key in valid_sigs:
159 print >>sys.stderr, "Trusting", key.fingerprint, "for", domain
160 trust.trust_db.trust_key(key.fingerprint, domain)
162 trust.trust_db.notify()
164 def report_error(self, exception, tb = None):
165 """Report an exception to the user.
166 @param exception: the exception to report
167 @type exception: L{SafeException}
168 @param tb: optional traceback
169 @since: 0.25"""
170 warn("%s", exception)
171 #import traceback
172 #traceback.print_exception(exception, None, tb)