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