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
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
37 __slots__
= ['monitored_downloads', '_loop', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads', '_current_confirm']
39 def __init__(self
, mainloop
= None, dry_run
= False):
40 self
.monitored_downloads
= {}
42 self
.dry_run
= dry_run
43 self
.n_completed_downloads
= 0
44 self
.total_bytes_downloaded
= 0
45 self
._current
_confirm
= None
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 """Run a recursive mainloop until blocker is triggered.
81 @param blocker: event to wait on
82 @type blocker: L{tasks.Blocker}"""
83 if not blocker
.happened
:
89 quit
= tasks
.Task(quitter(), "quitter")
91 assert self
._loop
is None # Avoid recursion
92 self
._loop
= gobject
.MainLoop(gobject
.main_context_default())
94 debug(_("Entering mainloop, waiting for %s"), blocker
)
99 assert blocker
.happened
, "Someone quit the main loop!"
103 def get_download(self
, url
, force
= False, hint
= None):
104 """Return the Download object currently downloading 'url'.
105 If no download for this URL has been started, start one now (and
106 start monitoring it).
107 If the download failed and force is False, return it anyway.
108 If force is True, abort any current or failed download and start
110 @rtype: L{download.Download}
113 raise NeedDownload(url
)
116 dl
= self
.monitored_downloads
[url
]
121 dl
= download
.Download(url
, hint
)
122 self
.monitor_download(dl
)
125 def confirm_keys(self
, pending
, fetch_key_info
):
126 """We don't trust any of the signatures yet. Ask the user.
127 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
128 This method just calls L{confirm_import_feed} if the handler (self) is
129 new-style, or L{confirm_trust_keys} for older classes. A class
130 is considered old-style if it overrides confirm_trust_keys and
131 not confirm_import_feed.
133 @arg pending: an object holding details of the updated feed
134 @type pending: L{PendingFeed}
135 @arg fetch_key_info: a function which can be used to fetch information about a key fingerprint
136 @type fetch_key_info: str -> L{Blocker}
137 @return: A blocker that triggers when the user has chosen, or None if already done.
138 @rtype: None | L{Blocker}"""
142 if hasattr(self
.confirm_trust_keys
, 'original') or not hasattr(self
.confirm_import_feed
, 'original'):
144 from zeroinstall
.injector
import gpg
145 valid_sigs
= [s
for s
in pending
.sigs
if isinstance(s
, gpg
.ValidSig
)]
147 raise SafeException('No valid signatures found on "%s". Signatures:%s' %
148 (pending
.url
, ''.join(['\n- ' + str(s
) for s
in pending
.sigs
])))
150 # Start downloading information about the keys...
152 for sig
in valid_sigs
:
153 kfs
[sig
] = fetch_key_info(sig
.fingerprint
)
155 return self
._queue
_confirm
_import
_feed
(pending
, kfs
)
158 from zeroinstall
.injector
import iface_cache
160 warnings
.warn(_("Should override confirm_import_feed(); using old confirm_trust_keys() for now"), DeprecationWarning, stacklevel
= 2)
162 iface
= iface_cache
.iface_cache
.get_interface(pending
.url
)
163 return self
.confirm_trust_keys(iface
, pending
.sigs
, pending
.new_xml
)
166 def _queue_confirm_import_feed(self
, pending
, valid_sigs
):
167 # If we're already confirming something else, wait for that to finish...
168 while self
._current
_confirm
is not None:
169 yield self
._current
_confirm
171 self
._current
_confirm
= lock
= tasks
.Blocker('confirm key lock')
173 done
= self
.confirm_import_feed(pending
, valid_sigs
)
177 self
._current
_confirm
= None
181 def confirm_import_feed(self
, pending
, valid_sigs
):
182 """Sub-classes should override this method to interact with the user about new feeds.
183 If multiple feeds need confirmation, L{confirm_keys} will only invoke one instance of this
185 @param pending: the new feed to be imported
186 @type pending: L{PendingFeed}
187 @param valid_sigs: maps signatures to a list of fetchers collecting information about the key
188 @type valid_sigs: {L{gpg.ValidSig} : L{fetch.KeyInfoFetcher}}
190 @see: L{confirm_keys}"""
191 from zeroinstall
.injector
import trust
195 domain
= trust
.domain_from_url(pending
.url
)
197 # Ask on stderr, because we may be writing XML to stdout
198 print >>sys
.stderr
, "\nFeed:", pending
.url
199 print >>sys
.stderr
, "The feed is correctly signed with the following keys:"
201 print >>sys
.stderr
, "-", x
205 for node
in parent
.childNodes
:
206 if node
.nodeType
== node
.TEXT_NODE
:
207 text
= text
+ node
.data
211 key_info_fetchers
= valid_sigs
.values()
212 while key_info_fetchers
:
213 old_kfs
= key_info_fetchers
214 key_info_fetchers
= []
216 infos
= set(kf
.info
) - shown
218 if len(valid_sigs
) > 1:
219 print "%s: " % kf
.fingerprint
221 print >>sys
.stderr
, "-", text(info
)
224 key_info_fetchers
.append(kf
)
225 if key_info_fetchers
:
226 for kf
in key_info_fetchers
: print >>sys
.stderr
, kf
.status
227 stdin
= tasks
.InputBlocker(0, 'console')
228 blockers
= [kf
.blocker
for kf
in key_info_fetchers
] + [stdin
]
233 except Exception, ex
:
234 warn("Failed to get key info: %s", ex
)
236 print >>sys
.stderr
, "Skipping remaining key lookups due to input from user"
239 if len(valid_sigs
) == 1:
240 print >>sys
.stderr
, "Do you want to trust this key to sign feeds from '%s'?" % domain
242 print >>sys
.stderr
, "Do you want to trust all of these keys to sign feeds from '%s'?" % domain
244 print >>sys
.stderr
, "Trust [Y/N] ",
248 raise NoTrustedKeys(_('Not signed with a trusted key'))
251 for key
in valid_sigs
:
252 print >>sys
.stderr
, "Trusting", key
.fingerprint
, "for", domain
253 trust
.trust_db
.trust_key(key
.fingerprint
, domain
)
255 confirm_import_feed
.original
= True
257 def confirm_trust_keys(self
, interface
, sigs
, iface_xml
):
258 """We don't trust any of the signatures yet. Ask the user.
259 When done update the L{trust} database, and then call L{trust.TrustDB.notify}.
260 @deprecated: see L{confirm_keys}
261 @arg interface: the interface being updated
262 @arg sigs: a list of signatures (from L{gpg.check_stream})
263 @arg iface_xml: the downloaded data (not yet trusted)
264 @return: a blocker, if confirmation will happen asynchronously, or None
265 @rtype: L{tasks.Blocker}"""
267 warnings
.warn(_("Use confirm_keys, not confirm_trust_keys"), DeprecationWarning, stacklevel
= 2)
268 from zeroinstall
.injector
import trust
, gpg
270 valid_sigs
= [s
for s
in sigs
if isinstance(s
, gpg
.ValidSig
)]
272 raise SafeException('No valid signatures found on "%s". Signatures:%s' %
273 (interface
.uri
, ''.join(['\n- ' + str(s
) for s
in sigs
])))
275 domain
= trust
.domain_from_url(interface
.uri
)
277 # Ask on stderr, because we may be writing XML to stdout
278 print >>sys
.stderr
, "\nInterface:", interface
.uri
279 print >>sys
.stderr
, "The interface is correctly signed with the following keys:"
281 print >>sys
.stderr
, "-", x
283 if len(valid_sigs
) == 1:
284 print >>sys
.stderr
, "Do you want to trust this key to sign feeds from '%s'?" % domain
286 print >>sys
.stderr
, "Do you want to trust all of these keys to sign feeds from '%s'?" % domain
288 print >>sys
.stderr
, "Trust [Y/N] ",
292 raise NoTrustedKeys(_('Not signed with a trusted key'))
295 for key
in valid_sigs
:
296 print >>sys
.stderr
, "Trusting", key
.fingerprint
, "for", domain
297 trust
.trust_db
.trust_key(key
.fingerprint
, domain
)
299 trust
.trust_db
.notify()
301 confirm_trust_keys
.original
= True # Detect if someone overrides it
303 def report_error(self
, exception
, tb
= None):
304 """Report an exception to the user.
305 @param exception: the exception to report
306 @type exception: L{SafeException}
307 @param tb: optional traceback
309 warn("%s", str(exception
) or type(exception
))
311 #traceback.print_exception(exception, None, tb)