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