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 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."""
25 class Handler(object):
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
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."""
51 self
.monitored_downloads
[dl
.url
] = dl
52 self
.downloads_changed()
55 def download_done_stats():
57 # NB: we don't check for exceptions here; someone else should be doing that
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()
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}
75 def downloads_changed(self
):
76 """This is just for the GUI to override to update its display."""
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
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
90 @rtype: L{download.Download}
93 raise NeedDownload(url
)
96 dl
= self
.monitored_downloads
[url
]
102 dl
= download
.Download(url
, hint
)
104 dl
= factory(url
, hint
)
105 self
.monitor_download(dl
)
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
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}}
118 from zeroinstall
.injector
import trust
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:")
128 print >>sys
.stderr
, "-", unicode(x
).encode('ascii', 'xmlcharrefreplace')
132 for node
in parent
.childNodes
:
133 if node
.nodeType
== node
.TEXT_NODE
:
134 text
= text
+ node
.data
138 key_info_fetchers
= valid_sigs
.values()
139 while key_info_fetchers
:
140 old_kfs
= key_info_fetchers
141 key_info_fetchers
= []
143 infos
= set(kf
.info
) - shown
145 if len(valid_sigs
) > 1:
146 print "%s: " % kf
.fingerprint
147 for key_info
in infos
:
148 print >>sys
.stderr
, "-", text(key_info
)
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]
160 except Exception, ex
:
161 warn(_("Failed to get key info: %s"), ex
)
163 # print >>sys.stderr, _("Skipping remaining key lookups due to input from user")
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
171 print >>sys
.stderr
, _("Do you want to trust all of these keys to sign feeds from '%s'?") % domain
173 print >>sys
.stderr
, _("Trust [Y/N] ")
178 raise NoTrustedKeys(_('Not signed with a trusted key'))
181 for key
in valid_sigs
:
182 print >>sys
.stderr
, _("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key
.fingerprint
, 'domain': domain
}
183 trust
.trust_db
.trust_key(key
.fingerprint
, domain
)
186 def confirm_install(self
, msg
):
187 """We need to check something with the user before continuing with the install.
188 @raise download.DownloadAborted: if the user cancels"""
190 print >>sys
.stderr
, msg
192 sys
.stderr
.write(_("Install [Y/N] "))
197 raise download
.DownloadAborted()
201 def report_error(self
, exception
, tb
= None):
202 """Report an exception to the user.
203 @param exception: the exception to report
204 @type exception: L{SafeException}
205 @param tb: optional traceback
207 warn("%s", str(exception
) or type(exception
))
209 #traceback.print_exception(exception, None, tb)
211 class ConsoleHandler(Handler
):
212 """A Handler that displays progress on stdout (a tty).
219 def downloads_changed(self
):
221 if self
.monitored_downloads
and self
.update
is None:
222 if self
.screen_width
is None:
226 self
.screen_width
= curses
.tigetnum('cols') or 80
227 except Exception, ex
:
228 info("Failed to initialise curses library: %s", ex
)
229 self
.screen_width
= 80
231 self
.update
= gobject
.timeout_add(200, self
.show_progress
)
232 elif len(self
.monitored_downloads
) == 0:
234 gobject
.source_remove(self
.update
)
237 self
.last_msg_len
= None
239 def show_progress(self
):
240 urls
= self
.monitored_downloads
.keys()
241 if not urls
: return True
243 if self
.disable_progress
: return True
245 screen_width
= self
.screen_width
- 2
246 item_width
= max(16, screen_width
/ len(self
.monitored_downloads
))
247 url_width
= item_width
- 7
250 for url
in sorted(urls
):
251 dl
= self
.monitored_downloads
[url
]
252 so_far
= dl
.get_bytes_downloaded_so_far()
253 leaf
= url
.rsplit('/', 1)[-1]
254 if len(leaf
) >= url_width
:
255 display
= leaf
[:url_width
]
257 display
= url
[-url_width
:]
259 msg
+= "[%s %d%%] " % (display
, int(so_far
* 100 / dl
.expected_size
))
261 msg
+= "[%s] " % (display
)
262 msg
= msg
[:screen_width
]
264 if self
.last_msg_len
is None:
265 sys
.stdout
.write(msg
)
267 sys
.stdout
.write(chr(13) + msg
)
268 if len(msg
) < self
.last_msg_len
:
269 sys
.stdout
.write(" " * (self
.last_msg_len
- len(msg
)))
271 self
.last_msg_len
= len(msg
)
276 def clear_display(self
):
277 if self
.last_msg_len
!= None:
278 sys
.stdout
.write(chr(13) + " " * self
.last_msg_len
+ chr(13))
280 self
.last_msg_len
= None
282 def report_error(self
, exception
, tb
= None):
284 Handler
.report_error(self
, exception
, tb
)
286 def confirm_import_feed(self
, pending
, valid_sigs
):
288 self
.disable_progress
+= 1
289 blocker
= Handler
.confirm_import_feed(self
, pending
, valid_sigs
)
293 self
.disable_progress
-= 1
298 class BatchHandler(Handler
):
299 """A Handler that writes easily parseable data to stderr."""
301 def confirm_import_feed(self
, pending
, valid_sigs
):
302 print >>sys
.stderr
, "QUESTION:"
303 return Handler
.confirm_import_feed(self
, pending
, valid_sigs
)
305 def confirm_trust_keys(self
, interface
, sigs
, iface_xml
):
306 print >>sys
.stderr
, "QUESTION:"
307 return Handler
.confirm_trust_keys(self
, interface
, sigs
, iface_xml
)