Added CA verification for Fedora and OpenSUSE systems too
[zeroinstall.git] / zeroinstall / injector / packagekit.py
blob97db33198636696c09e674f4a1bec8ad6af82a82
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 as 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 # PackageKit is really slow at handling separate queries, so we use this to
33 # batch them up.
34 self._next_batch = set()
36 @property
37 def available(self):
38 return self.pk is not None
40 @property
41 def pk(self):
42 if self._pk is False:
43 try:
44 self._pk = dbus.Interface(dbus.SystemBus().get_object(
45 'org.freedesktop.PackageKit',
46 '/org/freedesktop/PackageKit', False),
47 'org.freedesktop.PackageKit')
48 _logger_pk.info(_('PackageKit dbus service found'))
49 except Exception as ex:
50 _logger_pk.info(_('PackageKit dbus service not found: %s'), ex)
51 self._pk = None
52 return self._pk
54 def get_candidates(self, package_name, factory, prefix):
55 """Add any cached candidates.
56 The candidates are those discovered by a previous call to L{fetch_candidates}.
57 @param package_name: the distribution's name for the package
58 @param factory: a function to add a new implementation to the feed
59 @param prefix: the prefix for the implementation's ID
60 """
61 candidate = self._candidates.get(package_name, None)
62 if candidate is None:
63 return
65 if isinstance(candidate, tasks.Blocker):
66 return # Fetch still in progress
68 impl_name = '%s:%s:%s:%s' % (prefix, package_name, candidate['version'], candidate['arch'])
70 impl = factory(impl_name, only_if_missing = True, installed = candidate['installed'])
71 if impl is None:
72 # (checking this way because the cached candidate['installed'] may be stale)
73 return # Already installed
75 impl.version = model.parse_version(candidate['version'])
76 if candidate['arch'] != '*':
77 impl.machine = candidate['arch']
79 def install(handler):
80 packagekit_id = candidate['packagekit_id']
81 def download_factory(url, hint):
82 return PackageKitDownload(url, hint, pk = self.pk, packagekit_id = packagekit_id)
83 dl = handler.get_download('packagekit:' + packagekit_id, factory = download_factory, hint = impl)
84 dl.expected_size = candidate['size']
85 return dl.downloaded
86 impl.download_sources.append(model.DistributionSource(package_name, candidate['size'], install))
88 @tasks.async
89 def fetch_candidates(self, package_names):
90 assert self.pk
92 # Batch requests up
93 self._next_batch |= set(package_names)
94 yield
95 package_names = self._next_batch
96 self._next_batch = set()
97 # The first fetch_candidates instance will now have all the packages
98 # For the others, package_names will now be empty
100 known = [self._candidates[p] for p in package_names if p in self._candidates]
101 # (use set because a single task may be checking multiple packages and we need
102 # to avoid duplicates).
103 in_progress = list(set([b for b in known if isinstance(b, tasks.Blocker)]))
104 _logger_pk.debug('Already downloading: %s', in_progress)
106 # Filter out the ones we've already fetched
107 package_names = [p for p in package_names if p not in self._candidates]
109 if package_names:
110 versions = {}
112 blocker = None
114 def error_cb(sender):
115 # Note: probably just means the package wasn't found
116 _logger_pk.info(_('Transaction failed: %s(%s)'), sender.error_code, sender.error_details)
117 blocker.trigger()
119 def details_cb(sender):
120 for packagekit_id, info in versions.items():
121 if packagekit_id in sender.details:
122 info.update(sender.details[packagekit_id])
123 info['packagekit_id'] = packagekit_id
124 self._candidates[info['name']] = info
125 else:
126 _logger_pk.info(_('Empty details for %s'), packagekit_id)
127 blocker.trigger()
129 def resolve_cb(sender):
130 if sender.package:
131 for packagekit_id, info in sender.package.iteritems():
132 parts = packagekit_id.split(';', 3)
133 if ':' in parts[3]:
134 parts[3] = parts[3].split(':', 1)[0]
135 packagekit_id = ';'.join(parts)
136 versions[packagekit_id] = info
137 tran = _PackageKitTransaction(self.pk, details_cb, error_cb)
138 tran.proxy.GetDetails(versions.keys())
139 else:
140 _logger_pk.info(_('Empty resolve for %s'), package_names)
141 blocker.trigger()
143 # Send queries
144 blocker = tasks.Blocker('PackageKit %s' % package_names)
145 for package in package_names:
146 self._candidates[package] = blocker
148 _logger_pk.debug(_('Ask for %s'), package_names)
149 tran = _PackageKitTransaction(self.pk, resolve_cb, error_cb)
150 tran.proxy.Resolve('none', package_names)
152 in_progress.append(blocker)
154 while in_progress:
155 yield in_progress
156 in_progress = [b for b in in_progress if not b.happened]
158 class PackageKitDownload(download.Download):
159 def __init__(self, url, hint, pk, packagekit_id):
160 download.Download.__init__(self, url, hint)
162 self.packagekit_id = packagekit_id
163 self._impl = hint
164 self._transaction = None
165 self.pk = pk
167 def start(self):
168 assert self.status == download.download_starting
169 assert self.downloaded is None
171 def error_cb(sender):
172 self.status = download.download_failed
173 ex = SafeException('PackageKit install failed: %s' % (sender.error_details or sender.error_code))
174 self.downloaded.trigger(exception = (ex, None))
176 def installed_cb(sender):
177 self._impl.installed = True;
178 self.status = download.download_complete
179 self.downloaded.trigger()
181 def install_packages():
182 package_name = self.packagekit_id
183 self._transaction = _PackageKitTransaction(self.pk, installed_cb, error_cb)
184 self._transaction.compat_call([
185 ('InstallPackages', [package_name]),
186 ('InstallPackages', False, [package_name]),
189 _auth_wrapper(install_packages)
191 self.status = download.download_fetching
192 self.downloaded = tasks.Blocker('PackageKit install %s' % self.packagekit_id)
194 def abort(self):
195 _logger_pk.debug(_('Cancel transaction'))
196 self.aborted_by_user = True
197 self._transaction.proxy.Cancel()
198 self.status = download.download_failed
199 self.downloaded.trigger()
201 def get_current_fraction(self):
202 if self._transaction is None:
203 return None
204 percentage = self._transaction.getPercentage()
205 if percentage > 100:
206 return None
207 else:
208 return float(percentage) / 100.
210 def get_bytes_downloaded_so_far(self):
211 fraction = self.get_current_fraction()
212 if fraction is None:
213 return 0
214 else:
215 if self.expected_size is None:
216 return 0
217 return int(self.expected_size * fraction)
219 def _auth_wrapper(method, *args):
220 try:
221 return method(*args)
222 except dbus.exceptions.DBusException as e:
223 if e.get_dbus_name() != \
224 'org.freedesktop.PackageKit.Transaction.RefusedByPolicy':
225 raise
227 iface, auth = e.get_dbus_message().split()
228 if not auth.startswith('auth_'):
229 raise
231 _logger_pk.debug(_('Authentication required for %s'), auth)
233 pk_auth = dbus.SessionBus().get_object(
234 'org.freedesktop.PolicyKit.AuthenticationAgent', '/',
235 'org.gnome.PolicyKit.AuthorizationManager.SingleInstance')
237 if not pk_auth.ObtainAuthorization(iface, dbus.UInt32(0),
238 dbus.UInt32(os.getpid()), timeout=300):
239 raise
241 return method(*args)
243 class _PackageKitTransaction(object):
244 def __init__(self, pk, finished_cb=None, error_cb=None):
245 self._finished_cb = finished_cb
246 self._error_cb = error_cb
247 self.error_code = None
248 self.error_details = None
249 self.package = {}
250 self.details = {}
251 self.files = {}
253 self.object = dbus.SystemBus().get_object(
254 'org.freedesktop.PackageKit', pk.GetTid(), False)
255 self.proxy = dbus.Interface(self.object,
256 'org.freedesktop.PackageKit.Transaction')
257 self._props = dbus.Interface(self.object, dbus.PROPERTIES_IFACE)
259 self._signals = []
260 for signal, cb in [('Finished', self.__finished_cb),
261 ('ErrorCode', self.__error_code_cb),
262 ('StatusChanged', self.__status_changed_cb),
263 ('Package', self.__package_cb),
264 ('Details', self.__details_cb),
265 ('Files', self.__files_cb)]:
266 self._signals.append(self.proxy.connect_to_signal(signal, cb))
268 defaultlocale = locale.getdefaultlocale()[0]
269 if defaultlocale is not None:
270 self.compat_call([
271 ('SetLocale', defaultlocale),
272 ('SetHints', ['locale=%s' % defaultlocale]),
275 def getPercentage(self):
276 result = self.get_prop('Percentage')
277 if result is None:
278 result, __, __, __ = self.proxy.GetProgress()
279 return result
281 def get_prop(self, prop, default = None):
282 try:
283 return self._props.Get('org.freedesktop.PackageKit.Transaction', prop)
284 except:
285 return default
287 def compat_call(self, calls):
288 for call in calls:
289 method = call[0]
290 args = call[1:]
291 try:
292 dbus_method = self.proxy.get_dbus_method(method)
293 return dbus_method(*args)
294 except dbus.exceptions.DBusException as e:
295 if e.get_dbus_name() != \
296 'org.freedesktop.DBus.Error.UnknownMethod':
297 raise
298 raise Exception('Cannot call %r DBus method' % calls)
300 def __finished_cb(self, exit, runtime):
301 _logger_pk.debug(_('Transaction finished: %s'), exit)
303 for i in self._signals:
304 i.remove()
306 if self.error_code is not None:
307 self._error_cb(self)
308 else:
309 self._finished_cb(self)
311 def __error_code_cb(self, code, details):
312 _logger_pk.info(_('Transaction failed: %s(%s)'), details, code)
313 self.error_code = code
314 self.error_details = details
316 def __package_cb(self, status, id, summary):
317 from zeroinstall.injector import distro
319 package_name, version, arch, repo_ = id.split(';')
320 clean_version = distro.try_cleanup_distro_version(version)
321 if not clean_version:
322 _logger_pk.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package_name})
323 clean_arch = distro.canonical_machine(arch)
324 package = {'version': clean_version,
325 'name': package_name,
326 'arch': clean_arch,
327 'installed': (status == 'installed')}
328 _logger_pk.debug(_('Package: %s %r'), id, package)
329 self.package[str(id)] = package
331 def __details_cb(self, id, licence, group, detail, url, size):
332 details = {'licence': str(licence),
333 'group': str(group),
334 'detail': str(detail),
335 'url': str(url),
336 'size': int(size)}
337 _logger_pk.debug(_('Details: %s %r'), id, details)
338 self.details[id] = details
340 def __files_cb(self, id, files):
341 self.files[id] = files.split(';')
343 def __status_changed_cb(self, status):
344 pass