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
.support
import tasks
21 from zeroinstall
.injector
import download
23 class NoTrustedKeys(SafeException
):
24 """Thrown by L{Handler.confirm_import_feed} on failure."""
27 class Handler(object):
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
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()
56 def download_done_stats():
58 # NB: we don't check for exceptions here; someone else should be doing that
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
:
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}
76 def downloads_changed(self
):
77 """This is just for the GUI to override to update its display."""
80 def wait_for_blocker(self
, blocker
):
81 """@deprecated: use tasks.wait_for_blocker instead"""
82 tasks
.wait_for_blocker(blocker
)
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
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}}
94 from zeroinstall
.injector
import trust
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
)
104 print("-", x
, file=sys
.stderr
)
108 for node
in parent
.childNodes
:
109 if node
.nodeType
== node
.TEXT_NODE
:
110 text
= text
+ node
.data
114 key_info_fetchers
= valid_sigs
.values()
115 while key_info_fetchers
:
116 old_kfs
= key_info_fetchers
117 key_info_fetchers
= []
119 infos
= set(kf
.info
) - shown
121 if len(valid_sigs
) > 1:
122 print("%s: " % kf
.fingerprint
)
123 for key_info
in infos
:
124 print("-", text(key_info
), file=sys
.stderr
)
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
]
136 except Exception as ex
:
137 warn(_("Failed to get key info: %s"), ex
)
139 print(_("Skipping remaining key lookups due to input from user"), file=sys
.stderr
)
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
)
147 print(_("Do you want to trust all of these keys to sign feeds from '%s'?") % domain
, file=sys
.stderr
)
149 print(_("Trust [Y/N] "), end
=' ', file=sys
.stderr
)
153 raise NoTrustedKeys(_('Not signed with a trusted key'))
156 for key
in valid_sigs
:
157 print(_("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key
.fingerprint
, 'domain': domain
}, file=sys
.stderr
)
158 trust
.trust_db
.trust_key(key
.fingerprint
, domain
)
161 def confirm_install(self
, msg
):
162 """We need to check something with the user before continuing with the install.
163 @raise download.DownloadAborted: if the user cancels"""
165 print(msg
, file=sys
.stderr
)
167 sys
.stderr
.write(_("Install [Y/N] "))
171 raise download
.DownloadAborted()
175 def report_error(self
, exception
, tb
= None):
176 """Report an exception to the user.
177 @param exception: the exception to report
178 @type exception: L{SafeException}
179 @param tb: optional traceback
181 warn("%s", str(exception
) or type(exception
))
183 #traceback.print_exception(exception, None, tb)
185 class ConsoleHandler(Handler
):
186 """A Handler that displays progress on stdout (a tty).
193 def downloads_changed(self
):
195 if self
.monitored_downloads
and self
.update
is None:
196 if self
.screen_width
is None:
200 self
.screen_width
= curses
.tigetnum('cols') or 80
201 except Exception as ex
:
202 info("Failed to initialise curses library: %s", ex
)
203 self
.screen_width
= 80
205 self
.update
= gobject
.timeout_add(200, self
.show_progress
)
206 elif len(self
.monitored_downloads
) == 0:
208 gobject
.source_remove(self
.update
)
211 self
.last_msg_len
= None
213 def show_progress(self
):
214 if not self
.monitored_downloads
: return True
215 urls
= [(dl
.url
, dl
) for dl
in self
.monitored_downloads
]
217 if self
.disable_progress
: return True
219 screen_width
= self
.screen_width
- 2
220 item_width
= max(16, screen_width
/ len(self
.monitored_downloads
))
221 url_width
= item_width
- 7
224 for url
, dl
in sorted(urls
):
225 so_far
= dl
.get_bytes_downloaded_so_far()
226 leaf
= url
.rsplit('/', 1)[-1]
227 if len(leaf
) >= url_width
:
228 display
= leaf
[:url_width
]
230 display
= url
[-url_width
:]
232 msg
+= "[%s %d%%] " % (display
, int(so_far
* 100 / dl
.expected_size
))
234 msg
+= "[%s] " % (display
)
235 msg
= msg
[:screen_width
]
237 if self
.last_msg_len
is None:
238 sys
.stdout
.write(msg
)
240 sys
.stdout
.write(chr(13) + msg
)
241 if len(msg
) < self
.last_msg_len
:
242 sys
.stdout
.write(" " * (self
.last_msg_len
- len(msg
)))
244 self
.last_msg_len
= len(msg
)
249 def clear_display(self
):
250 if self
.last_msg_len
!= None:
251 sys
.stdout
.write(chr(13) + " " * self
.last_msg_len
+ chr(13))
253 self
.last_msg_len
= None
255 def report_error(self
, exception
, tb
= None):
257 Handler
.report_error(self
, exception
, tb
)
259 def confirm_import_feed(self
, pending
, valid_sigs
):
261 self
.disable_progress
+= 1
262 blocker
= Handler
.confirm_import_feed(self
, pending
, valid_sigs
)
266 self
.disable_progress
-= 1