Fetch transaction data from system bus (recognize if daemon crashed)
[appstream:software-center.git] / softwarecenter / backend / installbackend_impl / packagekitd.py
1 # Copyright (C) 2009-2010 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 import logging
21 import dbus
22 import dbus.mainloop.glib
23
24 from gi.repository import GObject
25 from gi.repository import PackageKitGlib as packagekit
26
27 from softwarecenter.enums import TransactionTypes
28 from softwarecenter.backend.transactionswatcher import (
29     BaseTransactionsWatcher,
30     BaseTransaction,
31     TransactionFinishedResult,
32     TransactionProgress
33 )
34 from softwarecenter.backend.installbackend import InstallBackend
35
36 # temporary, must think of better solution
37 from softwarecenter.db.pkginfo import get_pkg_info
38
39 LOG = logging.getLogger("softwarecenter.backend.packagekit")
40
41
42 class PackagekitTransaction(BaseTransaction):
43     _meta_data = {}
44
45     def __init__(self, trans):
46         """ trans -- a PkProgress object """
47         GObject.GObject.__init__(self)
48         self._trans = trans
49         self._setup_signals()
50
51     def _setup_signals(self):
52         """ Connect signals to the PkProgress from libpackagekitlib,
53         because PK DBus exposes only a generic Changed, without
54         specifying the property changed
55         """
56         self._trans.connect('notify::role', self._emit,
57             'role-changed', 'role')
58         self._trans.connect('notify::status', self._emit,
59             'status-changed', 'status')
60         self._trans.connect('notify::percentage', self._emit,
61             'progress-changed', 'percentage')
62         # TODO: Handle item-progress ??
63           #self._trans.connect('notify::item-progress', self._emit,
64           #    'progress-changed', 'item-progress')
65         self._trans.connect('notify::percentage', self._emit,
66             'progress-changed', 'percentage')
67         self._trans.connect('notify::allow-cancel', self._emit,
68             'cancellable-changed', 'allow-cancel')
69
70         # connect the delete (required to update information if e.g. the daemon crashes)
71         proxy = dbus.SystemBus().get_object('org.freedesktop.PackageKit',
72                                              self.tid)
73         trans = dbus.Interface(proxy, 'org.freedesktop.PackageKit.Transaction')
74         trans.connect_to_signal("Destroy", self._remove)
75
76     def _emit(self, *args):
77         prop, what = args[-1], args[-2]
78         self.emit(what, self._trans.get_property(prop))
79
80     @property
81     def tid(self):
82         return self._trans.get_property('transaction-id')
83
84     @property
85     def status_details(self):
86         return self.get_status_description()  # FIXME
87
88     @property
89     def meta_data(self):
90         return self._meta_data
91
92     @property
93     def cancellable(self):
94         return self._trans.get_property('allow-cancel')
95
96     @property
97     def progress(self):
98         return self._trans.get_property('percentage')
99
100     def get_role_description(self, role=None):
101         role = role if role is not None else self._trans.get_property('role')
102         return self.meta_data.get('sc_appname',
103             packagekit.role_enum_to_localised_present(role))
104
105     def get_status_description(self, status=packagekit.StatusEnum.UNKNOWN):
106         if status is packagekit.StatusEnum.UNKNOWN:
107             status = self._trans.get_property('status')
108
109         return packagekit.status_enum_to_string(status) # TODO: Localize!
110
111     def is_waiting(self):
112         """ return true if a time consuming task is taking place """
113         #LOG.debug('is_waiting ' + str(self._trans.get_property('status')))
114         status = self._trans.get_property('status')
115         return status == packagekit.StatusEnum.WAIT or \
116                status == packagekit.StatusEnum.LOADING_CACHE or \
117                status == packagekit.StatusEnum.SETUP
118
119     def is_downloading(self):
120         #LOG.debug('is_downloading ' + str(self._trans.get_property('status')))
121         status = self._trans.get_property('status')
122         return status == packagekit.StatusEnum.DOWNLOAD or \
123                (status >= packagekit.StatusEnum.DOWNLOAD_REPOSITORY and \
124                status <= packagekit.StatusEnum.DOWNLOAD_UPDATEINFO)
125
126     def cancel(self):
127         proxy = dbus.SystemBus().get_object('org.freedesktop.PackageKit',
128             self.tid)
129         trans = dbus.Interface(proxy, 'org.freedesktop.PackageKit.Transaction')
130         trans.Cancel()
131
132     def _remove(self):
133         """ delete transaction from _tlist """
134         # also notify pk install backend, so that this transaction gets removed
135         # from pending_transactions
136         self.emit('deleted')
137         if self.tid in PackagekitTransactionsWatcher._tlist.keys():
138             del PackagekitTransactionsWatcher._tlist[self.tid]
139             LOG.debug("Delete transaction %s" % self.tid)
140
141
142 class PackagekitTransactionsWatcher(BaseTransactionsWatcher):
143     _tlist = {}
144
145     def __init__(self):
146         super(PackagekitTransactionsWatcher, self).__init__()
147         self.pkclient = packagekit.Client()
148
149         bus = dbus.SystemBus()
150         proxy = bus.get_object('org.freedesktop.PackageKit',
151             '/org/freedesktop/PackageKit')
152         daemon = dbus.Interface(proxy, 'org.freedesktop.PackageKit')
153         daemon.connect_to_signal("TransactionListChanged",
154                                      self._on_transactions_changed)
155         queued = daemon.GetTransactionList()
156         self._on_transactions_changed(queued)
157
158     def _on_transactions_changed(self, queued):
159         if len(queued) > 0:
160             current = queued[0]
161             queued = queued[1:] if len(queued) > 1 else []
162         else:
163             current = None
164         self.emit("lowlevel-transactions-changed", current, queued)
165
166     def add_transaction(self, tid, trans):
167         """ return a tuple, (transaction, is_new) """
168         if tid not in PackagekitTransactionsWatcher._tlist.keys():
169             LOG.debug("Trying to setup %s" % tid)
170             if not trans:
171                 trans = self.pkclient.get_progress(tid, None)
172             trans = PackagekitTransaction(trans)
173             LOG.debug("Add return new transaction %s %s" % (tid, trans))
174             PackagekitTransactionsWatcher._tlist[tid] = trans
175             return (trans, True)
176         return (PackagekitTransactionsWatcher._tlist[tid], False)
177
178     def get_transaction(self, tid):
179         if tid not in PackagekitTransactionsWatcher._tlist.keys():
180             trans, new = self.add_transaction(tid, None)
181             return trans
182         return PackagekitTransactionsWatcher._tlist[tid]
183
184
185 class PackagekitBackend(GObject.GObject, InstallBackend):
186
187     __gsignals__ = {'transaction-started': (GObject.SIGNAL_RUN_FIRST,
188                                             GObject.TYPE_NONE,
189                                             (str, str, str, str)),
190                     # emits a TransactionFinished object
191                     'transaction-finished': (GObject.SIGNAL_RUN_FIRST,
192                                              GObject.TYPE_NONE,
193                                              (GObject.TYPE_PYOBJECT, )),
194                     'transaction-stopped': (GObject.SIGNAL_RUN_FIRST,
195                                             GObject.TYPE_NONE,
196                                             (GObject.TYPE_PYOBJECT,)),
197                     'transactions-changed': (GObject.SIGNAL_RUN_FIRST,
198                                              GObject.TYPE_NONE,
199                                              (GObject.TYPE_PYOBJECT, )),
200                     'transaction-progress-changed': (GObject.SIGNAL_RUN_FIRST,
201                                                      GObject.TYPE_NONE,
202                                                      (str, int,)),
203                     # the number/names of the available channels changed
204                     # FIXME: not emitted.
205                     'channels-changed': (GObject.SIGNAL_RUN_FIRST,
206                                          GObject.TYPE_NONE,
207                                          (bool,)),
208                     }
209
210     def __init__(self):
211         GObject.GObject.__init__(self)
212         InstallBackend.__init__(self)
213
214         # transaction details for setting as meta
215         self.new_pkgname, self.new_appname, self.new_iconname = '', '', ''
216
217         # this is public exposed
218         self.pending_transactions = {}
219
220         self.pkclient = packagekit.Client()
221         self.pkginfo = get_pkg_info()
222         self.pkginfo.open()
223
224         self._transactions_watcher = PackagekitTransactionsWatcher()
225         self._transactions_watcher.connect('lowlevel-transactions-changed',
226                                 self._on_lowlevel_transactions_changed)
227
228     def upgrade(self, pkgname, appname, iconname, addons_install=[],
229                 addons_remove=[], metadata=None):
230         pass  # FIXME implement it
231
232     def remove(self, app, iconname, addons_install=[],
233                 addons_remove=[], metadata=None):
234         self.remove_multiple((app,), (iconname,),
235                 addons_install, addons_remove, metadata
236         )
237
238     def remove_multiple(self, apps, iconnames,
239                 addons_install=[], addons_remove=[], metadatas=None):
240
241         pkgnames = [app.pkgname for app in apps]
242         appnames = [app.appname for app in apps]
243
244         # keep track of pkg, app and icon for setting them as meta
245         self.new_pkgname = pkgnames[0]
246         self.new_appname = appnames[0]
247         self.new_iconname = iconnames[0]
248
249         # temporary hack
250         pkgnames = self._fix_pkgnames(pkgnames)
251
252         task = packagekit.Task()
253         task.remove_packages_async(pkgnames,
254                     False,  # allow deps
255                     True,  # autoremove
256                     None,  # cancellable
257                     self._on_progress_changed,
258                     None,  # progress data
259                     self._on_remove_ready,  # callback ready
260                     task  # callback data
261         )
262         self.emit("transaction-started", pkgnames[0], appnames[0], 0,
263             TransactionTypes.REMOVE)
264
265     def install(self, app, iconname, filename=None,
266                 addons_install=[], addons_remove=[], metadata=None):
267         if filename is not None:
268             LOG.error("Filename not implemented")  # FIXME
269         else:
270             self.install_multiple((app,), (iconname,),
271                  addons_install, addons_remove, metadata
272             )
273
274     def install_multiple(self, apps, iconnames,
275         addons_install=[], addons_remove=[], metadatas=None):
276
277         pkgnames = [app.pkgname for app in apps]
278         appnames = [app.appname for app in apps]
279
280         # keep track of pkg, app and icon for setting them as meta
281         self.new_pkgname = pkgnames[0]
282         self.new_appname = appnames[0]
283         self.new_iconname = iconnames[0]
284
285         # temporary hack
286         pkgnames = self._fix_pkgnames(pkgnames)
287
288         LOG.debug("Installing multiple packages: " + str(pkgnames))
289
290         task = packagekit.Task()
291         task.install_packages_async(pkgnames,
292                     None,  # cancellable
293                     self._on_progress_changed,
294                     None,  # progress data
295                     self._on_install_ready,  # GAsyncReadyCallback
296                     task  # ready data
297         )
298         self.emit("transaction-started", pkgnames[0], appnames[0], 0,
299             TransactionTypes.INSTALL)
300
301     def apply_changes(self, pkgname, appname, iconname,
302         addons_install=[], addons_remove=[], metadata=None):
303         pass
304
305     def reload(self, sources_list=None, metadata=None):
306         """ reload package list """
307         pass
308
309     def _on_transaction_deleted(self, trans):
310         name = trans.meta_data.get('sc_pkgname', '')
311         if name in self.pending_transactions:
312             del self.pending_transactions[name]
313             LOG.debug("Deleted transaction " + name)
314         else:
315             LOG.error("Could not delete: " + name + str(trans))
316         # this is needed too
317         self.emit('transactions-changed', self.pending_transactions)
318         # also hack PackagekitInfo cache so that it emits a cache-ready signal
319         if hasattr(self.pkginfo, '_reset_cache'):
320             self.pkginfo._reset_cache(name)
321
322     def _on_progress_changed(self, progress, ptype, data=None):
323         """ de facto callback on transaction's progress change """
324         tid = progress.get_property('transaction-id')
325         status = progress.get_property('status')
326         if not tid:
327             LOG.debug("Progress without transaction")
328             return
329
330         trans, new = self._transactions_watcher.add_transaction(tid, progress)
331         if new:
332             trans.connect('deleted', self._on_transaction_deleted)
333             LOG.debug("new transaction" + str(trans))
334             # should add it to pending_transactions, but
335             # i cannot get the pkgname here
336             trans.meta_data['sc_appname'] = self.new_appname
337             trans.meta_data['sc_pkgname'] = self.new_pkgname
338             trans.meta_data['sc_iconname'] = self.new_iconname
339             if self.new_pkgname not in self.pending_transactions:
340                 self.pending_transactions[self.new_pkgname] = trans
341
342         # LOG.debug("Progress update %s %s %s %s" %
343         #     (status, ptype, progress.get_property('transaction-id'),
344         #     progress.get_property('status')))
345
346         if status == packagekit.StatusEnum.FINISHED:
347             LOG.debug("Transaction finished %s" % tid)
348             self.emit("transaction-finished",
349                 TransactionFinishedResult(trans, True))
350
351         if status == packagekit.StatusEnum.CANCEL:
352             LOG.debug("Transaction canceled %s" % tid)
353             self.emit("transaction-stopped",
354                 TransactionFinishedResult(trans, True))
355
356         if ptype == packagekit.ProgressType.PACKAGE:
357             # this should be done better
358             # mvo: why getting package here at all?
359             #package = progress.get_property('package')
360             # fool sc ui about the name change
361             trans.emit('role-changed', packagekit.RoleEnum.LAST)
362
363         if ptype == packagekit.ProgressType.PERCENTAGE:
364             pkgname = trans.meta_data.get('sc_pkgname', '')
365             prog = progress.get_property('percentage')
366             if prog >= 0:
367                 self.emit("transaction-progress-changed", pkgname, prog)
368             else:
369                 self.emit("transaction-progress-changed", pkgname, 0)
370
371     def _on_lowlevel_transactions_changed(self, watcher, current, pending):
372         # update self.pending_transactions
373         self.pending_transactions.clear()
374
375         for tid in [current] + pending:
376             if not tid:
377                 continue
378             trans = self._transactions_watcher.get_transaction(tid)
379             trans_progress = TransactionProgress(trans)
380             try:
381                 self.pending_transactions[
382                     trans_progress.pkgname] = trans_progress
383             except:
384                 self.pending_transactions[trans.tid] = trans_progress
385
386         self.emit('transactions-changed', self.pending_transactions)
387
388     def _on_install_ready(self, source, result, task):
389         LOG.debug("install done %s %s", source, result)
390         results = task.generic_finish(result)
391         if not results:
392             LOG.debug("unable to fetch results")
393             return;
394         # update package cache
395         sack = results.get_package_sack()
396         array = sack.get_array()
397         if len(array) == 0:
398                 LOG.error("unable to get package results")
399                 return;
400         pkg = array[0]
401         infoCache = get_pkg_info()
402         infoCache.update_installed_status(pkg.get_name(), True)
403         LOG.debug("updated package-info cache for %s" % pkg.get_name())
404
405     def _on_remove_ready(self, source, result, task):
406         LOG.debug("remove done %s %s", source, result)
407
408     def _fix_pkgnames(self, pkgnames):
409         is_pk_id = lambda a: ';' in a
410         res = []
411         for p in pkgnames:
412             if not is_pk_id(p):
413                 candidate = self.pkginfo[p].candidate
414                 p = candidate.package.get_id()
415             res.append(p)
416         return res
417
418 if __name__ == "__main__":
419     package = 'firefox'
420
421     loop = dbus.mainloop.glib.DBusGMainLoop()
422     dbus.set_default_main_loop(loop)
423
424     backend = PackagekitBackend()
425     pkginfo = get_pkg_info()
426     if pkginfo[package].is_installed:
427         backend.remove(package, package, '')
428         backend.install(package, package, '')
429     else:
430         backend.install(package, package, '')
431         backend.remove(package, package, '')
432     from gi.repository import Gtk
433     Gtk.main()
434     #print backend._fix_pkgnames(('cheese',))