Updated exception handling to Python 2.6 syntax
[zeroinstall.git] / zeroinstall / injector / handler.py
blob60a71455b4aebf857254d4f60f6edf1afc1eda15
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 warn, info
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_import_feed} on failure."""
23 pass
25 class Handler(object):
26 """
27 A Handler is used to interact with the user (e.g. to confirm keys, display download progress, etc).
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 @ivar dry_run: instead of starting a download, just report what we would have downloaded
36 @type dry_run: bool
37 """
39 __slots__ = ['monitored_downloads', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads']
41 def __init__(self, mainloop = None, dry_run = False):
42 self.monitored_downloads = {}
43 self.dry_run = dry_run
44 self.n_completed_downloads = 0
45 self.total_bytes_downloaded = 0
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 as 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 """@deprecated: use tasks.wait_for_blocker instead"""
81 tasks.wait_for_blocker(blocker)
83 def get_download(self, url, force = False, hint = None, factory = None):
84 """Return the Download object currently downloading 'url'.
85 If no download for this URL has been started, start one now (and
86 start monitoring it).
87 If the download failed and force is False, return it anyway.
88 If force is True, abort any current or failed download and start
89 a new one.
90 @rtype: L{download.Download}
91 """
92 if self.dry_run:
93 raise NeedDownload(url)
95 try:
96 dl = self.monitored_downloads[url]
97 if dl and force:
98 dl.abort()
99 raise KeyError
100 except KeyError:
101 if factory is None:
102 dl = download.Download(url, hint)
103 else:
104 dl = factory(url, hint)
105 self.monitor_download(dl)
106 return dl
108 @tasks.async
109 def confirm_import_feed(self, pending, valid_sigs):
110 """Sub-classes should override this method to interact with the user about new feeds.
111 If multiple feeds need confirmation, L{trust.TrustMgr.confirm_keys} will only invoke one instance of this
112 method at a time.
113 @param pending: the new feed to be imported
114 @type pending: L{PendingFeed}
115 @param valid_sigs: maps signatures to a list of fetchers collecting information about the key
116 @type valid_sigs: {L{gpg.ValidSig} : L{fetch.KeyInfoFetcher}}
117 @since: 0.42"""
118 from zeroinstall.injector import trust
120 assert valid_sigs
122 domain = trust.domain_from_url(pending.url)
124 # Ask on stderr, because we may be writing XML to stdout
125 print >>sys.stderr, _("Feed: %s") % pending.url
126 print >>sys.stderr, _("The feed is correctly signed with the following keys:")
127 for x in valid_sigs:
128 print >>sys.stderr, "-", x
130 def text(parent):
131 text = ""
132 for node in parent.childNodes:
133 if node.nodeType == node.TEXT_NODE:
134 text = text + node.data
135 return text
137 shown = set()
138 key_info_fetchers = valid_sigs.values()
139 while key_info_fetchers:
140 old_kfs = key_info_fetchers
141 key_info_fetchers = []
142 for kf in old_kfs:
143 infos = set(kf.info) - shown
144 if infos:
145 if len(valid_sigs) > 1:
146 print "%s: " % kf.fingerprint
147 for key_info in infos:
148 print >>sys.stderr, "-", text(key_info)
149 shown.add(key_info)
150 if kf.blocker:
151 key_info_fetchers.append(kf)
152 if key_info_fetchers:
153 for kf in key_info_fetchers: print >>sys.stderr, kf.status
154 stdin = tasks.InputBlocker(0, 'console')
155 blockers = [kf.blocker for kf in key_info_fetchers] + [stdin]
156 yield blockers
157 for b in blockers:
158 try:
159 tasks.check(b)
160 except Exception as ex:
161 warn(_("Failed to get key info: %s"), ex)
162 if stdin.happened:
163 print >>sys.stderr, _("Skipping remaining key lookups due to input from user")
164 break
165 if not shown:
166 print >>sys.stderr, _("Warning: Nothing known about this key!")
168 if len(valid_sigs) == 1:
169 print >>sys.stderr, _("Do you want to trust this key to sign feeds from '%s'?") % domain
170 else:
171 print >>sys.stderr, _("Do you want to trust all of these keys to sign feeds from '%s'?") % domain
172 while True:
173 print >>sys.stderr, _("Trust [Y/N] "),
174 i = raw_input()
175 if not i: continue
176 if i in 'Nn':
177 raise NoTrustedKeys(_('Not signed with a trusted key'))
178 if i in 'Yy':
179 break
180 for key in valid_sigs:
181 print >>sys.stderr, _("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key.fingerprint, 'domain': domain}
182 trust.trust_db.trust_key(key.fingerprint, domain)
184 @tasks.async
185 def confirm_install(self, msg):
186 """We need to check something with the user before continuing with the install.
187 @raise download.DownloadAborted: if the user cancels"""
188 yield
189 print >>sys.stderr, msg
190 while True:
191 sys.stderr.write(_("Install [Y/N] "))
192 i = raw_input()
193 if not i: continue
194 if i in 'Nn':
195 raise download.DownloadAborted()
196 if i in 'Yy':
197 break
199 def report_error(self, exception, tb = None):
200 """Report an exception to the user.
201 @param exception: the exception to report
202 @type exception: L{SafeException}
203 @param tb: optional traceback
204 @since: 0.25"""
205 warn("%s", str(exception) or type(exception))
206 #import traceback
207 #traceback.print_exception(exception, None, tb)
209 class ConsoleHandler(Handler):
210 """A Handler that displays progress on stdout (a tty).
211 @since: 0.44"""
212 last_msg_len = None
213 update = None
214 disable_progress = 0
215 screen_width = None
217 def downloads_changed(self):
218 import gobject
219 if self.monitored_downloads and self.update is None:
220 if self.screen_width is None:
221 try:
222 import curses
223 curses.setupterm()
224 self.screen_width = curses.tigetnum('cols') or 80
225 except Exception as ex:
226 info("Failed to initialise curses library: %s", ex)
227 self.screen_width = 80
228 self.show_progress()
229 self.update = gobject.timeout_add(200, self.show_progress)
230 elif len(self.monitored_downloads) == 0:
231 if self.update:
232 gobject.source_remove(self.update)
233 self.update = None
234 print
235 self.last_msg_len = None
237 def show_progress(self):
238 urls = self.monitored_downloads.keys()
239 if not urls: return True
241 if self.disable_progress: return True
243 screen_width = self.screen_width - 2
244 item_width = max(16, screen_width / len(self.monitored_downloads))
245 url_width = item_width - 7
247 msg = ""
248 for url in sorted(urls):
249 dl = self.monitored_downloads[url]
250 so_far = dl.get_bytes_downloaded_so_far()
251 leaf = url.rsplit('/', 1)[-1]
252 if len(leaf) >= url_width:
253 display = leaf[:url_width]
254 else:
255 display = url[-url_width:]
256 if dl.expected_size:
257 msg += "[%s %d%%] " % (display, int(so_far * 100 / dl.expected_size))
258 else:
259 msg += "[%s] " % (display)
260 msg = msg[:screen_width]
262 if self.last_msg_len is None:
263 sys.stdout.write(msg)
264 else:
265 sys.stdout.write(chr(13) + msg)
266 if len(msg) < self.last_msg_len:
267 sys.stdout.write(" " * (self.last_msg_len - len(msg)))
269 self.last_msg_len = len(msg)
270 sys.stdout.flush()
272 return True
274 def clear_display(self):
275 if self.last_msg_len != None:
276 sys.stdout.write(chr(13) + " " * self.last_msg_len + chr(13))
277 sys.stdout.flush()
278 self.last_msg_len = None
280 def report_error(self, exception, tb = None):
281 self.clear_display()
282 Handler.report_error(self, exception, tb)
284 def confirm_import_feed(self, pending, valid_sigs):
285 self.clear_display()
286 self.disable_progress += 1
287 blocker = Handler.confirm_import_feed(self, pending, valid_sigs)
288 @tasks.async
289 def enable():
290 yield blocker
291 self.disable_progress -= 1
292 self.show_progress()
293 enable()
294 return blocker