Fixed bug where PackageKit downloaded the wrong architecture
[zeroinstall/solver.git] / zeroinstall / injector / packagekit.py
blob2d177107c2ead2c125334c2d391f30238ca30e61
1 """
2 PackageKit integration.
3 """
5 # Copyright (C) 2010, Aleksey Lim
6 # See the README file for details, or visit http://0install.net.
8 import os, sys
9 import locale
10 import logging
11 from zeroinstall import _, SafeException
13 from zeroinstall.support import tasks, unicode
14 from zeroinstall.injector import download, model
16 _logger_pk = logging.getLogger('0install.packagekit')
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
26 MAX_PACKAGE_KIT_TRANSACTION_SIZE = 100
28 class PackageKit(object):
29 def __init__(self):
30 self._pk = False
32 self._candidates = {} # { package_name : [ (version, arch, size) ] | Blocker }
34 # PackageKit is really slow at handling separate queries, so we use this to
35 # batch them up.
36 self._next_batch = set()
38 @property
39 def available(self):
40 return self.pk is not None
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
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
70 if isinstance(candidates, tasks.Blocker):
71 return # Fetch still in progress
73 for candidate in candidates:
74 impl_name = '%s:%s:%s:%s' % (prefix, package_name, candidate['version'], candidate['arch'])
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
81 impl.version = model.parse_version(candidate['version'])
82 if candidate['arch'] != '*':
83 impl.machine = candidate['arch']
85 def install(handler, candidate = candidate, impl = impl):
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))
92 @tasks.async
93 def fetch_candidates(self, package_names):
94 assert self.pk
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
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)
110 # Filter out the ones we've already fetched
111 package_names = [p for p in package_names if p not in self._candidates]
113 def do_batch(package_names):
114 #_logger_pk.info("sending %d packages in batch", len(package_names))
115 versions = {}
117 blocker = None
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()
124 def details_cb(sender):
125 # The key can be a dbus.String sometimes, so convert to a Python
126 # string to be sure we get a match.
127 details = {}
128 for packagekit_id, d in sender.details.items():
129 details[unicode(packagekit_id)] = d
131 _logger_pk.debug("Got: %r", details)
132 _logger_pk.debug("Expecting: %r", versions)
134 for packagekit_id, info in versions.items():
135 if packagekit_id in details:
136 info.update(details[packagekit_id])
137 info['packagekit_id'] = packagekit_id
138 if (info['name'] not in self._candidates or
139 isinstance(self._candidates[info['name']], tasks.Blocker)):
140 self._candidates[info['name']] = [info]
141 else:
142 self._candidates[info['name']].append(info)
143 else:
144 _logger_pk.info(_('Empty details for %s'), packagekit_id)
145 blocker.trigger()
147 def resolve_cb(sender):
148 if sender.package:
149 _logger_pk.debug(_('Resolved %r'), sender.package)
150 for packagekit_id, info in sender.package.items():
151 packagekit_id = unicode(packagekit_id) # Can be a dbus.String sometimes
152 parts = packagekit_id.split(';', 3)
153 if ':' in parts[3]:
154 parts[3] = parts[3].split(':', 1)[0]
155 packagekit_id = ';'.join(parts)
156 versions[packagekit_id] = info
157 tran = _PackageKitTransaction(self.pk, details_cb, error_cb)
158 tran.proxy.GetDetails(list(versions.keys()))
159 else:
160 _logger_pk.info(_('Empty resolve for %s'), package_names)
161 blocker.trigger()
163 # Send queries
164 blocker = tasks.Blocker('PackageKit %s' % package_names)
165 for package in package_names:
166 self._candidates[package] = blocker
168 try:
169 _logger_pk.debug(_('Ask for %s'), package_names)
170 tran = _PackageKitTransaction(self.pk, resolve_cb, error_cb)
171 tran.proxy.Resolve('none', package_names)
173 in_progress.append(blocker)
174 except:
175 __, ex, tb = sys.exc_info()
176 blocker.trigger((ex, tb))
177 raise
179 # Now we've collected all the requests together, split them up into chunks
180 # that PackageKit can handle ( < 100 per batch )
181 #_logger_pk.info("sending %d packages", len(package_names))
182 while package_names:
183 next_batch = package_names[:MAX_PACKAGE_KIT_TRANSACTION_SIZE]
184 package_names = package_names[MAX_PACKAGE_KIT_TRANSACTION_SIZE:]
185 do_batch(next_batch)
187 while in_progress:
188 yield in_progress
189 in_progress = [b for b in in_progress if not b.happened]
191 class PackageKitDownload:
192 def __init__(self, url, hint, pk, packagekit_id, expected_size):
193 self.url = url
194 self.status = download.download_fetching
195 self.hint = hint
196 self.aborted_by_user = False
198 self.downloaded = None
200 self.expected_size = expected_size
202 self.packagekit_id = packagekit_id
203 self._impl = hint
204 self._transaction = None
205 self.pk = pk
207 def error_cb(sender):
208 self.status = download.download_failed
209 ex = SafeException('PackageKit install failed: %s' % (sender.error_details or sender.error_code))
210 self.downloaded.trigger(exception = (ex, None))
212 def installed_cb(sender):
213 assert not self._impl.installed, impl
214 self._impl.installed = True
215 self._impl.distro.installed_fixup(self._impl)
217 self.status = download.download_complete
218 self.downloaded.trigger()
220 def install_packages():
221 package_name = self.packagekit_id
222 self._transaction = _PackageKitTransaction(self.pk, installed_cb, error_cb)
223 self._transaction.compat_call([
224 ('InstallPackages', False, [package_name]),
225 ('InstallPackages', [package_name]),
228 _auth_wrapper(install_packages)
230 self.downloaded = tasks.Blocker('PackageKit install %s' % self.packagekit_id)
232 def abort(self):
233 _logger_pk.debug(_('Cancel transaction'))
234 self.aborted_by_user = True
235 self._transaction.proxy.Cancel()
236 self.status = download.download_failed
237 self.downloaded.trigger()
239 def get_current_fraction(self):
240 if self._transaction is None:
241 return None
242 percentage = self._transaction.getPercentage()
243 if percentage > 100:
244 return None
245 else:
246 return float(percentage) / 100.
248 def get_bytes_downloaded_so_far(self):
249 fraction = self.get_current_fraction()
250 if fraction is None:
251 return 0
252 else:
253 if self.expected_size is None:
254 return 0
255 return int(self.expected_size * fraction)
257 def _auth_wrapper(method, *args):
258 try:
259 return method(*args)
260 except dbus.exceptions.DBusException as e:
261 if e.get_dbus_name() != \
262 'org.freedesktop.PackageKit.Transaction.RefusedByPolicy':
263 raise
265 iface, auth = e.get_dbus_message().split()
266 if not auth.startswith('auth_'):
267 raise
269 _logger_pk.debug(_('Authentication required for %s'), auth)
271 pk_auth = dbus.SessionBus().get_object(
272 'org.freedesktop.PolicyKit.AuthenticationAgent', '/',
273 'org.gnome.PolicyKit.AuthorizationManager.SingleInstance')
275 if not pk_auth.ObtainAuthorization(iface, dbus.UInt32(0),
276 dbus.UInt32(os.getpid()), timeout=300):
277 raise
279 return method(*args)
281 class _PackageKitTransaction(object):
282 def __init__(self, pk, finished_cb=None, error_cb=None):
283 self._finished_cb = finished_cb
284 self._error_cb = error_cb
285 self.error_code = None
286 self.error_details = None
287 self.package = {}
288 self.details = {}
289 self.files = {}
291 self.object = dbus.SystemBus().get_object(
292 'org.freedesktop.PackageKit', pk.GetTid(), False)
293 self.proxy = dbus.Interface(self.object,
294 'org.freedesktop.PackageKit.Transaction')
295 self._props = dbus.Interface(self.object, dbus.PROPERTIES_IFACE)
297 self._signals = []
298 for signal, cb in [('Finished', self.__finished_cb),
299 ('ErrorCode', self.__error_code_cb),
300 ('StatusChanged', self.__status_changed_cb),
301 ('Package', self.__package_cb),
302 ('Details', self.__details_cb),
303 ('Files', self.__files_cb)]:
304 self._signals.append(self.proxy.connect_to_signal(signal, cb))
306 defaultlocale = locale.getdefaultlocale()[0]
307 if defaultlocale is not None:
308 self.compat_call([
309 ('SetHints', ['locale=%s' % defaultlocale]),
310 ('SetLocale', defaultlocale),
313 def getPercentage(self):
314 result = self.get_prop('Percentage')
315 if result is None:
316 result, __, __, __ = self.proxy.GetProgress()
317 return result
319 def get_prop(self, prop, default = None):
320 try:
321 return self._props.Get('org.freedesktop.PackageKit.Transaction', prop)
322 except:
323 return default
325 # note: Ubuntu's aptdaemon implementation of PackageKit crashes if passed the wrong
326 # arguments (rather than returning InvalidArgs), so always try its API first.
327 def compat_call(self, calls):
328 for call in calls:
329 method = call[0]
330 args = call[1:]
331 try:
332 dbus_method = self.proxy.get_dbus_method(method)
333 return dbus_method(*args)
334 except dbus.exceptions.DBusException as e:
335 if e.get_dbus_name() not in (
336 'org.freedesktop.DBus.Error.UnknownMethod',
337 'org.freedesktop.DBus.Error.InvalidArgs'):
338 raise
339 raise Exception('Cannot call %r DBus method' % calls)
341 def __finished_cb(self, exit, runtime):
342 _logger_pk.debug(_('Transaction finished: %s'), exit)
344 for i in self._signals:
345 i.remove()
347 if self.error_code is not None:
348 self._error_cb(self)
349 else:
350 self._finished_cb(self)
352 def __error_code_cb(self, code, details):
353 _logger_pk.info(_('Transaction failed: %s(%s)'), details, code)
354 self.error_code = code
355 self.error_details = details
357 def __package_cb(self, status, id, summary):
358 try:
359 from zeroinstall.injector import distro
361 package_name, version, arch, repo_ = id.split(';')
362 clean_version = distro.try_cleanup_distro_version(version)
363 if not clean_version:
364 _logger_pk.info(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package_name})
365 return
366 clean_arch = distro.canonical_machine(arch)
367 package = {'version': clean_version,
368 'name': package_name,
369 'arch': clean_arch,
370 'installed': (status == 'installed')}
371 _logger_pk.debug(_('Package: %s %r'), id, package)
372 self.package[str(id)] = package
373 except Exception as ex:
374 _logger_pk.warn("__package_cb(%s, %s, %s): %s", status, id, summary, ex)
376 def __details_cb(self, id, licence, group, detail, url, size):
377 details = {'licence': str(licence),
378 'group': str(group),
379 'detail': str(detail),
380 'url': str(url),
381 'size': int(size)}
382 _logger_pk.debug(_('Details: %s %r'), id, details)
383 self.details[id] = details
385 def __files_cb(self, id, files):
386 self.files[id] = files.split(';')
388 def __status_changed_cb(self, status):
389 pass