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 __future__
import print_function
15 from zeroinstall
import _
17 from logging
import warn
, info
19 from zeroinstall
import SafeException
20 from zeroinstall
import support
21 from zeroinstall
.support
import tasks
22 from zeroinstall
.injector
import download
24 class NoTrustedKeys(SafeException
):
25 """Thrown by L{Handler.confirm_import_feed} on failure."""
28 class Handler(object):
30 A Handler is used to interact with the user (e.g. to confirm keys, display download progress, etc).
32 @ivar monitored_downloads: set of downloads in progress
33 @type monitored_downloads: {L{download.Download}}
34 @ivar n_completed_downloads: number of downloads which have finished for GUIs, etc (can be reset as desired).
35 @type n_completed_downloads: int
36 @ivar total_bytes_downloaded: informational counter for GUIs, etc (can be reset as desired). Updated when download finishes.
37 @type total_bytes_downloaded: int
38 @ivar dry_run: instead of starting a download, just report what we would have downloaded
42 __slots__
= ['monitored_downloads', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads']
44 def __init__(self
, mainloop
= None, dry_run
= False):
45 self
.monitored_downloads
= set()
46 self
.dry_run
= dry_run
47 self
.n_completed_downloads
= 0
48 self
.total_bytes_downloaded
= 0
50 def monitor_download(self
, dl
):
51 """Called when a new L{download} is started.
52 This is mainly used by the GUI to display the progress bar."""
53 self
.monitored_downloads
.add(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 self
.monitored_downloads
.remove(dl
)
64 self
.downloads_changed()
65 except Exception as ex
:
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 """@deprecated: use tasks.wait_for_blocker instead"""
83 tasks
.wait_for_blocker(blocker
)
86 def confirm_import_feed(self
, pending
, valid_sigs
):
87 """Sub-classes should override this method to interact with the user about new feeds.
88 If multiple feeds need confirmation, L{trust.TrustMgr.confirm_keys} will only invoke one instance of this
90 @param pending: the new feed to be imported
91 @type pending: L{PendingFeed}
92 @param valid_sigs: maps signatures to a list of fetchers collecting information about the key
93 @type valid_sigs: {L{gpg.ValidSig} : L{fetch.KeyInfoFetcher}}
95 from zeroinstall
.injector
import trust
99 domain
= trust
.domain_from_url(pending
.url
)
101 # Ask on stderr, because we may be writing XML to stdout
102 print(_("Feed: %s") % pending
.url
, file=sys
.stderr
)
103 print(_("The feed is correctly signed with the following keys:"), file=sys
.stderr
)
105 print("-", x
, file=sys
.stderr
)
109 for node
in parent
.childNodes
:
110 if node
.nodeType
== node
.TEXT_NODE
:
111 text
= text
+ node
.data
115 key_info_fetchers
= valid_sigs
.values()
116 while key_info_fetchers
:
117 old_kfs
= key_info_fetchers
118 key_info_fetchers
= []
120 infos
= set(kf
.info
) - shown
122 if len(valid_sigs
) > 1:
123 print("%s: " % kf
.fingerprint
)
124 for key_info
in infos
:
125 print("-", text(key_info
), file=sys
.stderr
)
128 key_info_fetchers
.append(kf
)
129 if key_info_fetchers
:
130 for kf
in key_info_fetchers
: print(kf
.status
, file=sys
.stderr
)
131 stdin
= tasks
.InputBlocker(0, 'console')
132 blockers
= [kf
.blocker
for kf
in key_info_fetchers
] + [stdin
]
137 except Exception as ex
:
138 warn(_("Failed to get key info: %s"), ex
)
140 print(_("Skipping remaining key lookups due to input from user"), file=sys
.stderr
)
143 print(_("Warning: Nothing known about this key!"), file=sys
.stderr
)
145 if len(valid_sigs
) == 1:
146 print(_("Do you want to trust this key to sign feeds from '%s'?") % domain
, file=sys
.stderr
)
148 print(_("Do you want to trust all of these keys to sign feeds from '%s'?") % domain
, file=sys
.stderr
)
150 print(_("Trust [Y/N] "), end
=' ', file=sys
.stderr
)
151 i
= support
.raw_input()
154 raise NoTrustedKeys(_('Not signed with a trusted key'))
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
)
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"""
166 print(msg
, file=sys
.stderr
)
168 sys
.stderr
.write(_("Install [Y/N] "))
172 raise download
.DownloadAborted()
176 def report_error(self
, exception
, tb
= None):
177 """Report an exception to the user.
178 @param exception: the exception to report
179 @type exception: L{SafeException}
180 @param tb: optional traceback
182 warn("%s", str(exception
) or type(exception
))
184 #traceback.print_exception(exception, None, tb)
186 class ConsoleHandler(Handler
):
187 """A Handler that displays progress on stdout (a tty).
194 def downloads_changed(self
):
195 from zeroinstall
import gobject
196 if self
.monitored_downloads
and self
.update
is None:
197 if self
.screen_width
is None:
201 self
.screen_width
= curses
.tigetnum('cols') or 80
202 except Exception as ex
:
203 info("Failed to initialise curses library: %s", ex
)
204 self
.screen_width
= 80
206 self
.update
= gobject
.timeout_add(200, self
.show_progress
)
207 elif len(self
.monitored_downloads
) == 0:
209 gobject
.source_remove(self
.update
)
212 self
.last_msg_len
= None
214 def show_progress(self
):
215 if not self
.monitored_downloads
: return True
216 urls
= [(dl
.url
, dl
) for dl
in self
.monitored_downloads
]
218 if self
.disable_progress
: return True
220 screen_width
= self
.screen_width
- 2
221 item_width
= max(16, screen_width
// len(self
.monitored_downloads
))
222 url_width
= item_width
- 7
225 for url
, dl
in sorted(urls
):
226 so_far
= dl
.get_bytes_downloaded_so_far()
227 leaf
= url
.rsplit('/', 1)[-1]
228 if len(leaf
) >= url_width
:
229 display
= leaf
[:url_width
]
231 display
= url
[-url_width
:]
233 msg
+= "[%s %d%%] " % (display
, int(so_far
* 100 / dl
.expected_size
))
235 msg
+= "[%s] " % (display
)
236 msg
= msg
[:screen_width
]
238 if self
.last_msg_len
is None:
239 sys
.stdout
.write(msg
)
241 sys
.stdout
.write(chr(13) + msg
)
242 if len(msg
) < self
.last_msg_len
:
243 sys
.stdout
.write(" " * (self
.last_msg_len
- len(msg
)))
245 self
.last_msg_len
= len(msg
)
250 def clear_display(self
):
251 if self
.last_msg_len
!= None:
252 sys
.stdout
.write(chr(13) + " " * self
.last_msg_len
+ chr(13))
254 self
.last_msg_len
= None
256 def report_error(self
, exception
, tb
= None):
258 Handler
.report_error(self
, exception
, tb
)
260 def confirm_import_feed(self
, pending
, valid_sigs
):
262 self
.disable_progress
+= 1
263 blocker
= Handler
.confirm_import_feed(self
, pending
, valid_sigs
)
267 self
.disable_progress
-= 1