Cope better with missing details from PackageKit
[zeroinstall.git] / zeroinstall / injector / packagekit.py
blob82c86d0b9119353c497274c3d03ca148893574fb
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
9 import locale
10 import logging
11 from zeroinstall import _, SafeException
13 from zeroinstall.support import tasks
14 from zeroinstall.injector import download, model
16 _logger_pk = logging.getLogger('packagekit')
18 try:
19 import dbus
20 import dbus.mainloop.glib
21 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
22 except Exception, ex:
23 _logger_pk.info("D-BUS not available: %s", ex)
24 dbus = None
26 class PackageKit(object):
27 def __init__(self):
28 self._pk = False
30 self._candidates = {} # { package_name : (version, arch, size) | Blocker }
32 @property
33 def available(self):
34 return self.pk is not None
36 @property
37 def pk(self):
38 if self._pk is False:
39 try:
40 self._pk = dbus.Interface(dbus.SystemBus().get_object(
41 'org.freedesktop.PackageKit',
42 '/org/freedesktop/PackageKit', False),
43 'org.freedesktop.PackageKit')
44 _logger_pk.info(_('PackageKit dbus service found'))
45 except Exception, ex:
46 _logger_pk.info(_('PackageKit dbus service not found: %s'), ex)
47 self._pk = None
48 return self._pk
50 def get_candidates(self, package_name, factory, prefix):
51 """Add any cached candidates.
52 The candidates are those discovered by a previous call to L{fetch_candidates}.
53 @param package_name: the distribution's name for the package
54 @param factory: a function to add a new implementation to the feed
55 @param prefix: the prefix for the implementation's ID
56 """
57 candidate = self._candidates.get(package_name, None)
58 if candidate is None:
59 return
61 if isinstance(candidate, tasks.Blocker):
62 return # Fetch still in progress
64 impl_name = '%s:%s:%s:%s' % (prefix, package_name, candidate['version'], candidate['arch'])
66 impl = factory(impl_name, only_if_missing = True, installed = candidate['installed'])
67 if impl is None:
68 # (checking this way because the cached candidate['installed'] may be stale)
69 return # Already installed
71 impl.version = model.parse_version(candidate['version'])
72 if candidate['arch'] != '*':
73 impl.machine = candidate['arch']
75 def install(handler):
76 packagekit_id = candidate['packagekit_id']
77 def download_factory(url, hint):
78 return PackageKitDownload(url, hint, pk = self.pk, packagekit_id = packagekit_id)
79 dl = handler.get_download('packagekit:' + packagekit_id, factory = download_factory, hint = impl)
80 dl.expected_size = candidate['size']
81 return dl.downloaded
82 impl.download_sources.append(model.DistributionSource(package_name, candidate['size'], install))
84 @tasks.async
85 def fetch_candidates(self, package_names):
86 assert self.pk
88 known = [self._candidates[p] for p in package_names if p in self._candidates]
89 in_progress = [b for b in known if isinstance(b, tasks.Blocker)]
90 _logger_pk.debug('Already downloading: %s', in_progress)
92 # Filter out the ones we've already fetched
93 package_names = [p for p in package_names if p not in self._candidates]
95 if package_names:
96 versions = {}
98 blocker = None
100 def error_cb(sender):
101 # Note: probably just means the package wasn't found
102 _logger_pk.info(_('Transaction failed: %s(%s)'), sender.error_code, sender.error_details)
103 blocker.trigger()
105 def details_cb(sender):
106 for packagekit_id, info in versions.items():
107 if packagekit_id in sender.details:
108 info.update(sender.details[packagekit_id])
109 info['packagekit_id'] = packagekit_id
110 self._candidates[info['name']] = info
111 else:
112 _logger_pk.info(_('Empty details for %s'), packagekit_id)
113 blocker.trigger()
115 def resolve_cb(sender):
116 if sender.package:
117 versions.update(sender.package)
118 tran = _PackageKitTransaction(self.pk, details_cb, error_cb)
119 tran.proxy.GetDetails(versions.keys())
120 else:
121 _logger_pk.info(_('Empty resolve for %s'), package_names)
122 blocker.trigger()
124 # Send queries
125 blocker = tasks.Blocker('PackageKit %s' % package_names)
126 for package in package_names:
127 self._candidates[package] = blocker
129 _logger_pk.debug(_('Ask for %s'), package_names)
130 tran = _PackageKitTransaction(self.pk, resolve_cb, error_cb)
131 tran.proxy.Resolve('none', package_names)
133 in_progress.append(blocker)
135 while in_progress:
136 yield in_progress
137 in_progress = [b for b in in_progress if not b.happened]
139 class PackageKitDownload(download.Download):
140 def __init__(self, url, hint, pk, packagekit_id):
141 download.Download.__init__(self, url, hint)
143 self.packagekit_id = packagekit_id
144 self._impl = hint
145 self._transaction = None
146 self.pk = pk
148 def start(self):
149 assert self.status == download.download_starting
150 assert self.downloaded is None
152 def error_cb(sender):
153 self.status = download.download_failed
154 ex = SafeException('PackageKit install failed: %s' % (sender.error_details or sender.error_code))
155 self.downloaded.trigger(exception = (ex, None))
157 def installed_cb(sender):
158 self._impl.installed = True;
159 self.status = download.download_complete
160 self.downloaded.trigger()
162 def install_packages():
163 package_name = self.packagekit_id
164 self._transaction = _PackageKitTransaction(self.pk, installed_cb, error_cb)
165 self._transaction.compat_call([
166 ('InstallPackages', [package_name]),
167 ('InstallPackages', False, [package_name]),
170 _auth_wrapper(install_packages)
172 self.status = download.download_fetching
173 self.downloaded = tasks.Blocker('PackageKit install %s' % self.packagekit_id)
175 def abort(self):
176 _logger_pk.debug(_('Cancel transaction'))
177 self.aborted_by_user = True
178 self._transaction.proxy.Cancel()
179 self.status = download.download_failed
180 self.downloaded.trigger()
182 def get_current_fraction(self):
183 if self._transaction is None:
184 return None
185 percentage = self._transaction.getPercentage()
186 if percentage > 100:
187 return None
188 else:
189 return float(percentage) / 100.
191 def get_bytes_downloaded_so_far(self):
192 fraction = self.get_current_fraction()
193 if fraction is None:
194 return 0
195 else:
196 if self.expected_size is None:
197 return 0
198 return int(self.expected_size * fraction)
200 def _auth_wrapper(method, *args):
201 try:
202 return method(*args)
203 except dbus.exceptions.DBusException, e:
204 if e.get_dbus_name() != \
205 'org.freedesktop.PackageKit.Transaction.RefusedByPolicy':
206 raise
208 iface, auth = e.get_dbus_message().split()
209 if not auth.startswith('auth_'):
210 raise
212 _logger_pk.debug(_('Authentication required for %s'), auth)
214 pk_auth = dbus.SessionBus().get_object(
215 'org.freedesktop.PolicyKit.AuthenticationAgent', '/',
216 'org.gnome.PolicyKit.AuthorizationManager.SingleInstance')
218 if not pk_auth.ObtainAuthorization(iface, dbus.UInt32(0),
219 dbus.UInt32(os.getpid()), timeout=300):
220 raise
222 return method(*args)
224 class _PackageKitTransaction(object):
225 def __init__(self, pk, finished_cb=None, error_cb=None):
226 self._finished_cb = finished_cb
227 self._error_cb = error_cb
228 self.error_code = None
229 self.error_details = None
230 self.package = {}
231 self.details = {}
232 self.files = {}
234 self.object = dbus.SystemBus().get_object(
235 'org.freedesktop.PackageKit', pk.GetTid(), False)
236 self.proxy = dbus.Interface(self.object,
237 'org.freedesktop.PackageKit.Transaction')
238 self._props = dbus.Interface(self.object, dbus.PROPERTIES_IFACE)
240 self._signals = []
241 for signal, cb in [('Finished', self.__finished_cb),
242 ('ErrorCode', self.__error_code_cb),
243 ('StatusChanged', self.__status_changed_cb),
244 ('Package', self.__package_cb),
245 ('Details', self.__details_cb),
246 ('Files', self.__files_cb)]:
247 self._signals.append(self.proxy.connect_to_signal(signal, cb))
249 defaultlocale = locale.getdefaultlocale()[0]
250 if defaultlocale is not None:
251 self.compat_call([
252 ('SetLocale', defaultlocale),
253 ('SetHints', ['locale=%s' % defaultlocale]),
256 def getPercentage(self):
257 result = self.get_prop('Percentage')
258 if result is None:
259 result, __, __, __ = self.proxy.GetProgress()
260 return result
262 def get_prop(self, prop, default = None):
263 try:
264 return self._props.Get('org.freedesktop.PackageKit.Transaction', prop)
265 except:
266 return default
268 def compat_call(self, calls):
269 for call in calls:
270 method = call[0]
271 args = call[1:]
272 try:
273 dbus_method = self.proxy.get_dbus_method(method)
274 return dbus_method(*args)
275 except dbus.exceptions.DBusException, e:
276 if e.get_dbus_name() != \
277 'org.freedesktop.DBus.Error.UnknownMethod':
278 raise
279 raise Exception('Cannot call %r DBus method' % calls)
281 def __finished_cb(self, exit, runtime):
282 _logger_pk.debug(_('Transaction finished: %s'), exit)
284 for i in self._signals:
285 i.remove()
287 if self.error_code is not None:
288 self._error_cb(self)
289 else:
290 self._finished_cb(self)
292 def __error_code_cb(self, code, details):
293 _logger_pk.info(_('Transaction failed: %s(%s)'), details, code)
294 self.error_code = code
295 self.error_details = details
297 def __package_cb(self, status, id, summary):
298 from zeroinstall.injector import distro
300 package_name, version, arch, repo_ = id.split(';')
301 clean_version = distro.try_cleanup_distro_version(version)
302 if not clean_version:
303 _logger_pk.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package_name})
304 clean_arch = distro.canonical_machine(arch)
305 package = {'version': clean_version,
306 'name': package_name,
307 'arch': clean_arch,
308 'installed': (status == 'installed')}
309 _logger_pk.debug(_('Package: %s %r'), id, package)
310 self.package[str(id)] = package
312 def __details_cb(self, id, licence, group, detail, url, size):
313 details = {'licence': str(licence),
314 'group': str(group),
315 'detail': str(detail),
316 'url': str(url),
317 'size': int(size)}
318 _logger_pk.debug(_('Details: %s %r'), id, details)
319 self.details[id] = details
321 def __files_cb(self, id, files):
322 self.files[id] = files.split(';')
324 def __status_changed_cb(self, status):
325 pass