Release 1.4.1
[zeroinstall.git] / zeroinstall / injector / packagekit.py
blob4c4645a37693d99f8f195b73b672935ca5d9724a
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
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 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 try:
46 self._pk = dbus.Interface(dbus.SystemBus().get_object(
47 'org.freedesktop.PackageKit',
48 '/org/freedesktop/PackageKit', False),
49 'org.freedesktop.PackageKit')
50 _logger_pk.info(_('PackageKit dbus service found'))
51 except Exception as ex:
52 _logger_pk.info(_('PackageKit dbus service not found: %s'), ex)
53 self._pk = None
54 return self._pk
56 def get_candidates(self, package_name, factory, prefix):
57 """Add any cached candidates.
58 The candidates are those discovered by a previous call to L{fetch_candidates}.
59 @param package_name: the distribution's name for the package
60 @param factory: a function to add a new implementation to the feed
61 @param prefix: the prefix for the implementation's ID
62 """
63 candidates = self._candidates.get(package_name, None)
64 if candidates is None:
65 return
67 if isinstance(candidates, tasks.Blocker):
68 return # Fetch still in progress
70 for candidate in candidates:
71 impl_name = '%s:%s:%s:%s' % (prefix, package_name, candidate['version'], candidate['arch'])
73 impl = factory(impl_name, only_if_missing = True, installed = candidate['installed'])
74 if impl is None:
75 # (checking this way because the cached candidate['installed'] may be stale)
76 return # Already installed
78 impl.version = model.parse_version(candidate['version'])
79 if candidate['arch'] != '*':
80 impl.machine = candidate['arch']
82 def install(handler):
83 packagekit_id = candidate['packagekit_id']
84 def download_factory(url, hint):
85 return PackageKitDownload(url, hint, pk = self.pk, packagekit_id = packagekit_id)
86 dl = handler.get_download('packagekit:' + packagekit_id, factory = download_factory, hint = impl)
87 dl.expected_size = candidate['size']
88 return dl.downloaded
89 impl.download_sources.append(model.DistributionSource(package_name, candidate['size'], install))
91 @tasks.async
92 def fetch_candidates(self, package_names):
93 assert self.pk
95 # Batch requests up
96 self._next_batch |= set(package_names)
97 yield
98 package_names = self._next_batch
99 self._next_batch = set()
100 # The first fetch_candidates instance will now have all the packages
101 # For the others, package_names will now be empty
103 known = [self._candidates[p] for p in package_names if p in self._candidates]
104 # (use set because a single task may be checking multiple packages and we need
105 # to avoid duplicates).
106 in_progress = list(set([b for b in known if isinstance(b, tasks.Blocker)]))
107 _logger_pk.debug('Already downloading: %s', in_progress)
109 # Filter out the ones we've already fetched
110 package_names = [p for p in package_names if p not in self._candidates]
112 def do_batch(package_names):
113 #_logger_pk.info("sending %d packages in batch", len(package_names))
114 versions = {}
116 blocker = None
118 def error_cb(sender):
119 # Note: probably just means the package wasn't found
120 _logger_pk.info(_('Transaction failed: %s(%s)'), sender.error_code, sender.error_details)
121 blocker.trigger()
123 def details_cb(sender):
124 for packagekit_id, info in versions.items():
125 if packagekit_id in sender.details:
126 info.update(sender.details[packagekit_id])
127 info['packagekit_id'] = packagekit_id
128 if (info['name'] not in self._candidates or
129 isinstance(self._candidates[info['name']], tasks.Blocker)):
130 self._candidates[info['name']] = [info]
131 else:
132 self._candidates[info['name']].append(info)
133 else:
134 _logger_pk.info(_('Empty details for %s'), packagekit_id)
135 blocker.trigger()
137 def resolve_cb(sender):
138 if sender.package:
139 for packagekit_id, info in sender.package.iteritems():
140 parts = packagekit_id.split(';', 3)
141 if ':' in parts[3]:
142 parts[3] = parts[3].split(':', 1)[0]
143 packagekit_id = ';'.join(parts)
144 versions[packagekit_id] = info
145 tran = _PackageKitTransaction(self.pk, details_cb, error_cb)
146 tran.proxy.GetDetails(versions.keys())
147 else:
148 _logger_pk.info(_('Empty resolve for %s'), package_names)
149 blocker.trigger()
151 # Send queries
152 blocker = tasks.Blocker('PackageKit %s' % package_names)
153 for package in package_names:
154 self._candidates[package] = blocker
156 try:
157 _logger_pk.debug(_('Ask for %s'), package_names)
158 tran = _PackageKitTransaction(self.pk, resolve_cb, error_cb)
159 tran.proxy.Resolve('none', package_names)
161 in_progress.append(blocker)
162 except:
163 __, ex, tb = sys.exc_info()
164 blocker.trigger((ex, tb))
165 raise
167 # Now we've collected all the requests together, split them up into chunks
168 # that PackageKit can handle ( < 100 per batch )
169 #_logger_pk.info("sending %d packages", len(package_names))
170 while package_names:
171 next_batch = package_names[:MAX_PACKAGE_KIT_TRANSACTION_SIZE]
172 package_names = package_names[MAX_PACKAGE_KIT_TRANSACTION_SIZE:]
173 do_batch(next_batch)
175 while in_progress:
176 yield in_progress
177 in_progress = [b for b in in_progress if not b.happened]
179 class PackageKitDownload:
180 def __init__(self, url, hint, pk, packagekit_id):
181 self.url = url
182 self.status = download.download_starting
183 self.hint = hint
184 self.aborted_by_user = False
186 self.downloaded = None
188 self.expected_size = None # Final size (excluding skipped bytes)
190 self.packagekit_id = packagekit_id
191 self._impl = hint
192 self._transaction = None
193 self.pk = pk
195 def start(self):
196 assert self.status == download.download_starting
197 assert self.downloaded is None
199 def error_cb(sender):
200 self.status = download.download_failed
201 ex = SafeException('PackageKit install failed: %s' % (sender.error_details or sender.error_code))
202 self.downloaded.trigger(exception = (ex, None))
204 def installed_cb(sender):
205 self._impl.installed = True;
206 self.status = download.download_complete
207 self.downloaded.trigger()
209 def install_packages():
210 package_name = self.packagekit_id
211 self._transaction = _PackageKitTransaction(self.pk, installed_cb, error_cb)
212 self._transaction.compat_call([
213 ('InstallPackages', [package_name]),
214 ('InstallPackages', False, [package_name]),
217 _auth_wrapper(install_packages)
219 self.status = download.download_fetching
220 self.downloaded = tasks.Blocker('PackageKit install %s' % self.packagekit_id)
222 def abort(self):
223 _logger_pk.debug(_('Cancel transaction'))
224 self.aborted_by_user = True
225 self._transaction.proxy.Cancel()
226 self.status = download.download_failed
227 self.downloaded.trigger()
229 def get_current_fraction(self):
230 if self._transaction is None:
231 return None
232 percentage = self._transaction.getPercentage()
233 if percentage > 100:
234 return None
235 else:
236 return float(percentage) / 100.
238 def get_bytes_downloaded_so_far(self):
239 fraction = self.get_current_fraction()
240 if fraction is None:
241 return 0
242 else:
243 if self.expected_size is None:
244 return 0
245 return int(self.expected_size * fraction)
247 def _auth_wrapper(method, *args):
248 try:
249 return method(*args)
250 except dbus.exceptions.DBusException as e:
251 if e.get_dbus_name() != \
252 'org.freedesktop.PackageKit.Transaction.RefusedByPolicy':
253 raise
255 iface, auth = e.get_dbus_message().split()
256 if not auth.startswith('auth_'):
257 raise
259 _logger_pk.debug(_('Authentication required for %s'), auth)
261 pk_auth = dbus.SessionBus().get_object(
262 'org.freedesktop.PolicyKit.AuthenticationAgent', '/',
263 'org.gnome.PolicyKit.AuthorizationManager.SingleInstance')
265 if not pk_auth.ObtainAuthorization(iface, dbus.UInt32(0),
266 dbus.UInt32(os.getpid()), timeout=300):
267 raise
269 return method(*args)
271 class _PackageKitTransaction(object):
272 def __init__(self, pk, finished_cb=None, error_cb=None):
273 self._finished_cb = finished_cb
274 self._error_cb = error_cb
275 self.error_code = None
276 self.error_details = None
277 self.package = {}
278 self.details = {}
279 self.files = {}
281 self.object = dbus.SystemBus().get_object(
282 'org.freedesktop.PackageKit', pk.GetTid(), False)
283 self.proxy = dbus.Interface(self.object,
284 'org.freedesktop.PackageKit.Transaction')
285 self._props = dbus.Interface(self.object, dbus.PROPERTIES_IFACE)
287 self._signals = []
288 for signal, cb in [('Finished', self.__finished_cb),
289 ('ErrorCode', self.__error_code_cb),
290 ('StatusChanged', self.__status_changed_cb),
291 ('Package', self.__package_cb),
292 ('Details', self.__details_cb),
293 ('Files', self.__files_cb)]:
294 self._signals.append(self.proxy.connect_to_signal(signal, cb))
296 defaultlocale = locale.getdefaultlocale()[0]
297 if defaultlocale is not None:
298 self.compat_call([
299 ('SetLocale', defaultlocale),
300 ('SetHints', ['locale=%s' % defaultlocale]),
303 def getPercentage(self):
304 result = self.get_prop('Percentage')
305 if result is None:
306 result, __, __, __ = self.proxy.GetProgress()
307 return result
309 def get_prop(self, prop, default = None):
310 try:
311 return self._props.Get('org.freedesktop.PackageKit.Transaction', prop)
312 except:
313 return default
315 def compat_call(self, calls):
316 for call in calls:
317 method = call[0]
318 args = call[1:]
319 try:
320 dbus_method = self.proxy.get_dbus_method(method)
321 return dbus_method(*args)
322 except dbus.exceptions.DBusException as e:
323 if e.get_dbus_name() != \
324 'org.freedesktop.DBus.Error.UnknownMethod':
325 raise
326 raise Exception('Cannot call %r DBus method' % calls)
328 def __finished_cb(self, exit, runtime):
329 _logger_pk.debug(_('Transaction finished: %s'), exit)
331 for i in self._signals:
332 i.remove()
334 if self.error_code is not None:
335 self._error_cb(self)
336 else:
337 self._finished_cb(self)
339 def __error_code_cb(self, code, details):
340 _logger_pk.info(_('Transaction failed: %s(%s)'), details, code)
341 self.error_code = code
342 self.error_details = details
344 def __package_cb(self, status, id, summary):
345 from zeroinstall.injector import distro
347 package_name, version, arch, repo_ = id.split(';')
348 clean_version = distro.try_cleanup_distro_version(version)
349 if not clean_version:
350 _logger_pk.warn(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package_name})
351 clean_arch = distro.canonical_machine(arch)
352 package = {'version': clean_version,
353 'name': package_name,
354 'arch': clean_arch,
355 'installed': (status == 'installed')}
356 _logger_pk.debug(_('Package: %s %r'), id, package)
357 self.package[str(id)] = package
359 def __details_cb(self, id, licence, group, detail, url, size):
360 details = {'licence': str(licence),
361 'group': str(group),
362 'detail': str(detail),
363 'url': str(url),
364 'size': int(size)}
365 _logger_pk.debug(_('Details: %s %r'), id, details)
366 self.details[id] = details
368 def __files_cb(self, id, files):
369 self.files[id] = files.split(';')
371 def __status_changed_cb(self, status):
372 pass