Merged changes from master
[zeroinstall/solver.git] / zeroinstall / injector / handler.py
blobe9de93d52ddd729e55c03ea1fecec753a7e76069
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 _, logger
16 import sys
18 from zeroinstall import SafeException
19 from zeroinstall import support
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).encode('ascii', 'xmlcharrefreplace'), 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 logger.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]", file=sys.stderr)
150 sys.stderr.flush()
151 i = support.raw_input()
152 if not i: continue
153 if i in 'Nn':
154 raise NoTrustedKeys(_('Not signed with a trusted key'))
155 if i in 'Yy':
156 break
157 for key in valid_sigs:
158 print(_("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key.fingerprint, 'domain': domain}, file=sys.stderr)
159 trust.trust_db.trust_key(key.fingerprint, domain)
161 @tasks.async
162 def confirm_install(self, msg):
163 """We need to check something with the user before continuing with the install.
164 @raise download.DownloadAborted: if the user cancels"""
165 yield
166 print(msg, file=sys.stderr)
167 while True:
168 print("Install [Y/N]", file=sys.stderr)
169 sys.stderr.flush()
170 i = support.raw_input()
171 if not i: continue
172 if i in 'Nn':
173 raise download.DownloadAborted()
174 if i in 'Yy':
175 break
177 def report_error(self, exception, tb = None):
178 """Report an exception to the user.
179 @param exception: the exception to report
180 @type exception: L{SafeException}
181 @param tb: optional traceback
182 @since: 0.25"""
183 logger.warn("%s", str(exception) or type(exception))
184 #import traceback
185 #traceback.print_exception(exception, None, tb)
187 class ConsoleHandler(Handler):
188 """A Handler that displays progress on stdout (a tty).
189 @since: 0.44"""
190 last_msg_len = None
191 update = None
192 disable_progress = 0
193 screen_width = None
195 def downloads_changed(self):
196 from zeroinstall import gobject
197 if self.monitored_downloads and self.update is None:
198 if self.screen_width is None:
199 try:
200 import curses
201 curses.setupterm()
202 self.screen_width = curses.tigetnum('cols') or 80
203 except Exception as ex:
204 logger.info("Failed to initialise curses library: %s", ex)
205 self.screen_width = 80
206 self.show_progress()
207 self.update = gobject.timeout_add(200, self.show_progress)
208 elif len(self.monitored_downloads) == 0:
209 if self.update:
210 gobject.source_remove(self.update)
211 self.update = None
212 print()
213 self.last_msg_len = None
215 def show_progress(self):
216 if not self.monitored_downloads: return True
217 urls = [(dl.url, dl) for dl in self.monitored_downloads]
219 if self.disable_progress: return True
221 screen_width = self.screen_width - 2
222 item_width = max(16, screen_width // len(self.monitored_downloads))
223 url_width = item_width - 7
225 msg = ""
226 for url, dl in sorted(urls):
227 so_far = dl.get_bytes_downloaded_so_far()
228 leaf = url.rsplit('/', 1)[-1]
229 if len(leaf) >= url_width:
230 display = leaf[:url_width]
231 else:
232 display = url[-url_width:]
233 if dl.expected_size:
234 msg += "[%s %d%%] " % (display, int(so_far * 100 / dl.expected_size))
235 else:
236 msg += "[%s] " % (display)
237 msg = msg[:screen_width]
239 if self.last_msg_len is None:
240 sys.stdout.write(msg)
241 else:
242 sys.stdout.write(chr(13) + msg)
243 if len(msg) < self.last_msg_len:
244 sys.stdout.write(" " * (self.last_msg_len - len(msg)))
246 self.last_msg_len = len(msg)
247 sys.stdout.flush()
249 return True
251 def clear_display(self):
252 if self.last_msg_len != None:
253 sys.stdout.write(chr(13) + " " * self.last_msg_len + chr(13))
254 sys.stdout.flush()
255 self.last_msg_len = None
257 def report_error(self, exception, tb = None):
258 self.clear_display()
259 Handler.report_error(self, exception, tb)
261 def confirm_import_feed(self, pending, valid_sigs):
262 self.clear_display()
263 self.disable_progress += 1
264 blocker = Handler.confirm_import_feed(self, pending, valid_sigs)
265 @tasks.async
266 def enable():
267 yield blocker
268 self.disable_progress -= 1
269 self.show_progress()
270 enable()
271 return blocker
273 class BatchHandler(Handler):
274 """A Handler that writes easily parseable data to stderr."""
276 def confirm_import_feed(self, pending, valid_sigs):
277 print("QUESTION:", file=sys.stderr)
278 return Handler.confirm_import_feed(self, pending, valid_sigs)
280 def confirm_trust_keys(self, interface, sigs, iface_xml):
281 print("QUESTION:", file=sys.stderr)
282 return Handler.confirm_trust_keys(self, interface, sigs, iface_xml)