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}.
10 # Copyright (C) 2009, Thomas Leonard
11 # See the README file for details, or visit http://0install.net.
13 from zeroinstall
import _
15 from logging
import debug
, 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_trust_keys} on failure."""
25 class Handler(object):
27 This implementation uses the GLib mainloop. Note that QT4 can use the GLib mainloop too.
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
39 __slots__
= ['monitored_downloads', '_loop', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads', '_current_confirm']
41 def __init__(self
, mainloop
= None, dry_run
= False):
42 self
.monitored_downloads
= {}
44 self
.dry_run
= dry_run
45 self
.n_completed_downloads
= 0
46 self
.total_bytes_downloaded
= 0
47 self
._current
_confirm
= None
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."""
53 self
.monitored_downloads
[dl
.url
] = dl
54 self
.downloads_changed()
57 def download_done_stats():
59 # NB: we don't check for exceptions here; someone else should be doing that
61 self
.n_completed_downloads
+= 1
62 self
.total_bytes_downloaded
+= dl
.get_bytes_downloaded_so_far()
63 del self
.monitored_downloads
[dl
.url
]
64 self
.downloads_changed()
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}
77 def downloads_changed(self
):
78 """This is just for the GUI to override to update its display."""
81 def wait_for_blocker(self
, blocker
):
82 """Run a recursive mainloop until blocker is triggered.
83 @param blocker: event to wait on
84 @type blocker: L{tasks.Blocker}"""
85 if not blocker
.happened
:
91 tasks
.Task(quitter(), "quitter")
93 assert self
._loop
is None # Avoid recursion
94 self
._loop
= gobject
.MainLoop(gobject
.main_context_default())
96 debug(_("Entering mainloop, waiting for %s"), blocker
)
101 assert blocker
.happened
, "Someone quit the main loop!"
105 def get_download(self
, url
, force
= False, hint
= None, factory
= None):
106 """Return the Download object currently downloading 'url'.
107 If no download for this URL has been started, start one now (and
108 start monitoring it).
109 If the download failed and force is False, return it anyway.
110 If force is True, abort any current or failed download and start
112 @rtype: L{download.Download}
115 raise NeedDownload(url
)
118 dl
= self
.monitored_downloads
[url
]
124 dl
= download
.Download(url
, hint
)
126 dl
= factory(url
, hint
)
127 self
.monitor_download(dl
)
130 def confirm_keys(self
, pending
, fetch_key_info
):
131 """We don't trust any of the signatures yet. Ask the user.
132 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
133 This method just calls L{confirm_import_feed} if the handler (self) is
134 new-style, or L{confirm_trust_keys} for older classes. A class
135 is considered old-style if it overrides confirm_trust_keys and
136 not confirm_import_feed.
138 @arg pending: an object holding details of the updated feed
139 @type pending: L{PendingFeed}
140 @arg fetch_key_info: a function which can be used to fetch information about a key fingerprint
141 @type fetch_key_info: str -> L{Blocker}
142 @return: A blocker that triggers when the user has chosen, or None if already done.
143 @rtype: None | L{Blocker}"""
147 if hasattr(self
.confirm_trust_keys
, 'original') or not hasattr(self
.confirm_import_feed
, 'original'):
149 from zeroinstall
.injector
import gpg
150 valid_sigs
= [s
for s
in pending
.sigs
if isinstance(s
, gpg
.ValidSig
)]
155 msg
+= "\nMessages from GPG:\n" + sig
.messages
157 raise SafeException(_('No valid signatures found on "%(url)s". Signatures:%(signatures)s') %
158 {'url': pending
.url
, 'signatures': ''.join(['\n- ' + format_sig(s
) for s
in pending
.sigs
])})
160 # Start downloading information about the keys...
162 for sig
in valid_sigs
:
163 kfs
[sig
] = fetch_key_info(sig
.fingerprint
)
165 return self
._queue
_confirm
_import
_feed
(pending
, kfs
)
168 from zeroinstall
.injector
import iface_cache
170 warnings
.warn("Should override confirm_import_feed(); using old confirm_trust_keys() for now", DeprecationWarning, stacklevel
= 2)
172 iface
= iface_cache
.iface_cache
.get_interface(pending
.url
)
173 return self
.confirm_trust_keys(iface
, pending
.sigs
, pending
.new_xml
)
176 def _queue_confirm_import_feed(self
, pending
, valid_sigs
):
177 # If we're already confirming something else, wait for that to finish...
178 while self
._current
_confirm
is not None:
179 yield self
._current
_confirm
181 # Check whether we still need to confirm. The user may have
182 # already approved one of the keys while dealing with another
184 from zeroinstall
.injector
import trust
185 domain
= trust
.domain_from_url(pending
.url
)
186 for sig
in valid_sigs
:
187 is_trusted
= trust
.trust_db
.is_trusted(sig
.fingerprint
, domain
)
191 # Take the lock and confirm this feed
192 self
._current
_confirm
= lock
= tasks
.Blocker('confirm key lock')
194 done
= self
.confirm_import_feed(pending
, valid_sigs
)
199 self
._current
_confirm
= None
203 def confirm_import_feed(self
, pending
, valid_sigs
):
204 """Sub-classes should override this method to interact with the user about new feeds.
205 If multiple feeds need confirmation, L{confirm_keys} will only invoke one instance of this
207 @param pending: the new feed to be imported
208 @type pending: L{PendingFeed}
209 @param valid_sigs: maps signatures to a list of fetchers collecting information about the key
210 @type valid_sigs: {L{gpg.ValidSig} : L{fetch.KeyInfoFetcher}}
212 @see: L{confirm_keys}"""
213 from zeroinstall
.injector
import trust
217 domain
= trust
.domain_from_url(pending
.url
)
219 # Ask on stderr, because we may be writing XML to stdout
220 print >>sys
.stderr
, _("Feed: %s") % pending
.url
221 print >>sys
.stderr
, _("The feed is correctly signed with the following keys:")
223 print >>sys
.stderr
, "-", x
227 for node
in parent
.childNodes
:
228 if node
.nodeType
== node
.TEXT_NODE
:
229 text
= text
+ node
.data
233 key_info_fetchers
= valid_sigs
.values()
234 while key_info_fetchers
:
235 old_kfs
= key_info_fetchers
236 key_info_fetchers
= []
238 infos
= set(kf
.info
) - shown
240 if len(valid_sigs
) > 1:
241 print "%s: " % kf
.fingerprint
242 for key_info
in infos
:
243 print >>sys
.stderr
, "-", text(key_info
)
246 key_info_fetchers
.append(kf
)
247 if key_info_fetchers
:
248 for kf
in key_info_fetchers
: print >>sys
.stderr
, kf
.status
249 stdin
= tasks
.InputBlocker(0, 'console')
250 blockers
= [kf
.blocker
for kf
in key_info_fetchers
] + [stdin
]
255 except Exception, ex
:
256 warn(_("Failed to get key info: %s"), ex
)
258 print >>sys
.stderr
, _("Skipping remaining key lookups due to input from user")
261 if len(valid_sigs
) == 1:
262 print >>sys
.stderr
, _("Do you want to trust this key to sign feeds from '%s'?") % domain
264 print >>sys
.stderr
, _("Do you want to trust all of these keys to sign feeds from '%s'?") % domain
266 print >>sys
.stderr
, _("Trust [Y/N] "),
270 raise NoTrustedKeys(_('Not signed with a trusted key'))
273 for key
in valid_sigs
:
274 print >>sys
.stderr
, _("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key
.fingerprint
, 'domain': domain
}
275 trust
.trust_db
.trust_key(key
.fingerprint
, domain
)
277 confirm_import_feed
.original
= True
279 def confirm_trust_keys(self
, interface
, sigs
, iface_xml
):
280 """We don't trust any of the signatures yet. Ask the user.
281 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
282 @deprecated: see L{confirm_keys}
283 @arg interface: the interface being updated
284 @arg sigs: a list of signatures (from L{gpg.check_stream})
285 @arg iface_xml: the downloaded data (not yet trusted)
286 @return: a blocker, if confirmation will happen asynchronously, or None
287 @rtype: L{tasks.Blocker}"""
289 warnings
.warn("Use confirm_keys, not confirm_trust_keys", DeprecationWarning, stacklevel
= 2)
290 from zeroinstall
.injector
import trust
, gpg
292 valid_sigs
= [s
for s
in sigs
if isinstance(s
, gpg
.ValidSig
)]
294 raise SafeException('No valid signatures found on "%s". Signatures:%s' %
295 (interface
.uri
, ''.join(['\n- ' + str(s
) for s
in sigs
])))
297 domain
= trust
.domain_from_url(interface
.uri
)
299 # Ask on stderr, because we may be writing XML to stdout
300 print >>sys
.stderr
, "\nInterface:", interface
.uri
301 print >>sys
.stderr
, _("The feed is correctly signed with the following keys:")
303 print >>sys
.stderr
, "-", x
305 if len(valid_sigs
) == 1:
306 print >>sys
.stderr
, _("Do you want to trust this key to sign feeds from '%s'?") % domain
308 print >>sys
.stderr
, _("Do you want to trust all of these keys to sign feeds from '%s'?") % domain
310 print >>sys
.stderr
, _("Trust [Y/N] "),
314 raise NoTrustedKeys(_('Not signed with a trusted key'))
317 for key
in valid_sigs
:
318 print >>sys
.stderr
, _("Trusting %s for %s") % (key
.fingerprint
, domain
)
319 trust
.trust_db
.trust_key(key
.fingerprint
, domain
)
321 trust
.trust_db
.notify()
323 confirm_trust_keys
.original
= True # Detect if someone overrides it
326 def confirm_install(self
, msg
):
327 """We need to check something with the user before continuing with the install.
328 @raise download.DownloadAborted: if the user cancels"""
330 print >>sys
.stderr
, msg
332 sys
.stderr
.write(_("Install [Y/N] "))
336 raise download
.DownloadAborted()
340 def report_error(self
, exception
, tb
= None):
341 """Report an exception to the user.
342 @param exception: the exception to report
343 @type exception: L{SafeException}
344 @param tb: optional traceback
346 warn("%s", str(exception
) or type(exception
))
348 #traceback.print_exception(exception, None, tb)
350 class ConsoleHandler(Handler
):
351 """A Handler that displays progress on stdout (a tty).
358 def downloads_changed(self
):
360 if self
.monitored_downloads
and self
.update
is None:
361 if self
.screen_width
is None:
365 self
.screen_width
= curses
.tigetnum('cols') or 80
366 except Exception, ex
:
367 info("Failed to initialise curses library: %s", ex
)
368 self
.screen_width
= 80
370 self
.update
= gobject
.timeout_add(200, self
.show_progress
)
371 elif len(self
.monitored_downloads
) == 0:
373 gobject
.source_remove(self
.update
)
376 self
.last_msg_len
= None
378 def show_progress(self
):
379 urls
= self
.monitored_downloads
.keys()
380 if not urls
: return True
382 if self
.disable_progress
: return True
384 screen_width
= self
.screen_width
- 2
385 item_width
= max(16, screen_width
/ len(self
.monitored_downloads
))
386 url_width
= item_width
- 7
389 for url
in sorted(urls
):
390 dl
= self
.monitored_downloads
[url
]
391 so_far
= dl
.get_bytes_downloaded_so_far()
392 leaf
= url
.rsplit('/', 1)[-1]
393 if len(leaf
) >= url_width
:
394 display
= leaf
[:url_width
]
396 display
= url
[-url_width
:]
398 msg
+= "[%s %d%%] " % (display
, int(so_far
* 100 / dl
.expected_size
))
400 msg
+= "[%s] " % (display
)
401 msg
= msg
[:screen_width
]
403 if self
.last_msg_len
is None:
404 sys
.stdout
.write(msg
)
406 sys
.stdout
.write(chr(13) + msg
)
407 if len(msg
) < self
.last_msg_len
:
408 sys
.stdout
.write(" " * (self
.last_msg_len
- len(msg
)))
410 self
.last_msg_len
= len(msg
)
415 def clear_display(self
):
416 if self
.last_msg_len
!= None:
417 sys
.stdout
.write(chr(13) + " " * self
.last_msg_len
+ chr(13))
419 self
.last_msg_len
= None
421 def report_error(self
, exception
, tb
= None):
423 Handler
.report_error(self
, exception
, tb
)
425 def confirm_import_feed(self
, pending
, valid_sigs
):
427 self
.disable_progress
+= 1
428 blocker
= Handler
.confirm_import_feed(self
, pending
, valid_sigs
)
432 self
.disable_progress
-= 1