~mvo/software-center/qml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
# 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 """
        GObject.GObject.__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')
        #self._trans.connect('notify::subpercentage', self._emit, 'progress-changed', 'subpercentage') # SC UI does not support subprogress
        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_string(role))

    def get_status_description(self, status=None):
        status = status if status is not None else self._trans.get_property('status')
        return packagekit.status_enum_to_string(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, pkgname, appname, iconname, addons_install=[],
                addons_remove=[], metadata=None):
        self.remove_multiple((pkgname,), (appname,), (iconname,),
                addons_install, addons_remove, metadata
        )

    def remove_multiple(self, pkgnames, appnames, iconnames,
                addons_install=[], addons_remove=[], metadatas=None):

        # keep track of pkg, app and icon for setting them as meta
        self.new_pkgname, self.new_appname, self.new_iconname = pkgnames[0], appnames[0], 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, pkgname, appname, iconname, filename=None,
                addons_install=[], addons_remove=[], metadata=None):
        if filename is not None:
            LOG.error("Filename not implemented") # FIXME
        else:
            self.install_multiple((pkgname,), (appname,), (iconname,),
                 addons_install, addons_remove, metadata
            )

    def install_multiple(self, pkgnames, appnames, iconnames,
        addons_install=[], addons_remove=[], metadatas=None):

        # keep track of pkg, app and icon for setting them as meta
        self.new_pkgname, self.new_appname, self.new_iconname = pkgnames[0], appnames[0], iconnames[0]

        # temporary hack
        pkgnames = self._fix_pkgnames(pkgnames)

        self.client.install_packages_async(False, # 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):
                version = self.pkginfo[p].candidate.version
                p = '{name};{version};{arch};{source}'.format(name=p,
                            version=version, arch='', source=''
                )
            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',))