# Copyright (C) 2009-2010 Canonical # # Authors: # Alex Eftimie # # This program is free software; you can redistribute it and/or modify it under # the terms of the GNU General Public License as published by the Free Software # Foundation; version 3. # # This program is distributed in the hope that it will be useful, but WITHOUT # ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS # FOR A PARTICULAR PURPOSE. See the GNU General Public License for more # details. # # You should have received a copy of the GNU General Public License along with # this program; if not, write to the Free Software Foundation, Inc., # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA import logging import dbus import dbus.mainloop.glib from gi.repository import GObject from gi.repository import PackageKitGlib as packagekit from softwarecenter.enums import TransactionTypes from softwarecenter.backend.transactionswatcher import ( BaseTransactionsWatcher, BaseTransaction, TransactionFinishedResult, TransactionProgress ) from softwarecenter.backend.installbackend import InstallBackend # temporary, must think of better solution from softwarecenter.db.pkginfo import get_pkg_info LOG = logging.getLogger("softwarecenter.backend.packagekit") class PackagekitTransaction(BaseTransaction): _meta_data = {} def __init__(self, trans): """ trans -- a PkProgress object """ BaseTransaction.__init__(self) self._trans = trans self._setup_signals() def _setup_signals(self): """ Connect signals to the PkProgress from libpackagekitlib, because PK DBus exposes only a generic Changed, without specifying the property changed """ self._trans.connect('notify::role', self._emit, 'role-changed', 'role') self._trans.connect('notify::status', self._emit, 'status-changed', 'status') self._trans.connect('notify::percentage', self._emit, 'progress-changed', 'percentage') # SC UI does not support subprogress: #self._trans.connect('notify::subpercentage', self._emit, # 'progress-changed', 'subpercentage') self._trans.connect('notify::percentage', self._emit, 'progress-changed', 'percentage') self._trans.connect('notify::allow-cancel', self._emit, 'cancellable-changed', 'allow-cancel') # connect the delete: proxy = dbus.SystemBus().get_object('org.freedesktop.PackageKit', self.tid) trans = dbus.Interface(proxy, 'org.freedesktop.PackageKit.Transaction') trans.connect_to_signal("Destroy", self._remove) def _emit(self, *args): prop, what = args[-1], args[-2] self.emit(what, self._trans.get_property(prop)) @property def tid(self): return self._trans.get_property('transaction-id') @property def status_details(self): return self.get_status_description() # FIXME @property def meta_data(self): return self._meta_data @property def cancellable(self): return self._trans.get_property('allow-cancel') @property def progress(self): return self._trans.get_property('percentage') def get_role_description(self, role=None): role = role if role is not None else self._trans.get_property('role') return self.meta_data.get('sc_appname', packagekit.role_enum_to_localised_present(role)) def get_status_description(self, status=None): if status is None: status = self._trans.get_property('status') return packagekit.info_enum_to_localised_present(status) def is_waiting(self): """ return true if a time consuming task is taking place """ #LOG.debug('is_waiting ' + str(self._trans.get_property('status'))) status = self._trans.get_property('status') return status == packagekit.StatusEnum.WAIT or \ status == packagekit.StatusEnum.LOADING_CACHE or \ status == packagekit.StatusEnum.SETUP def is_downloading(self): #LOG.debug('is_downloading ' + str(self._trans.get_property('status'))) status = self._trans.get_property('status') return status == packagekit.StatusEnum.DOWNLOAD or \ (status >= packagekit.StatusEnum.DOWNLOAD_REPOSITORY and status <= packagekit.StatusEnum.DOWNLOAD_UPDATEINFO) def cancel(self): proxy = dbus.SystemBus().get_object('org.freedesktop.PackageKit', self.tid) trans = dbus.Interface(proxy, 'org.freedesktop.PackageKit.Transaction') trans.Cancel() def _remove(self): """ delete transaction from _tlist """ # also notify pk install backend, so that this transaction gets removed # from pending_transactions self.emit('deleted') if self.tid in PackagekitTransactionsWatcher._tlist.keys(): del PackagekitTransactionsWatcher._tlist[self.tid] LOG.debug("Delete transaction %s" % self.tid) class PackagekitTransactionsWatcher(BaseTransactionsWatcher): _tlist = {} def __init__(self): super(PackagekitTransactionsWatcher, self).__init__() self.client = packagekit.Client() bus = dbus.SystemBus() proxy = bus.get_object('org.freedesktop.PackageKit', '/org/freedesktop/PackageKit') daemon = dbus.Interface(proxy, 'org.freedesktop.PackageKit') daemon.connect_to_signal("TransactionListChanged", self._on_transactions_changed) queued = daemon.GetTransactionList() self._on_transactions_changed(queued) def _on_transactions_changed(self, queued): if len(queued) > 0: current = queued[0] queued = queued[1:] if len(queued) > 1 else [] else: current = None self.emit("lowlevel-transactions-changed", current, queued) def add_transaction(self, tid, trans): """ return a tuple, (transaction, is_new) """ if tid not in PackagekitTransactionsWatcher._tlist.keys(): LOG.debug("Trying to setup %s" % tid) if not trans: trans = self.client.get_progress(tid, None) trans = PackagekitTransaction(trans) LOG.debug("Add return new transaction %s %s" % (tid, trans)) PackagekitTransactionsWatcher._tlist[tid] = trans return (trans, True) return (PackagekitTransactionsWatcher._tlist[tid], False) def get_transaction(self, tid): if tid not in PackagekitTransactionsWatcher._tlist.keys(): trans, new = self.add_transaction(tid, None) return trans return PackagekitTransactionsWatcher._tlist[tid] class PackagekitBackend(GObject.GObject, InstallBackend): __gsignals__ = {'transaction-started': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (str, str, str, str)), # emits a TransactionFinished object 'transaction-finished': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, )), 'transaction-stopped': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT,)), 'transactions-changed': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (GObject.TYPE_PYOBJECT, )), 'transaction-progress-changed': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (str, int,)), # the number/names of the available channels changed # FIXME: not emitted. 'channels-changed': (GObject.SIGNAL_RUN_FIRST, GObject.TYPE_NONE, (bool,)), } def __init__(self): GObject.GObject.__init__(self) InstallBackend.__init__(self) # transaction details for setting as meta self.new_pkgname, self.new_appname, self.new_iconname = '', '', '' # this is public exposed self.pending_transactions = {} self.client = packagekit.Client() self.pkginfo = get_pkg_info() self.pkginfo.open() self._transactions_watcher = PackagekitTransactionsWatcher() self._transactions_watcher.connect('lowlevel-transactions-changed', self._on_lowlevel_transactions_changed) def upgrade(self, pkgname, appname, iconname, addons_install=[], addons_remove=[], metadata=None): pass # FIXME implement it def remove(self, app, iconname, addons_install=[], addons_remove=[], metadata=None): self.remove_multiple( (app,), (iconname,), addons_install, addons_remove, metadata) def remove_multiple(self, apps, iconnames, addons_install=[], addons_remove=[], metadatas=None): pkgnames = [app.pkgname for app in apps] appnames = [app.appname for app in apps] # keep track of pkg, app and icon for setting them as meta self.new_pkgname = pkgnames[0] self.new_appname = appnames[0] self.new_iconname = iconnames[0] # temporary hack pkgnames = self._fix_pkgnames(pkgnames) self.client.remove_packages_async( pkgnames, False, # allow deps False, # autoremove None, # cancellable self._on_progress_changed, None, # progress data self._on_remove_ready, # callback ready None # callback data ) self.emit("transaction-started", pkgnames[0], appnames[0], 0, TransactionTypes.REMOVE) def install(self, app, iconname, filename=None, addons_install=[], addons_remove=[], metadata=None): if filename is not None: LOG.error("Filename not implemented") # FIXME else: self.install_multiple( (app,), (iconname,), addons_install, addons_remove, metadata) def install_multiple(self, apps, iconnames, addons_install=[], addons_remove=[], metadatas=None): pkgnames = [app.pkgname for app in apps] appnames = [app.appname for app in apps] # keep track of pkg, app and icon for setting them as meta self.new_pkgname = pkgnames[0] self.new_appname = appnames[0] self.new_iconname = iconnames[0] # temporary hack pkgnames = self._fix_pkgnames(pkgnames) LOG.debug("Installing multiple packages: " + str(pkgnames)) # FIXME we set the only_trusted flag, which will prevent # PackageKit from installing untrusted packages # (in general, all enabled repos should have GPG signatures, # which is enough for being marked "trusted", but still) self.client.install_packages_async( True, # only trusted pkgnames, None, # cancellable self._on_progress_changed, None, # progress data self._on_install_ready, # GAsyncReadyCallback None # ready data ) self.emit("transaction-started", pkgnames[0], appnames[0], 0, TransactionTypes.INSTALL) def apply_changes(self, pkgname, appname, iconname, addons_install=[], addons_remove=[], metadata=None): pass def reload(self, sources_list=None, metadata=None): """ reload package list """ pass def _on_transaction_deleted(self, trans): name = trans.meta_data.get('sc_pkgname', '') if name in self.pending_transactions: del self.pending_transactions[name] LOG.debug("Deleted transaction " + name) else: LOG.error("Could not delete: " + name + str(trans)) # this is needed too self.emit('transactions-changed', self.pending_transactions) # also hack PackagekitInfo cache so that it emits a cache-ready signal if hasattr(self.pkginfo, '_reset_cache'): self.pkginfo._reset_cache(name) def _on_progress_changed(self, progress, ptype, data=None): """ de facto callback on transaction's progress change """ tid = progress.get_property('transaction-id') status = progress.get_property('status') if not tid: LOG.debug("Progress without transaction") return trans, new = self._transactions_watcher.add_transaction(tid, progress) if new: trans.connect('deleted', self._on_transaction_deleted) LOG.debug("new transaction" + str(trans)) # should add it to pending_transactions, but # i cannot get the pkgname here trans.meta_data['sc_appname'] = self.new_appname trans.meta_data['sc_pkgname'] = self.new_pkgname trans.meta_data['sc_iconname'] = self.new_iconname if self.new_pkgname not in self.pending_transactions: self.pending_transactions[self.new_pkgname] = trans # LOG.debug("Progress update %s %s %s %s" % # (status, ptype, progress.get_property('transaction-id'), # progress.get_property('status'))) if status == packagekit.StatusEnum.FINISHED: LOG.debug("Transaction finished %s" % tid) self.emit("transaction-finished", TransactionFinishedResult(trans, True)) if status == packagekit.StatusEnum.CANCEL: LOG.debug("Transaction canceled %s" % tid) self.emit("transaction-stopped", TransactionFinishedResult(trans, True)) if ptype == packagekit.ProgressType.PACKAGE: # this should be done better # mvo: why getting package here at all? #package = progress.get_property('package') # fool sc UI about the name change trans.emit('role-changed', packagekit.RoleEnum.LAST) if ptype == packagekit.ProgressType.PERCENTAGE: pkgname = trans.meta_data.get('sc_pkgname', '') prog = progress.get_property('percentage') if prog >= 0: self.emit("transaction-progress-changed", pkgname, prog) else: self.emit("transaction-progress-changed", pkgname, 0) def _on_lowlevel_transactions_changed(self, watcher, current, pending): # update self.pending_transactions self.pending_transactions.clear() for tid in [current] + pending: if not tid: continue trans = self._transactions_watcher.get_transaction(tid) trans_progress = TransactionProgress(trans) try: self.pending_transactions[ trans_progress.pkgname] = trans_progress except: self.pending_transactions[trans.tid] = trans_progress self.emit('transactions-changed', self.pending_transactions) def _on_install_ready(self, source, result, data=None): LOG.debug("install done %s %s", source, result) def _on_remove_ready(self, source, result, data=None): LOG.debug("remove done %s %s", source, result) def _fix_pkgnames(self, pkgnames): is_pk_id = lambda a: ';' in a res = [] for p in pkgnames: if not is_pk_id(p): candidate = self.pkginfo[p].candidate p = candidate.package.get_id() res.append(p) return res if __name__ == "__main__": package = 'firefox' loop = dbus.mainloop.glib.DBusGMainLoop() dbus.set_default_main_loop(loop) backend = PackagekitBackend() pkginfo = get_pkg_info() if pkginfo[package].is_installed: backend.remove(package, package, '') backend.install(package, package, '') else: backend.install(package, package, '') backend.remove(package, package, '') from gi.repository import Gtk Gtk.main() #print backend._fix_pkgnames(('cheese',))