Fetch transaction data from system bus (recognize if daemon crashed)
[appstream:software-center.git] / softwarecenter / db / pkginfo_impl / packagekit.py
1 # Copyright (C) 2011 Canonical
2 #
3 # Authors:
4 #  Alex Eftimie
5 #  Matthias Klumpp
6 #
7 # This program is free software; you can redistribute it and/or modify it under
8 # the terms of the GNU General Public License as published by the Free Software
9 # Foundation; version 3.
10 #
11 # This program is distributed in the hope that it will be useful, but WITHOUT
12 # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
13 # FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
14 # details.
15 #
16 # You should have received a copy of the GNU General Public License along with
17 # this program; if not, write to the Free Software Foundation, Inc.,
18 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
19
20 from gi.repository import PackageKitGlib as packagekit
21 from gi.repository import GObject
22 from gi.repository import GLib as glib
23 from gi.repository import Gio as gio
24 import logging
25 import locale
26 import os
27
28 from gettext import gettext as _
29
30 from softwarecenter.db.pkginfo import PackageInfo, _Version
31 from softwarecenter.distro import get_distro
32 from softwarecenter.db.database import StoreDatabase
33 from softwarecenter.paths import XAPIAN_BASE_PATH
34
35 LOG = logging.getLogger('softwarecenter.db.packagekit')
36
37 class PkOrigin:
38     def __init__(self, repo):
39         if repo:
40             repo_id = repo.get_property('repo-id')
41             if repo_id.endswith('-source'):
42                 repo_id = repo_id[:-len('-source')]
43                 self.component = 'source'
44             elif repo_id.endswith('-debuginfo'):
45                 repo_id = repo_id[:-len('-debuginfo')]
46                 self.component = 'debuginfo'
47             else:
48                 self.component = 'main'
49
50             if repo_id == 'updates':
51                 self.origin = get_distro().get_distro_channel_name()
52                 self.archive = 'stable'
53             elif repo_id == 'updates-testing':
54                 self.origin = get_distro().get_distro_channel_name()
55                 self.archive = 'testing'
56             elif repo_id.endswith('-updates-testing'):
57                 self.origin = repo_id[:-len('-updates-testing')]
58                 self.archive = 'testing'
59             else:
60                 self.origin = repo_id
61                 self.archive = 'stable'
62
63             self.trusted = True
64             self.label = repo.get_property('description')
65         else:
66             self.origin = 'unknown'
67             self.archive = 'unknown'
68             self.trusted = False
69             self.label = _("Unknown repository")
70             self.component = 'main'
71         self.site = ''
72
73
74 class PackagekitVersion(_Version):
75     def __init__(self, package, pkginfo):
76         self.package = package
77         self.pkginfo = pkginfo
78
79     @property
80     def description(self):
81         pkgid = self.package.get_id()
82         return self.pkginfo.get_description(pkgid)
83
84     @property
85     def downloadable(self):
86         return True  # FIXME: check for an equivalent
87
88     @property
89     def summary(self):
90         return self.package.get_property('summary')
91
92     @property
93     def size(self):
94         return self.pkginfo.get_size(self.package.get_name())
95
96     @property
97     def installed_size(self):
98         """In packagekit, installed_size can be fetched only for installed
99         packages, and is stored in the same 'size' property as the package
100         size"""
101         return self.pkginfo.get_installed_size(self.package.get_name())
102
103     @property
104     def version(self):
105         return self.package.get_version()
106
107     @property
108     def origins(self):
109         return self.pkginfo.get_origins(self.package.get_name())
110
111
112 def make_locale_string():
113     loc = locale.getlocale(locale.LC_MESSAGES)
114     if loc[1]:
115         return loc[0] + '.' + loc[1]
116     return loc[0]
117
118
119 class PackagekitInfo(PackageInfo):
120     USE_CACHE = True
121
122     def __init__(self):
123         super(PackagekitInfo, self).__init__()
124         self.pkclient = packagekit.Client()
125         self.pkclient.set_locale(make_locale_string())
126         self._cache_pkg_filter_none = {} # temporary hack for decent testing
127         self._cache_pkg_filter_newest = {} # temporary hack for decent testing
128         self._cache_details = {} # temporary hack for decent testing
129         self._notfound_cache_pkg = []
130         self._repocache = {}
131         self._ready = False
132         self.distro = get_distro()
133         self._pkgs_cache = {}
134
135     def __contains__(self, pkgname):
136         # setting it like this for now
137         return pkgname not in self._notfound_cache_pkg
138
139     def open(self):
140         """ (re)open the cache, this sends cache-invalid, cache-ready signals
141         """
142         LOG.info("packagekit.cache.open()")
143         self.emit("cache-invalid")
144         if not self._ready or not self._pkgs_cache:
145             self._fill_package_cache_from_cache(force = True)
146             self._update_package_cache()
147
148         if self._ready:
149             self.emit("cache-ready")
150
151     def prefill_cache(self, wanted_pkgs = None, prefill_descriptions = False, only_newest = False):
152         pass
153
154     def update_installed_status(self, pkgname, installed):
155         pkgs = self._get_packages(pkgname)
156         if pkgs is None:
157             return
158         for p in pkgs:
159             if installed:
160                 if p.get_info() == packagekit.InfoEnum.AVAILABLE:
161                     p.set_property("status", packagekit.InfoEnum.INSTALLED)
162             else:
163                 p.set_property("status", packagekit.InfoEnum.AVAILABLE)
164
165     def is_installed(self, pkgname):
166         pkgs = self._get_packages(pkgname)
167         if pkgs is None:
168             return False
169         for p in pkgs:
170             if p.get_info() == packagekit.InfoEnum.INSTALLED:
171                 return True
172         return False
173
174     def is_upgradable(self, pkgname):
175         # FIXME: how is this done via PK ?
176         return False
177
178     def is_available(self, pkgname):
179         # FIXME: i don't think this is being used
180         return True
181
182     def get_installed(self, pkgname):
183         for p in self._get_packages(pkgname):
184             if p.get_info() == packagekit.InfoEnum.INSTALLED:
185                 return PackagekitVersion(p, self)
186         return None
187
188     def get_candidate(self, pkgname):
189         p = self._get_one_package(pkgname, pfilter=packagekit.FilterEnum.NEWEST)
190         return PackagekitVersion(p, self) if p else None
191
192     def get_versions(self, pkgname):
193         return [PackagekitVersion(p, self) for p in self._get_packages(pkgname)]
194
195     def get_section(self, pkgname):
196         # FIXME: things are fuzzy here - group-section association
197         p = self._get_one_package(pkgname)
198         if p:
199             return packagekit.group_enum_to_string(p.get_property('group'))
200
201     def get_summary(self, pkgname):
202         p = self._get_one_package(pkgname)
203         return p.get_property('summary') if p else ''
204
205     def get_description(self, packageid):
206         p = self._get_package_details(packageid)
207         return p.get_property('description').replace('\n', ' ') if p else ''
208
209     def get_website(self, pkgname):
210         p = self._get_one_package(pkgname)
211         if not p:
212             return ''
213         p = self._get_package_details(p.get_id())
214         return p.get_property('url') if p else ''
215
216     def get_installed_files(self, pkgname):
217         p = self._get_one_package(pkgname)
218         if not p:
219             return []
220         res = self.pkclient.get_files((p.get_id(),), None, self._on_progress_changed, None)
221         files = res.get_files_array()
222         if not files:
223             return []
224         return files[0].get_property('files')
225
226     def get_size(self, pkgname):
227         p = self._get_one_package(pkgname)
228         if not p:
229             return -1
230         p = self._get_package_details(p.get_id())
231         return p.get_property('size') if p else -1
232
233     def get_installed_size(self, pkgname):
234         return self.get_size(pkgname)
235
236     def get_origins(self, pkgname, cache=USE_CACHE):
237         out = set()
238         out.add(PkOrigin(None))
239         return out
240         self._get_repolist()
241         # FIXME: this is wrong, as cache is not about NOT_INSTALLED; and if we
242         # don't use the cache, we use NOT_INSTALLED (which is possibly wrong
243         # too?)
244         if cache and (pkgname in self._cache_pkg_filter_none.keys()):
245             pkgs = self._cache_pkg_filter_none[pkgname]
246         else:
247             pkgs = self._get_packages(pkgname, pfilter=packagekit.FilterEnum.NOT_INSTALLED)
248
249         out = set()
250
251         for p in pkgs:
252             repoid = p.get_data()
253             try:
254                 out.add(PkOrigin(self._repocache[repoid]))
255             except KeyError:
256                 # could be a removed repository
257                 LOG.info('key %s not found in repocache' % repoid)
258                 out.add(PkOrigin(None))
259
260         return out
261
262     def get_origin(self, pkgname):
263         p = self._get_one_package(pkgname)
264         if not p:
265             return ''
266         origin = p.get_data()
267         if origin.startswith('installed:'):
268             return origin[len('installed:'):]
269         return origin
270
271     def component_available(self, distro_codename, component):
272         # FIXME stub
273         return True
274
275     def get_addons(self, pkgname, ignore_installed=True):
276         # FIXME implement it
277         return ([], [])
278
279     def get_packages_removed_on_remove(self, pkg):
280         """ Returns a package names list of reverse dependencies
281         which will be removed if the package is removed."""
282         p = self.get_installed(pkg.name)
283         if not p:
284             return []
285         autoremove = False
286         # simulate RemovePackages()
287         res = self.pkclient.remove_packages(packagekit.TransactionFlagEnum.SIMULATE,
288                                             (p.package.get_id(),),
289                                             True, # allow deps
290                                             autoremove,
291                                             None,
292                                             self._on_progress_changed, None)
293         if not res:
294             return []
295         return [p.get_name() for p in res.get_package_array() if p.get_name() != pkg.name]
296
297     def get_packages_removed_on_install(self, pkg):
298         """ Returns a package names list of dependencies
299         which will be removed if the package is installed."""
300         p = self.get_candidate(pkg.name)
301         if not p:
302             return []
303         # simulate InstallPackages()
304         res = self.pkclient.install_packages(packagekit.TransactionFlagEnum.SIMULATE,
305                                             (p.package.get_id(),),
306                                             None,
307                                             self._on_progress_changed, None)
308         if not res:
309             return []
310         return [p.get_name() for p in res.get_package_array() if (p.get_name() != pkg.name) and p.get_info() == packagekit.InfoEnum.INSTALLED]
311
312     def get_total_size_on_install(self, pkgname,
313                                   addons_install=None, addons_remove=None,
314                                   archive_suite=None):
315         """ Returns a tuple (download_size, installed_size)
316         with disk size in KB calculated for pkgname installation
317         plus addons change.
318         """
319         # FIXME: support archive_suite here too
320
321         # FIXME: PackageKit reports only one size at a time
322         if self.is_installed(pkgname):
323             return (0, self.get_size(pkgname))
324         else:
325             return (self.get_size(pkgname), 0)
326
327     @property
328     def ready(self):
329         """ No PK equivalent, simply returning True """
330         return True
331
332     def get_license(self, pkgname):
333         p = self._get_one_package(pkgname)
334         if not p:
335             return ""
336         details = self._get_package_details(p.get_property('package-id'))
337         if not details:
338             return ""
339         return details.get_property('license')
340
341     """ private methods """
342
343     def _add_package_to_cache(self, pkg, cache = None):
344         if cache is None:
345             cache = self._pkgs_cache
346         cache.update({ pkg.get_name() : pkg })
347
348
349     def _fill_package_cache_from_cache(self, force = False):
350         """Build a package-cache, to allow fast searching of packages."""
351
352         # we never want source packages
353         pfilter = 1 << packagekit.FilterEnum.NOT_SOURCE
354         #if only_newest:
355          #pfilter |= 1 << packagekit.FilterEnum.NOT_INSTALLED
356
357         if force or not self._pkgs_cache:
358             self_ready = False
359
360             self._pkgs_cache = {}
361             """ Use PackageKit cache to make loading the package cache faster.
362             To get fresh data, we need to run _update_package_cache later. """
363             fp = gio.File.new_for_path("/var/lib/PackageKit/system.package-list")
364             if fp.query_exists(None):
365                 pksack = packagekit.PackageSack()
366                 pksack.add_packages_from_file (fp)
367                 array = pksack.get_array()
368                 for pkg in array:
369                     self._add_package_to_cache(pkg)
370
371             if self._pkgs_cache:
372                 self._ready = True
373
374     class PkResolveHelper:
375         def __init__(self, steps):
376             self.steps = steps
377             self.cache = {}
378
379     def _update_package_cache(self):
380         """Update the cache with fresh data from PackageKit """
381
382         # we are not ready if the cache is invalid
383         if not self._pkgs_cache:
384             self._ready = False
385         # we never want source packages
386         pfilter = 1 << packagekit.FilterEnum.NOT_SOURCE
387
388         pathname = os.path.join(XAPIAN_BASE_PATH, "xapian")
389         db = StoreDatabase(pathname, self)
390         db.open()
391
392         docs = db.get_docs_from_query("")
393         wanted_pkgs = list()
394         for doc in docs:
395             wanted_pkgs.append(db.get_pkgname(doc))
396
397         if len(wanted_pkgs) is 0:
398             LOG.warning("no packages to process!")
399             return
400
401         # we never want source packages
402         pfilter = 1 << packagekit.FilterEnum.NOT_SOURCE
403
404         helper = self.PkResolveHelper(0)
405         #steps = len(wanted_pkgs) + 0.5 // 100
406         for i in xrange(0, len(wanted_pkgs), 100):
407             helper.steps = helper.steps + 1
408
409         # Start async update of package-cache, as the data which was
410         # loaded before (from on-disk cache) might not be fully up-to-date
411         for i in xrange(0, len(wanted_pkgs), 100):
412             res = self.pkclient.resolve_async(pfilter,
413                                               wanted_pkgs[i:i+100],
414                                               None, # cancellable
415                                               lambda prog, t, u: None, # progress callback
416                                               None, # progress user data,
417                                               self._on_packages_resolve_ready,
418                                               helper
419             )
420
421     def _on_packages_resolve_ready(self, source, result, helper):
422         LOG.debug("packages resolve done %s %s", source, result)
423         results = None
424         try:
425             results = self.pkclient.generic_finish(result)
426         except Exception as e:
427             LOG.error("Unable to fetch packages: %s", str(e))
428
429         if not results:
430             LOG.debug("unable to fetch results")
431             return;
432
433         # update package cache
434         sack = results.get_package_sack()
435         array = sack.get_array()
436         for pkg in array:
437             self._add_package_to_cache(pkg, helper.cache)
438         helper.steps = helper.steps - 1
439         LOG.debug("Steps count: %i" % helper.steps)
440
441         if helper.steps is 0:
442             self._pkgs_cache = helper.cache
443             LOG.debug("updated package-info cache")
444         else:
445             LOG.debug("processed package batch")
446
447     def _get_package_details(self, packageid, cache=USE_CACHE):
448         LOG.debug("package_details %s", packageid) #, self._cache.keys()
449         if cache and (packageid in self._cache_details.keys()):
450             return self._cache_details[packageid]
451
452         task = packagekit.Task()
453         try:
454             result = task.get_details_sync((packageid,), None, self._on_progress_changed, None)
455         except GObject.GError as e:
456             return None
457
458         pkgs = result.get_details_array()
459         if not pkgs:
460             LOG.debug("no details found for %s", packageid)
461             return None
462         packageid = pkgs[0].get_property('package-id')
463         self._cache_details[packageid] = pkgs[0]
464         LOG.debug("returning package details for %s", packageid)
465         return pkgs[0]
466
467     def _get_one_package(self, pkgname, pfilter=packagekit.FilterEnum.NONE, cache=USE_CACHE):
468         LOG.debug("package_one %s", pkgname) #, self._cache.keys()
469         ps = self._get_packages(pkgname, pfilter)
470         if not ps:
471             # also keep it in not found, to prevent further calls of resolve
472             if pkgname not in self._notfound_cache_pkg:
473                 LOG.debug("blacklisted %s", pkgname)
474                 self._notfound_cache_pkg.append(pkgname)
475             return None
476
477         return ps[0]
478
479     def _get_packages(self, pkgname, pfilter=packagekit.FilterEnum.NONE):
480         """ make sure we're ready and have a working cache """
481         if not self._ready:
482             return None
483
484         """ resolve a package name into a PkPackage object or return None """
485         LOG.debug("fetch packages for %s", pkgname) #, self._cache.keys()
486
487         if pfilter in (packagekit.FilterEnum.NONE, packagekit.FilterEnum.NOT_SOURCE):
488             cache_pkg_filter = self._cache_pkg_filter_none
489         elif pfilter in (packagekit.FilterEnum.NEWEST,):
490             cache_pkg_filter = self._cache_pkg_filter_newest
491         else:
492             cache_pkg_filter = None
493
494 #        if cache and cache_pkg_filter is not None and (pkgname in cache_pkg_filter.keys()):
495 #            return cache_pkg_filter[pkgname]
496
497         pfilter = 1 << pfilter
498         # we never want source packages
499         pfilter |= 1 << packagekit.FilterEnum.NOT_SOURCE
500
501         pkgs = []
502         if pkgname in self._pkgs_cache:
503             pkgs.append(self._pkgs_cache[pkgname])
504         if pkgs:
505             LOG.debug('Found package: %s' % pkgname)
506
507         #print "Package: %s", self._pkgs_cache[pkgname]
508
509         return pkgs
510
511     def _get_repolist(self, pfilter=packagekit.FilterEnum.NONE, cache=USE_CACHE):
512         """ obtain and cache a dictionary of repositories """
513         if self._repocache:
514             return self._repocache
515
516         pfilter = 1 << pfilter
517         result = self.pkclient.get_repo_list(pfilter,
518                                            None,
519                                            self._on_progress_changed, None)
520         repos = result.get_repo_detail_array()
521         for r in repos:
522             self._repocache[r.get_property('repo-id')] = r
523
524     def _reset_cache(self, name=None):
525         # Clean resolved packages cache
526         # This is used after finishing a transaction, so that we always
527         # have the latest package information
528         LOG.debug("[reset_cache] name: %s", name)
529         if name and (name in self._cache_pkg_filter_none.keys()):
530             del self._cache_pkg_filter_none[name]
531         if name and (name in self._cache_pkg_filter_newest.keys()):
532             del self._cache_pkg_filter_newest[name]
533         if name and (name in self._cache_details.keys()):
534             del self._cache_details[name]
535         else:
536             # delete all
537             self._cache_pkg_filter_none = {}
538             self._cache_pkg_filter_newest = {}
539             self._cache_details = {}
540         # appdetails gets refreshed:
541         self.emit('cache-ready')
542
543     def _on_progress_changed(self, progress, ptype, data=None):
544         pass
545
546 if __name__ == "__main__":
547    pi = PackagekitInfo()
548
549    print "Firefox, installed ", pi.is_installed('firefox')