Removed deprecated Handler.get_download
[zeroinstall.git] / zeroinstall / injector / handler.py
blob0896163095a478d40c126acb4217e7bbf1b546cc
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 self.monitored_downloads.add(dl)
53 self.downloads_changed()
55 @tasks.async
56 def download_done_stats():
57 yield dl.downloaded
58 # NB: we don't check for exceptions here; someone else should be doing that
59 try:
60 self.n_completed_downloads += 1
61 self.total_bytes_downloaded += dl.get_bytes_downloaded_so_far()
62 self.monitored_downloads.remove(dl)
63 self.downloads_changed()
64 except Exception as ex:
65 self.report_error(ex)
66 download_done_stats()
68 def impl_added_to_store(self, impl):
69 """Called by the L{fetch.Fetcher} when adding an implementation.
70 The GUI uses this to update its display.
71 @param impl: the implementation which has been added
72 @type impl: L{model.Implementation}
73 """
74 pass
76 def downloads_changed(self):
77 """This is just for the GUI to override to update its display."""
78 pass
80 def wait_for_blocker(self, blocker):
81 """@deprecated: use tasks.wait_for_blocker instead"""
82 tasks.wait_for_blocker(blocker)
84 @tasks.async
85 def confirm_import_feed(self, pending, valid_sigs):
86 """Sub-classes should override this method to interact with the user about new feeds.
87 If multiple feeds need confirmation, L{trust.TrustMgr.confirm_keys} will only invoke one instance of this
88 method at a time.
89 @param pending: the new feed to be imported
90 @type pending: L{PendingFeed}
91 @param valid_sigs: maps signatures to a list of fetchers collecting information about the key
92 @type valid_sigs: {L{gpg.ValidSig} : L{fetch.KeyInfoFetcher}}
93 @since: 0.42"""
94 from zeroinstall.injector import trust
96 assert valid_sigs
98 domain = trust.domain_from_url(pending.url)
100 # Ask on stderr, because we may be writing XML to stdout
101 print(_("Feed: %s") % pending.url, file=sys.stderr)
102 print(_("The feed is correctly signed with the following keys:"), file=sys.stderr)
103 for x in valid_sigs:
104 print("-", x, file=sys.stderr)
106 def text(parent):
107 text = ""
108 for node in parent.childNodes:
109 if node.nodeType == node.TEXT_NODE:
110 text = text + node.data
111 return text
113 shown = set()
114 key_info_fetchers = valid_sigs.values()
115 while key_info_fetchers:
116 old_kfs = key_info_fetchers
117 key_info_fetchers = []
118 for kf in old_kfs:
119 infos = set(kf.info) - shown
120 if infos:
121 if len(valid_sigs) > 1:
122 print("%s: " % kf.fingerprint)
123 for key_info in infos:
124 print("-", text(key_info), file=sys.stderr)
125 shown.add(key_info)
126 if kf.blocker:
127 key_info_fetchers.append(kf)
128 if key_info_fetchers:
129 for kf in key_info_fetchers: print(kf.status, file=sys.stderr)
130 stdin = tasks.InputBlocker(0, 'console')
131 blockers = [kf.blocker for kf in key_info_fetchers] + [stdin]
132 yield blockers
133 for b in blockers:
134 try:
135 tasks.check(b)
136 except Exception as ex:
137 warn(_("Failed to get key info: %s"), ex)
138 if stdin.happened:
139 print(_("Skipping remaining key lookups due to input from user"), file=sys.stderr)
140 break
141 if not shown:
142 print(_("Warning: Nothing known about this key!"), file=sys.stderr)
144 if len(valid_sigs) == 1:
145 print(_("Do you want to trust this key to sign feeds from '%s'?") % domain, file=sys.stderr)
146 else:
147 print(_("Do you want to trust all of these keys to sign feeds from '%s'?") % domain, file=sys.stderr)
148 while True:
149 print(_("Trust [Y/N] "), end=' ', file=sys.stderr)
150 i = raw_input()
151 if not i: continue
152 if i in 'Nn':
153 raise NoTrustedKeys(_('Not signed with a trusted key'))
154 if i in 'Yy':
155 break
156 for key in valid_sigs:
157 print(_("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key.fingerprint, 'domain': domain}, file=sys.stderr)
158 trust.trust_db.trust_key(key.fingerprint, domain)
160 @tasks.async
161 def confirm_install(self, msg):
162 """We need to check something with the user before continuing with the install.
163 @raise download.DownloadAborted: if the user cancels"""
164 yield
165 print(msg, file=sys.stderr)
166 while True:
167 sys.stderr.write(_("Install [Y/N] "))
168 i = raw_input()
169 if not i: continue
170 if i in 'Nn':
171 raise download.DownloadAborted()
172 if i in 'Yy':
173 break
175 def report_error(self, exception, tb = None):
176 """Report an exception to the user.
177 @param exception: the exception to report
178 @type exception: L{SafeException}
179 @param tb: optional traceback
180 @since: 0.25"""
181 warn("%s", str(exception) or type(exception))
182 #import traceback
183 #traceback.print_exception(exception, None, tb)
185 class ConsoleHandler(Handler):
186 """A Handler that displays progress on stdout (a tty).
187 @since: 0.44"""
188 last_msg_len = None
189 update = None
190 disable_progress = 0
191 screen_width = None
193 def downloads_changed(self):
194 import gobject
195 if self.monitored_downloads and self.update is None:
196 if self.screen_width is None:
197 try:
198 import curses
199 curses.setupterm()
200 self.screen_width = curses.tigetnum('cols') or 80
201 except Exception as ex:
202 info("Failed to initialise curses library: %s", ex)
203 self.screen_width = 80
204 self.show_progress()
205 self.update = gobject.timeout_add(200, self.show_progress)
206 elif len(self.monitored_downloads) == 0:
207 if self.update:
208 gobject.source_remove(self.update)
209 self.update = None
210 print()
211 self.last_msg_len = None
213 def show_progress(self):
214 if not self.monitored_downloads: return True
215 urls = [(dl.url, dl) for dl in self.monitored_downloads]
217 if self.disable_progress: return True
219 screen_width = self.screen_width - 2
220 item_width = max(16, screen_width / len(self.monitored_downloads))
221 url_width = item_width - 7
223 msg = ""
224 for url, dl in sorted(urls):
225 so_far = dl.get_bytes_downloaded_so_far()
226 leaf = url.rsplit('/', 1)[-1]
227 if len(leaf) >= url_width:
228 display = leaf[:url_width]
229 else:
230 display = url[-url_width:]
231 if dl.expected_size:
232 msg += "[%s %d%%] " % (display, int(so_far * 100 / dl.expected_size))
233 else:
234 msg += "[%s] " % (display)
235 msg = msg[:screen_width]
237 if self.last_msg_len is None:
238 sys.stdout.write(msg)
239 else:
240 sys.stdout.write(chr(13) + msg)
241 if len(msg) < self.last_msg_len:
242 sys.stdout.write(" " * (self.last_msg_len - len(msg)))
244 self.last_msg_len = len(msg)
245 sys.stdout.flush()
247 return True
249 def clear_display(self):
250 if self.last_msg_len != None:
251 sys.stdout.write(chr(13) + " " * self.last_msg_len + chr(13))
252 sys.stdout.flush()
253 self.last_msg_len = None
255 def report_error(self, exception, tb = None):
256 self.clear_display()
257 Handler.report_error(self, exception, tb)
259 def confirm_import_feed(self, pending, valid_sigs):
260 self.clear_display()
261 self.disable_progress += 1
262 blocker = Handler.confirm_import_feed(self, pending, valid_sigs)
263 @tasks.async
264 def enable():
265 yield blocker
266 self.disable_progress -= 1
267 self.show_progress()
268 enable()
269 return blocker