1 """
2 PackageKit integration.
3 """
4
5
6
7
8 import os, sys
9 import locale
10 import logging
11 from zeroinstall import _, SafeException
12
13 from zeroinstall.support import tasks, unicode
14 from zeroinstall.injector import download, model
15
16 _logger_pk = logging.getLogger('0install.packagekit')
17
18
19 try:
20 import dbus
21 import dbus.mainloop.glib
22 dbus.mainloop.glib.DBusGMainLoop(set_as_default=True)
23 except Exception as ex:
24 _logger_pk.info("D-BUS not available: %s", ex)
25 dbus = None
26
27 MAX_PACKAGE_KIT_TRANSACTION_SIZE = 100
31 self._pk = False
32
33 self._candidates = {}
34
35
36
37 self._next_batch = set()
38
39 @property
41 return self.pk is not None
42
43 @property
45 if self._pk is False:
46 if dbus is None:
47 self._pk = None
48 else:
49 try:
50 self._pk = dbus.Interface(dbus.SystemBus().get_object(
51 'org.freedesktop.PackageKit',
52 '/org/freedesktop/PackageKit', False),
53 'org.freedesktop.PackageKit')
54 _logger_pk.info(_('PackageKit dbus service found'))
55 except Exception as ex:
56 _logger_pk.info(_('PackageKit dbus service not found: %s'), ex)
57 self._pk = None
58 return self._pk
59
61 """Add any cached candidates.
62 The candidates are those discovered by a previous call to L{fetch_candidates}.
63 @param package_name: the distribution's name for the package
64 @type package_name: str
65 @param factory: a function to add a new implementation to the feed
66 @param prefix: the prefix for the implementation's ID
67 @type prefix: str"""
68 candidates = self._candidates.get(package_name, None)
69 if candidates is None:
70 return
71
72 if isinstance(candidates, tasks.Blocker):
73 return
74
75 for candidate in candidates:
76 impl_name = '%s:%s:%s:%s' % (prefix, package_name, candidate['version'], candidate['arch'])
77
78 impl = factory(impl_name, only_if_missing = True, installed = candidate['installed'])
79 if impl is None:
80
81 return
82
83 impl.version = model.parse_version(candidate['version'])
84 if candidate['arch'] != '*':
85 impl.machine = candidate['arch']
86
87 def install(handler, candidate = candidate, impl = impl):
88 packagekit_id = candidate['packagekit_id']
89 dl = PackageKitDownload('packagekit:' + packagekit_id, hint = impl, pk = self.pk, packagekit_id = packagekit_id, expected_size = candidate['size'])
90 handler.monitor_download(dl)
91 return dl.downloaded
92 impl.download_sources.append(model.DistributionSource(package_name, candidate['size'], install))
93
94 @tasks.async
96 """@type package_names: [str]"""
97 assert self.pk
98
99
100 self._next_batch |= set(package_names)
101 yield
102 batched_package_names = self._next_batch
103 self._next_batch = set()
104
105
106
107 self._fetch_batch(list(batched_package_names))
108
109 results = [self._candidates[p] for p in package_names]
110
111
112
113 in_progress = list(set([b for b in results if isinstance(b, tasks.Blocker)]))
114 _logger_pk.debug('Currently querying PackageKit for: %s', in_progress)
115
116 while in_progress:
117 yield in_progress
118 in_progress = [b for b in in_progress if not b.happened]
119
121 """Ensure that each of these packages is in self._candidates.
122 Start a new fetch if necessary. Ignore packages that are already downloaded or
123 in the process of being downloaded."""
124
125
126 package_names = [n for n in package_names if n not in self._candidates]
127
128 def do_batch(package_names):
129
130 versions = {}
131
132 blocker = None
133
134 def error_cb(sender):
135
136 _logger_pk.info(_('Transaction failed: %s(%s)'), sender.error_code, sender.error_details)
137 blocker.trigger()
138
139 def details_cb(sender):
140
141
142 details = {}
143 for packagekit_id, d in sender.details.items():
144 details[unicode(packagekit_id)] = d
145
146 for packagekit_id in details:
147 if packagekit_id not in versions:
148 _logger_pk.info("Unexpected package info for '%s'; was expecting one of %r", packagekit_id, list(versions.keys()))
149
150 for packagekit_id, info in versions.items():
151 if packagekit_id in details:
152 info.update(details[packagekit_id])
153 info['packagekit_id'] = packagekit_id
154 if (info['name'] not in self._candidates or
155 isinstance(self._candidates[info['name']], tasks.Blocker)):
156 self._candidates[info['name']] = [info]
157 else:
158 self._candidates[info['name']].append(info)
159 else:
160 _logger_pk.info(_('Empty details for %s'), packagekit_id)
161 blocker.trigger()
162
163 def resolve_cb(sender):
164 if sender.package:
165 _logger_pk.debug(_('Resolved %r'), sender.package)
166 for packagekit_id, info in sender.package.items():
167 packagekit_id = unicode(packagekit_id)
168 parts = packagekit_id.split(';', 3)
169 if ':' in parts[3]:
170 parts[3] = parts[3].split(':', 1)[0]
171 packagekit_id = ';'.join(parts)
172 versions[packagekit_id] = info
173 tran = _PackageKitTransaction(self.pk, details_cb, error_cb)
174 tran.proxy.GetDetails(list(versions.keys()))
175 else:
176 _logger_pk.info(_('Empty resolve for %s'), package_names)
177 blocker.trigger()
178
179
180 blocker = tasks.Blocker('PackageKit %s' % package_names)
181 for package in package_names:
182 self._candidates[package] = blocker
183
184 try:
185 _logger_pk.debug(_('Ask for %s'), package_names)
186 tran = _PackageKitTransaction(self.pk, resolve_cb, error_cb)
187 tran.Resolve(package_names)
188 except:
189 __, ex, tb = sys.exc_info()
190 blocker.trigger((ex, tb))
191 raise
192
193
194
195
196 while package_names:
197 next_batch = package_names[:MAX_PACKAGE_KIT_TRANSACTION_SIZE]
198 package_names = package_names[MAX_PACKAGE_KIT_TRANSACTION_SIZE:]
199 do_batch(next_batch)
200
202 - def __init__(self, url, hint, pk, packagekit_id, expected_size):
203 """@type url: str
204 @type packagekit_id: str
205 @type expected_size: int"""
206 self.url = url
207 self.status = download.download_fetching
208 self.hint = hint
209 self.aborted_by_user = False
210
211 self.downloaded = None
212
213 self.expected_size = expected_size
214
215 self.packagekit_id = packagekit_id
216 self._impl = hint
217 self._transaction = None
218 self.pk = pk
219
220 def error_cb(sender):
221 self.status = download.download_failed
222 ex = SafeException('PackageKit install failed: %s' % (sender.error_details or sender.error_code))
223 self.downloaded.trigger(exception = (ex, None))
224
225 def installed_cb(sender):
226 assert not self._impl.installed, self._impl
227 self._impl.installed = True
228 self._impl.distro.installed_fixup(self._impl)
229
230 self.status = download.download_complete
231 self.downloaded.trigger()
232
233 def install_packages():
234 package_name = self.packagekit_id
235 self._transaction = _PackageKitTransaction(self.pk, installed_cb, error_cb)
236 self._transaction.InstallPackages([package_name])
237
238 _auth_wrapper(install_packages)
239
240 self.downloaded = tasks.Blocker('PackageKit install %s' % self.packagekit_id)
241
248
250 """@rtype: float"""
251 if self._transaction is None:
252 return None
253 percentage = self._transaction.getPercentage()
254 if percentage > 100:
255 return None
256 else:
257 return float(percentage) / 100.
258
260 """@rtype: int"""
261 fraction = self.get_current_fraction()
262 if fraction is None:
263 return 0
264 else:
265 if self.expected_size is None:
266 return 0
267 return int(self.expected_size * fraction)
268
270 try:
271 return method(*args)
272 except dbus.exceptions.DBusException as e:
273 if e.get_dbus_name() != \
274 'org.freedesktop.PackageKit.Transaction.RefusedByPolicy':
275 raise
276
277 iface, auth = e.get_dbus_message().split()
278 if not auth.startswith('auth_'):
279 raise
280
281 _logger_pk.debug(_('Authentication required for %s'), auth)
282
283 pk_auth = dbus.SessionBus().get_object(
284 'org.freedesktop.PolicyKit.AuthenticationAgent', '/',
285 'org.gnome.PolicyKit.AuthorizationManager.SingleInstance')
286
287 if not pk_auth.ObtainAuthorization(iface, dbus.UInt32(0),
288 dbus.UInt32(os.getpid()), timeout=300):
289 raise
290
291 return method(*args)
292
294 - def __init__(self, pk, finished_cb=None, error_cb=None):
295 self._finished_cb = finished_cb
296 self._error_cb = error_cb
297 self.error_code = None
298 self.error_details = None
299 self.package = {}
300 self.details = {}
301 self.files = {}
302
303 try:
304
305
306 tid = pk.GetTid()
307 self.have_0_8_1_api = False
308 except dbus.exceptions.DBusException:
309 tid = pk.CreateTransaction()
310 self.have_0_8_1_api = True
311
312 self.object = dbus.SystemBus().get_object(
313 'org.freedesktop.PackageKit', tid, False)
314 self.proxy = dbus.Interface(self.object,
315 'org.freedesktop.PackageKit.Transaction')
316 self._props = dbus.Interface(self.object, dbus.PROPERTIES_IFACE)
317
318 self._signals = []
319 for signal, cb in [('Finished', self.__finished_cb),
320 ('ErrorCode', self.__error_code_cb),
321 ('StatusChanged', self.__status_changed_cb),
322 ('Package', self.__package_cb),
323 ('Details', self.__details_cb),
324 ('Files', self.__files_cb)]:
325 self._signals.append(self.proxy.connect_to_signal(signal, cb))
326
327 defaultlocale = locale.getdefaultlocale()[0]
328 if defaultlocale is not None:
329 self.compat_call([
330 ('SetHints', ['locale=%s' % defaultlocale]),
331 ('SetLocale', defaultlocale),
332 ])
333
335 """@rtype: int"""
336 result = self.get_prop('Percentage')
337 if result is None:
338 result, __, __, __ = self.proxy.GetProgress()
339 return result
340
341 - def get_prop(self, prop, default = None):
342 """@type prop: str"""
343 try:
344 return self._props.Get('org.freedesktop.PackageKit.Transaction', prop)
345 except:
346 return default
347
348
349
351 for call in calls:
352 method = call[0]
353 args = call[1:]
354 try:
355 dbus_method = self.proxy.get_dbus_method(method)
356 return dbus_method(*args)
357 except dbus.exceptions.DBusException as e:
358 if e.get_dbus_name() not in (
359 'org.freedesktop.DBus.Error.UnknownMethod',
360 'org.freedesktop.DBus.Error.InvalidArgs'):
361 raise
362 raise Exception('Cannot call %r DBus method' % calls)
363
365 """@type exit: str
366 @type runtime: int"""
367 _logger_pk.debug(_('Transaction finished: %s'), exit)
368
369 for i in self._signals:
370 i.remove()
371
372 if self.error_code is not None:
373 self._error_cb(self)
374 else:
375 self._finished_cb(self)
376
378 _logger_pk.info(_('Transaction failed: %s(%s)'), details, code)
379 self.error_code = code
380 self.error_details = details
381
383 """@type status: str
384 @type id: str
385 @type summary: str"""
386 try:
387 from zeroinstall.injector import distro
388
389 package_name, version, arch, repo_ = id.split(';')
390 clean_version = distro.try_cleanup_distro_version(version)
391 if not clean_version:
392 _logger_pk.info(_("Can't parse distribution version '%(version)s' for package '%(package)s'"), {'version': version, 'package': package_name})
393 return
394 clean_arch = distro.canonical_machine(arch)
395 package = {'version': clean_version,
396 'name': package_name,
397 'arch': clean_arch,
398 'installed': (status == 'installed')}
399 _logger_pk.debug(_('Package: %s %r'), id, package)
400 self.package[str(id)] = package
401 except Exception as ex:
402 _logger_pk.warn("__package_cb(%s, %s, %s): %s", status, id, summary, ex)
403
404 - def __details_cb(self, id, licence, group, detail, url, size):
405 """@type id: str
406 @type licence: str
407 @type group: str
408 @type detail: str
409 @type url: str
410 @type size: int"""
411 details = {'licence': str(licence),
412 'group': str(group),
413 'detail': str(detail),
414 'url': str(url),
415 'size': int(size)}
416 _logger_pk.debug(_('Details: %s %r'), id, details)
417 self.details[id] = details
418
420 self.files[id] = files.split(';')
421
423 """@type status: str"""
424 pass
425
427 """@type package_names: [str]"""
428 if self.have_0_8_1_api:
429 self.proxy.Resolve(dbus.UInt64(0), package_names)
430 else:
431 self.proxy.Resolve('none', package_names)
432
434 """@type package_names: [str]"""
435 if self.have_0_8_1_api:
436 self.proxy.InstallPackages(dbus.UInt64(0), package_names)
437 else:
438 self.compat_call([
439 ('InstallPackages', False, package_names),
440 ('InstallPackages', package_names),
441 ])
442