Package zeroinstall :: Package injector :: Module handler
[frames] | no frames]

Source Code for Module zeroinstall.injector.handler

  1  """ 
  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. 
  6   
  7  To do this, you supply a L{Handler} to the L{policy}. 
  8  """ 
  9   
 10  # Copyright (C) 2009, Thomas Leonard 
 11  # See the README file for details, or visit http://0install.net. 
 12   
 13  from __future__ import print_function 
 14   
 15  from zeroinstall import _, logger 
 16  import sys 
 17   
 18  if sys.version_info[0] < 3: 
 19          import __builtin__ as builtins 
 20  else: 
 21          import builtins 
 22   
 23  from zeroinstall import SafeException 
 24  from zeroinstall import support 
 25  from zeroinstall.support import tasks 
 26  from zeroinstall.injector import download 
27 28 -class NoTrustedKeys(SafeException):
29 """Thrown by L{Handler.confirm_import_feed} on failure.""" 30 pass
31
32 -class Handler(object):
33 """ 34 A Handler is used to interact with the user (e.g. to confirm keys, display download progress, etc). 35 36 @ivar monitored_downloads: set of downloads in progress 37 @type monitored_downloads: {L{download.Download}} 38 @ivar n_completed_downloads: number of downloads which have finished for GUIs, etc (can be reset as desired). 39 @type n_completed_downloads: int 40 @ivar total_bytes_downloaded: informational counter for GUIs, etc (can be reset as desired). Updated when download finishes. 41 @type total_bytes_downloaded: int 42 @ivar dry_run: don't write or execute any files, just print notes about what we would have done to stdout 43 @type dry_run: bool 44 """ 45 46 __slots__ = ['monitored_downloads', 'dry_run', 'total_bytes_downloaded', 'n_completed_downloads'] 47
48 - def __init__(self, mainloop = None, dry_run = False):
49 """@type dry_run: bool""" 50 self.monitored_downloads = set() 51 self.dry_run = dry_run 52 self.n_completed_downloads = 0 53 self.total_bytes_downloaded = 0
54
55 - def monitor_download(self, dl):
56 """Called when a new L{download} is started. 57 This is mainly used by the GUI to display the progress bar. 58 @type dl: L{zeroinstall.injector.download.Download}""" 59 self.monitored_downloads.add(dl) 60 self.downloads_changed() 61 62 @tasks.async 63 def download_done_stats(): 64 yield dl.downloaded 65 # NB: we don't check for exceptions here; someone else should be doing that 66 try: 67 self.n_completed_downloads += 1 68 self.total_bytes_downloaded += dl.get_bytes_downloaded_so_far() 69 self.monitored_downloads.remove(dl) 70 self.downloads_changed() 71 except Exception as ex: 72 self.report_error(ex)
73 download_done_stats()
74
75 - def impl_added_to_store(self, impl):
76 """Called by the L{fetch.Fetcher} when adding an implementation. 77 The GUI uses this to update its display. 78 @param impl: the implementation which has been added 79 @type impl: L{model.Implementation}""" 80 pass
81
82 - def downloads_changed(self):
83 """This is just for the GUI to override to update its display.""" 84 pass
85
86 - def wait_for_blocker(self, blocker):
87 """@type blocker: L{zeroinstall.support.tasks.Blocker} 88 @deprecated: use tasks.wait_for_blocker instead""" 89 tasks.wait_for_blocker(blocker)
90 91 @tasks.async
92 - def confirm_import_feed(self, pending, valid_sigs):
93 """Sub-classes should override this method to interact with the user about new feeds. 94 If multiple feeds need confirmation, L{trust.TrustMgr.confirm_keys} will only invoke one instance of this 95 method at a time. 96 @param pending: the new feed to be imported 97 @type pending: L{PendingFeed} 98 @param valid_sigs: maps signatures to a list of fetchers collecting information about the key 99 @type valid_sigs: {L{gpg.ValidSig} : L{fetch.KeyInfoFetcher}} 100 @since: 0.42""" 101 from zeroinstall.injector import trust 102 103 assert valid_sigs 104 105 domain = trust.domain_from_url(pending.url) 106 107 # Ask on stderr, because we may be writing XML to stdout 108 print(_("Feed: %s") % pending.url, file=sys.stderr) 109 print(_("The feed is correctly signed with the following keys:"), file=sys.stderr) 110 for x in valid_sigs: 111 print("-", x, file=sys.stderr) 112 113 def text(parent): 114 text = "" 115 for node in parent.childNodes: 116 if node.nodeType == node.TEXT_NODE: 117 text = text + node.data 118 return text
119 120 shown = set() 121 key_info_fetchers = valid_sigs.values() 122 while key_info_fetchers: 123 old_kfs = key_info_fetchers 124 key_info_fetchers = [] 125 for kf in old_kfs: 126 infos = set(kf.info) - shown 127 if infos: 128 if len(valid_sigs) > 1: 129 print("%s: " % kf.fingerprint) 130 for key_info in infos: 131 print("-", text(key_info), file=sys.stderr) 132 shown.add(key_info) 133 if kf.blocker: 134 key_info_fetchers.append(kf) 135 if key_info_fetchers: 136 for kf in key_info_fetchers: print(kf.status, file=sys.stderr) 137 stdin = tasks.InputBlocker(0, 'console') 138 blockers = [kf.blocker for kf in key_info_fetchers] + [stdin] 139 yield blockers 140 for b in blockers: 141 try: 142 tasks.check(b) 143 except Exception as ex: 144 logger.warning(_("Failed to get key info: %s"), ex) 145 if stdin.happened: 146 print(_("Skipping remaining key lookups due to input from user"), file=sys.stderr) 147 break 148 if not shown: 149 print(_("Warning: Nothing known about this key!"), file=sys.stderr) 150 151 if len(valid_sigs) == 1: 152 print(_("Do you want to trust this key to sign feeds from '%s'?") % domain, file=sys.stderr) 153 else: 154 print(_("Do you want to trust all of these keys to sign feeds from '%s'?") % domain, file=sys.stderr) 155 while True: 156 print(_("Trust [Y/N] "), end=' ', file=sys.stderr) 157 i = support.raw_input() 158 if not i: continue 159 if i in 'Nn': 160 raise NoTrustedKeys(_('Not signed with a trusted key')) 161 if i in 'Yy': 162 break 163 trust.trust_db._dry_run = self.dry_run 164 for key in valid_sigs: 165 print(_("Trusting %(key_fingerprint)s for %(domain)s") % {'key_fingerprint': key.fingerprint, 'domain': domain}, file=sys.stderr) 166 trust.trust_db.trust_key(key.fingerprint, domain) 167 168 @tasks.async
169 - def confirm_install(self, msg):
170 """We need to check something with the user before continuing with the install. 171 @raise download.DownloadAborted: if the user cancels""" 172 yield 173 print(msg, file=sys.stderr) 174 while True: 175 sys.stderr.write(_("Install [Y/N] ")) 176 i = support.raw_input() 177 if not i: continue 178 if i in 'Nn': 179 raise download.DownloadAborted() 180 if i in 'Yy': 181 break
182
183 - def report_error(self, exception, tb = None):
184 """Report an exception to the user. 185 @param exception: the exception to report 186 @type exception: L{SafeException} 187 @param tb: optional traceback 188 @since: 0.25""" 189 import logging 190 logger.warning("%s", str(exception) or type(exception), 191 exc_info = (exception, exception, tb) if logger.isEnabledFor(logging.INFO) else None)
192
193 -class ConsoleHandler(Handler):
194 """A Handler that displays progress on stdout (a tty). 195 @since: 0.44""" 196 last_msg_len = None 197 update = None 198 disable_progress = 0 199 screen_width = None 200 201 # While we are displaying progress, we override builtins.print to clear the display first. 202 original_print = None 203
204 - def downloads_changed(self):
205 if self.monitored_downloads and self.update is None: 206 if self.screen_width is None: 207 try: 208 import curses 209 curses.setupterm() 210 self.screen_width = curses.tigetnum('cols') or 80 211 except Exception as ex: 212 logger.info("Failed to initialise curses library: %s", ex) 213 self.screen_width = 80 214 self.show_progress() 215 self.original_print = print 216 builtins.print = self.print 217 self.update = tasks.loop.call_repeatedly(0.2, self.show_progress) 218 elif len(self.monitored_downloads) == 0: 219 if self.update: 220 self.update.cancel() 221 self.update = None 222 builtins.print = self.original_print 223 self.original_print = None 224 self.clear_display()
225
226 - def show_progress(self):
227 if not self.monitored_downloads: return 228 urls = [(dl.url, dl) for dl in self.monitored_downloads] 229 230 if self.disable_progress: return 231 232 screen_width = self.screen_width - 2 233 item_width = max(16, screen_width // len(self.monitored_downloads)) 234 url_width = item_width - 7 235 236 msg = "" 237 for url, dl in sorted(urls): 238 so_far = dl.get_bytes_downloaded_so_far() 239 leaf = url.rsplit('/', 1)[-1] 240 if len(leaf) >= url_width: 241 display = leaf[:url_width] 242 else: 243 display = url[-url_width:] 244 if dl.expected_size: 245 msg += "[%s %d%%] " % (display, int(so_far * 100 / dl.expected_size)) 246 else: 247 msg += "[%s] " % (display) 248 msg = msg[:screen_width] 249 250 if self.last_msg_len is None: 251 sys.stdout.write(msg) 252 else: 253 sys.stdout.write(chr(13) + msg) 254 if len(msg) < self.last_msg_len: 255 sys.stdout.write(" " * (self.last_msg_len - len(msg))) 256 257 self.last_msg_len = len(msg) 258 sys.stdout.flush() 259 260 return
261
262 - def clear_display(self):
263 if self.last_msg_len != None: 264 sys.stdout.write(chr(13) + " " * self.last_msg_len + chr(13)) 265 sys.stdout.flush() 266 self.last_msg_len = None
267
268 - def report_error(self, exception, tb = None):
269 self.clear_display() 270 Handler.report_error(self, exception, tb)
271
272 - def confirm_import_feed(self, pending, valid_sigs):
273 self.clear_display() 274 self.disable_progress += 1 275 blocker = Handler.confirm_import_feed(self, pending, valid_sigs) 276 @tasks.async 277 def enable(): 278 yield blocker 279 self.disable_progress -= 1 280 self.show_progress()
281 enable() 282 return blocker
283
284 - def print(self, *args, **kwargs):
285 self.clear_display() 286 self.original_print(*args, **kwargs)
287