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

Source Code for Module zeroinstall.injector.packagekit

  1  """ 
  2  PackageKit integration. 
  3  """ 
  4   
  5  # Copyright (C) 2010, Aleksey Lim 
  6  # See the README file for details, or visit http://0install.net. 
  7   
  8  import os, sys 
  9  import locale 
 10  import logging 
 11  from zeroinstall import _, SafeException 
 12   
 13  from zeroinstall.support import tasks 
 14  from zeroinstall.injector import download, model 
 15   
 16  _logger_pk = logging.getLogger('packagekit') 
 17   
 18  try: 
 19          import dbus 
 20          import dbus.mainloop.glib 
 21          dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) 
 22  except Exception as ex: 
 23          _logger_pk.info("D-BUS not available: %s", ex) 
 24          dbus = None 
 25   
 26  MAX_PACKAGE_KIT_TRANSACTION_SIZE = 100 
27 28 -class PackageKit(object):
29 - def __init__(self):
30 self._pk = False 31 32 self._candidates = {} # { package_name : [ (version, arch, size) ] | Blocker } 33 34 # PackageKit is really slow at handling separate queries, so we use this to 35 # batch them up. 36 self._next_batch = set()
37 38 @property
39 - def available(self):
40 return self.pk is not None
41 42 @property
43 - def pk(self):
44 if self._pk is False: 45 if dbus is None: 46 self._pk = None 47 else: 48 try: 49 self._pk = dbus.Interface(dbus.SystemBus().get_object( 50 'org.freedesktop.PackageKit', 51 '/org/freedesktop/PackageKit', False), 52 'org.freedesktop.PackageKit') 53 _logger_pk.info(_('PackageKit dbus service found')) 54 except Exception as ex: 55 _logger_pk.info(_('PackageKit dbus service not found: %s'), ex) 56 self._pk = None 57 return self._pk
58
59 - def get_candidates(self, package_name, factory, prefix):
60 """Add any cached candidates. 61 The candidates are those discovered by a previous call to L{fetch_candidates}. 62 @param package_name: the distribution's name for the package 63 @param factory: a function to add a new implementation to the feed 64 @param prefix: the prefix for the implementation's ID 65 """ 66 candidates = self._candidates.get(package_name, None) 67 if candidates is None: 68 return 69 70 if isinstance(candidates, tasks.Blocker): 71 return # Fetch still in progress 72 73 for candidate in candidates: 74 impl_name = '%s:%s:%s:%s' % (prefix, package_name, candidate['version'], candidate['arch']) 75 76 impl = factory(impl_name, only_if_missing = True, installed = candidate['installed']) 77 if impl is None: 78 # (checking this way because the cached candidate['installed'] may be stale) 79 return # Already installed 80 81 impl.version = model.parse_version(candidate['version']) 82 if candidate['arch'] != '*': 83 impl.machine = candidate['arch'] 84 85 def install(handler): 86 packagekit_id = candidate['packagekit_id'] 87 dl = PackageKitDownload('packagekit:' + packagekit_id, hint = impl, pk = self.pk, packagekit_id = packagekit_id, expected_size = candidate['size']) 88 handler.monitor_download(dl) 89 return dl.downloaded
90 impl.download_sources.append(model.DistributionSource(package_name, candidate['size'], install))
91 92 @tasks.async
93 - def fetch_candidates(self, package_names):
94 assert self.pk 95 96 # Batch requests up 97 self._next_batch |= set(package_names) 98 yield 99 package_names = self._next_batch 100 self._next_batch = set() 101 # The first fetch_candidates instance will now have all the packages 102 # For the others, package_names will now be empty 103 104 known = [self._candidates[p] for p in package_names if p in self._candidates] 105 # (use set because a single task may be checking multiple packages and we need 106 # to avoid duplicates). 107 in_progress = list(set([b for b in known if isinstance(b, tasks.Blocker)])) 108 _logger_pk.debug('Already downloading: %s', in_progress) 109 110 # Filter out the ones we've already fetched 111 package_names = [p for p in package_names if p not in self._candidates] 112 113 def do_batch(package_names): 114 #_logger_pk.info("sending %d packages in batch", len(package_names)) 115 versions = {} 116 117 blocker = None 118 119 def error_cb(sender): 120 # Note: probably just means the package wasn't found 121 _logger_pk.info(_('Transaction failed: %s(%s)'), sender.error_code, sender.error_details) 122 blocker.trigger()
123 124 def details_cb(sender): 125 for packagekit_id, info in versions.items(): 126 if packagekit_id in sender.details: 127 info.update(sender.details[packagekit_id]) 128 info['packagekit_id'] = packagekit_id 129 if (info['name'] not in self._candidates or 130 isinstance(self._candidates[info['name']], tasks.Blocker)): 131 self._candidates[info['name']] = [info] 132 else: 133 self._candidates[info['name']].append(info) 134 else: 135 _logger_pk.info(_('Empty details for %s'), packagekit_id) 136 blocker.trigger() 137 138 def resolve_cb(sender): 139 if sender.package: 140 for packagekit_id, info in sender.package.iteritems(): 141 parts = packagekit_id.split(';', 3) 142 if ':' in parts[3]: 143 parts[3] = parts[3].split(':', 1)[0] 144 packagekit_id = ';'.join(parts) 145 versions[packagekit_id] = info 146 tran = _PackageKitTransaction(self.pk, details_cb, error_cb) 147 tran.proxy.GetDetails(versions.keys()) 148 else: 149 _logger_pk.info(_('Empty resolve for %s'), package_names) 150 blocker.trigger() 151 152 # Send queries 153 blocker = tasks.Blocker('PackageKit %s' % package_names) 154 for package in package_names: 155 self._candidates[package] = blocker 156 157 try: 158 _logger_pk.debug(_('Ask for %s'), package_names) 159 tran = _PackageKitTransaction(self.pk, resolve_cb, error_cb) 160 tran.proxy.Resolve('none', package_names) 161 162 in_progress.append(blocker) 163 except: 164 __, ex, tb = sys.exc_info() 165 blocker.trigger((ex, tb)) 166 raise 167 168 # Now we've collected all the requests together, split them up into chunks 169 # that PackageKit can handle ( < 100 per batch ) 170 #_logger_pk.info("sending %d packages", len(package_names)) 171 while package_names: 172 next_batch = package_names[:MAX_PACKAGE_KIT_TRANSACTION_SIZE] 173 package_names = package_names[MAX_PACKAGE_KIT_TRANSACTION_SIZE:] 174 do_batch(next_batch) 175 176 while in_progress: 177 yield in_progress 178 in_progress = [b for b in in_progress if not b.happened] 179
180 -class PackageKitDownload:
181 - def __init__(self, url, hint, pk, packagekit_id, expected_size):
182 self.url = url 183 self.status = download.download_fetching 184 self.hint = hint 185 self.aborted_by_user = False 186 187 self.downloaded = None 188 189 self.expected_size = expected_size 190 191 self.packagekit_id = packagekit_id 192 self._impl = hint 193 self._transaction = None 194 self.pk = pk 195 196 def error_cb(sender): 197 self.status = download.download_failed 198 ex = SafeException('PackageKit install failed: %s' % (sender.error_details or sender.error_code)) 199 self.downloaded.trigger(exception = (ex, None))
200 201 def installed_cb(sender): 202 self._impl.installed = True 203 self.status = download.download_complete 204 self.downloaded.trigger()
205 206 def install_packages(): 207 package_name = self.packagekit_id 208 self._transaction = _PackageKitTransaction(self.pk, installed_cb, error_cb) 209 self._transaction.compat_call([ 210 ('InstallPackages', False, [package_name]), 211 ('InstallPackages', [package_name]), 212 ]) 213 214 _auth_wrapper(install_packages) 215 216 self.downloaded = tasks.Blocker('PackageKit install %s' % self.packagekit_id) 217
218 - def abort(self):
219 _logger_pk.debug(_('Cancel transaction')) 220 self.aborted_by_user = True 221 self._transaction.proxy.Cancel() 222 self.status = download.download_failed 223 self.downloaded.trigger()
224
225 - def get_current_fraction(self):
226 if self._transaction is None: 227 return None 228 percentage = self._transaction.getPercentage() 229 if percentage > 100: 230 return None 231 else: 232 return float(percentage) / 100.
233
234 - def get_bytes_downloaded_so_far(self):
235 fraction = self.get_current_fraction() 236 if fraction is None: 237 return 0 238 else: 239 if self.expected_size is None: 240 return 0 241 return int(self.expected_size * fraction)
242
243 -def _auth_wrapper(method, *args):
244 try: 245 return method(*args) 246 except dbus.exceptions.DBusException as e: 247 if e.get_dbus_name() != \ 248 'org.freedesktop.PackageKit.Transaction.RefusedByPolicy': 249 raise 250 251 iface, auth = e.get_dbus_message().split() 252 if not auth.startswith('auth_'): 253 raise 254 255 _logger_pk.debug(_('Authentication required for %s'), auth) 256 257 pk_auth = dbus.SessionBus().get_object( 258 'org.freedesktop.PolicyKit.AuthenticationAgent', '/', 259 'org.gnome.PolicyKit.AuthorizationManager.SingleInstance') 260 261 if not pk_auth.ObtainAuthorization(iface, dbus.UInt32(0), 262 dbus.UInt32(os.getpid()), timeout=300): 263 raise 264 265 return method(*args)
266
267 -class _PackageKitTransaction(object):
268 - def __init__(self, pk, finished_cb=None, error_cb=None):
269 self._finished_cb = finished_cb 270 self._error_cb = error_cb 271 self.error_code = None 272 self.error_details = None 273 self.package = {} 274 self.details = {} 275 self.files = {} 276 277 self.object = dbus.SystemBus().get_object( 278 'org.freedesktop.PackageKit', pk.GetTid(), False) 279 self.proxy = dbus.Interface(self.object, 280 'org.freedesktop.PackageKit.Transaction') 281 self._props = dbus.Interface(self.object, dbus.PROPERTIES_IFACE) 282 283 self._signals = [] 284 for signal, cb in [('Finished', self.__finished_cb), 285 ('ErrorCode', self.__error_code_cb), 286 ('StatusChanged', self.__status_changed_cb), 287 ('Package', self.__package_cb), 288 ('Details', self.__details_cb), 289 ('Files', self.__files_cb)]: 290 self._signals.append(self.proxy.connect_to_signal(signal, cb)) 291 292 defaultlocale = locale.getdefaultlocale()[0] 293 if defaultlocale is not None: 294 self.compat_call([ 295 ('SetHints', ['locale=%s' % defaultlocale]), 296 ('SetLocale', defaultlocale), 297 ])
298
299 - def getPercentage(self):
300 result = self.get_prop('Percentage') 301 if result is None: 302 result, __, __, __ = self.proxy.GetProgress() 303 return result
304
305 - def get_prop(self, prop, default = None):
306 try: 307 return self._props.Get('org.freedesktop.PackageKit.Transaction', prop) 308 except: 309 return default
310 311 # note: Ubuntu's aptdaemon implementation of PackageKit crashes if passed the wrong 312 # arguments (rather than returning InvalidArgs), so always try its API first.
313 - def compat_call(self, calls):
314 for call in calls: 315 method = call[0] 316 args = call[1:] 317 try: 318 dbus_method = self.proxy.get_dbus_method(method) 319 return dbus_method(*args) 320 except dbus.exceptions.DBusException as e: 321 if e.get_dbus_name() not in ( 322 'org.freedesktop.DBus.Error.UnknownMethod', 323 'org.freedesktop.DBus.Error.InvalidArgs'): 324 raise 325 raise Exception('Cannot call %r DBus method' % calls)
326
327 - def __finished_cb(self, exit, runtime):
328 _logger_pk.debug(_('Transaction finished: %s'), exit) 329 330 for i in self._signals: 331 i.remove() 332 333 if self.error_code is not None: 334 self._error_cb(self) 335 else: 336 self._finished_cb(self)
337
338 - def __error_code_cb(self, code, details):
339 _logger_pk.info(_('Transaction failed: %s(%s)'), details, code) 340 self.error_code = code 341 self.error_details = details
342
343 - def __package_cb(self, status, id, summary):
344 try: 345 from zeroinstall.injector import distro 346 347 package_name, version, arch, repo_ = id.split(';') 348 clean_version = distro.try_cleanup_distro_version(version) 349 if not clean_version: 350 _logger_pk.info(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package_name}) 351 return 352 clean_arch = distro.canonical_machine(arch) 353 package = {'version': clean_version, 354 'name': package_name, 355 'arch': clean_arch, 356 'installed': (status == 'installed')} 357 _logger_pk.debug(_('Package: %s %r'), id, package) 358 self.package[str(id)] = package 359 except Exception as ex: 360 _logger_pk.warn("__package_cb(%s, %s, %s): %s", status, id, summary, ex)
361
362 - def __details_cb(self, id, licence, group, detail, url, size):
363 details = {'licence': str(licence), 364 'group': str(group), 365 'detail': str(detail), 366 'url': str(url), 367 'size': int(size)} 368 _logger_pk.debug(_('Details: %s %r'), id, details) 369 self.details[id] = details
370
371 - def __files_cb(self, id, files):
372 self.files[id] = files.split(';')
373
374 - def __status_changed_cb(self, status):
375 pass
376