Changed Handler.monitored_downloads from a dict to a set
[zeroinstall/solver.git] / zeroinstall / injector / handler.py
blob1e55e3d2014986fcde10aa0074e8b42aa79e552c
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 __future__ import print_function
15 from zeroinstall import _
16 import sys
17 from logging import warn, info
19 from zeroinstall import NeedDownload, SafeException
20 from zeroinstall.support import tasks
21 from zeroinstall.injector import download
23 class NoTrustedKeys(SafeException):
24 """Thrown by L{Handler.confirm_import_feed} on failure."""
25 pass
27 class Handler(object):
28 """
29 A Handler is used to interact with the user (e.g. to confirm keys, display download progress, etc).
31 @ivar monitored_downloads: set of downloads in progress
32 @type monitored_downloads: {L{download.Download}}
33 @ivar n_completed_downloads: number of downloads which have finished for GUIs, etc (can be reset as desired).
34 @type n_completed_downloads: int
35 @ivar total_bytes_downloaded: informational counter for GUIs, etc (can be reset as desired). Updated when download finishes.
36 @type total_bytes_downloaded: int
37 @ivar dry_run: instead of starting a download, just report what we would have downloaded
38 @type dry_run: bool
39 """
41 __slots__ = ['monitored_downloads', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads']
43 def __init__(self, mainloop = None, dry_run = False):
44 self.monitored_downloads = set()
45 self.dry_run = dry_run
46 self.n_completed_downloads = 0
47 self.total_bytes_downloaded = 0
49 def monitor_download(self, dl):
50 """Called when a new L{download} is started.
51 This is mainly used by the GUI to display the progress bar."""
52 dl.start()
53 self.monitored_downloads.add(dl)
54 self.downloads_changed()
56 @tasks.async
57 def download_done_stats():
58 yield dl.downloaded
59 # NB: we don't check for exceptions here; someone else should be doing that
60 try:
61 self.n_completed_downloads += 1
62 self.total_bytes_downloaded += dl.get_bytes_downloaded_so_far()
63 self.monitored_downloads.remove(dl)
64 self.downloads_changed()
65 except Exception as ex:
66 self.report_error(ex)
67 download_done_stats()
69 def impl_added_to_store(self, impl):
70 """Called by the L{fetch.Fetcher} when adding an implementation.
71 The GUI uses this to update its display.
72 @param impl: the implementation which has been added
73 @type impl: L{model.Implementation}
74 """
75 pass
77 def downloads_changed(self):
78 """This is just for the GUI to override to update its display."""
79 pass
81 def wait_for_blocker(self, blocker):
82 """@deprecated: use tasks.wait_for_blocker instead"""
83 tasks.wait_for_blocker(blocker)
85 def get_download(self, url, force = False, hint = None, factory = None):
86 """Create a Download object to download 'url'.
87 @rtype: L{download.Download}
88 """
89 if self.dry_run:
90 raise NeedDownload(url)
92 if factory is None:
93 dl = download.Download(url, hint)
94 else:
95 dl = factory(url, hint)
96 self.monitor_download(dl)
97 return dl
99 @tasks.async
100 def confirm_import_feed(self, pending, valid_sigs):
101 """Sub-classes should override this method to interact with the user about new feeds.
102 If multiple feeds need confirmation, L{trust.TrustMgr.confirm_keys} will only invoke one instance of this
103 method at a time.
104 @param pending: the new feed to be imported
105 @type pending: L{PendingFeed}
106 @param valid_sigs: maps signatures to a list of fetchers collecting information about the key
107 @type valid_sigs: {L{gpg.ValidSig} : L{fetch.KeyInfoFetcher}}
108 @since: 0.42"""
109 from zeroinstall.injector import trust
111 assert valid_sigs
113 domain = trust.domain_from_url(pending.url)
115 # Ask on stderr, because we may be writing XML to stdout
116 print(_("Feed: %s") % pending.url, file=sys.stderr)
117 print(_("The feed is correctly signed with the following keys:"), file=sys.stderr)
118 for x in valid_sigs:
119 print("-", x, file=sys.stderr)
121 def text(parent):
122 text = ""
123 for node in parent.childNodes:
124 if node.nodeType == node.TEXT_NODE:
125 text = text + node.data
126 return text
128 shown = set()
129 key_info_fetchers = valid_sigs.values()
130 while key_info_fetchers:
131 old_kfs = key_info_fetchers
132 key_info_fetchers = []
133 for kf in old_kfs:
134 infos = set(kf.info) - shown
135 if infos:
136 if len(valid_sigs) > 1:
137 print("%s: " % kf.fingerprint)
138 for key_info in infos:
139 print("-", text(key_info), file=sys.stderr)
140 shown.add(key_info)
141 if kf.blocker:
142 key_info_fetchers.append(kf)
143 if key_info_fetchers:
144 for kf in key_info_fetchers: print(kf.status, file=sys.stderr)
145 stdin = tasks.InputBlocker(0, 'console')
146 blockers = [kf.blocker for kf in key_info_fetchers] + [stdin]
147 yield blockers
148 for b in blockers:
149 try:
150 tasks.check(b)
151 except Exception as ex:
152 warn(_("Failed to get key info: %s"), ex)
153 if stdin.happened:
154 print(_("Skipping remaining key lookups due to input from user"), file=sys.stderr)
155 break
156 if not shown:
157 print(_("Warning: Nothing known about this key!"), file=sys.stderr)
159 if len(valid_sigs) == 1:
160 print(_("Do you want to trust this key to sign feeds from '%s'?") % domain, file=sys.stderr)
161 else:
162 print(_("Do you want to trust all of these keys to sign feeds from '%s'?") % domain, file=sys.stderr)
163 while True:
164 print(_("Trust [Y/N] "), end=' ', file=sys.stderr)
165 i = raw_input()
166 if not i: continue
167 if i in 'Nn':
168 raise NoTrustedKeys(_('Not signed with a trusted key'))
169 if i in 'Yy':
170 break
171 for key in valid_sigs:
172 print(_("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key.fingerprint, 'domain': domain}, file=sys.stderr)
173 trust.trust_db.trust_key(key.fingerprint, domain)
175 @tasks.async
176 def confirm_install(self, msg):
177 """We need to check something with the user before continuing with the install.
178 @raise download.DownloadAborted: if the user cancels"""
179 yield
180 print(msg, file=sys.stderr)
181 while True:
182 sys.stderr.write(_("Install [Y/N] "))
183 i = raw_input()
184 if not i: continue
185 if i in 'Nn':
186 raise download.DownloadAborted()
187 if i in 'Yy':
188 break
190 def report_error(self, exception, tb = None):
191 """Report an exception to the user.
192 @param exception: the exception to report
193 @type exception: L{SafeException}
194 @param tb: optional traceback
195 @since: 0.25"""
196 warn("%s", str(exception) or type(exception))
197 #import traceback
198 #traceback.print_exception(exception, None, tb)
200 class ConsoleHandler(Handler):
201 """A Handler that displays progress on stdout (a tty).
202 @since: 0.44"""
203 last_msg_len = None
204 update = None
205 disable_progress = 0
206 screen_width = None
208 def downloads_changed(self):
209 import gobject
210 if self.monitored_downloads and self.update is None:
211 if self.screen_width is None:
212 try:
213 import curses
214 curses.setupterm()
215 self.screen_width = curses.tigetnum('cols') or 80
216 except Exception as ex:
217 info("Failed to initialise curses library: %s", ex)
218 self.screen_width = 80
219 self.show_progress()
220 self.update = gobject.timeout_add(200, self.show_progress)
221 elif len(self.monitored_downloads) == 0:
222 if self.update:
223 gobject.source_remove(self.update)
224 self.update = None
225 print()
226 self.last_msg_len = None
228 def show_progress(self):
229 if not self.monitored_downloads: return True
230 urls = [(dl.url, dl) for dl in self.monitored_downloads]
232 if self.disable_progress: return True
234 screen_width = self.screen_width - 2
235 item_width = max(16, screen_width / len(self.monitored_downloads))
236 url_width = item_width - 7
238 msg = ""
239 for url, dl in sorted(urls):
240 so_far = dl.get_bytes_downloaded_so_far()
241 leaf = url.rsplit('/', 1)[-1]
242 if len(leaf) >= url_width:
243 display = leaf[:url_width]
244 else:
245 display = url[-url_width:]
246 if dl.expected_size:
247 msg += "[%s %d%%] " % (display, int(so_far * 100 / dl.expected_size))
248 else:
249 msg += "[%s] " % (display)
250 msg = msg[:screen_width]
252 if self.last_msg_len is None:
253 sys.stdout.write(msg)
254 else:
255 sys.stdout.write(chr(13) + msg)
256 if len(msg) < self.last_msg_len:
257 sys.stdout.write(" " * (self.last_msg_len - len(msg)))
259 self.last_msg_len = len(msg)
260 sys.stdout.flush()
262 return True
264 def clear_display(self):
265 if self.last_msg_len != None:
266 sys.stdout.write(chr(13) + " " * self.last_msg_len + chr(13))
267 sys.stdout.flush()
268 self.last_msg_len = None
270 def report_error(self, exception, tb = None):
271 self.clear_display()
272 Handler.report_error(self, exception, tb)
274 def confirm_import_feed(self, pending, valid_sigs):
275 self.clear_display()
276 self.disable_progress += 1
277 blocker = Handler.confirm_import_feed(self, pending, valid_sigs)
278 @tasks.async
279 def enable():
280 yield blocker
281 self.disable_progress -= 1
282 self.show_progress()
283 enable()
284 return blocker