source: software-center/trunk/fuentes/softwarecenter/backend/installbackend_impl/aptd.py @ 408

Last change on this file since 408 was 408, checked in by mabarracus, 5 years ago

Added lliurex patch

File size: 46.1 KB
Line 
1# Copyright (C) 2009-2010 Canonical
2#
3# Authors:
4#  Michael Vogt
5#
6# This program is free software; you can redistribute it and/or modify it under
7# the terms of the GNU General Public License as published by the Free Software
8# Foundation; version 3.
9#
10# This program is distributed in the hope that it will be useful, but WITHOUT
11# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
12# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
13# details.
14#
15# You should have received a copy of the GNU General Public License along with
16# this program; if not, write to the Free Software Foundation, Inc.,
17# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
18
19import apt_pkg
20import dbus
21import logging
22import os
23import re
24import traceback
25
26from subprocess import (
27    Popen,
28    PIPE,
29)
30
31from gi.repository import GObject, GLib
32
33from softwarecenter.utils import (
34    sources_filename_from_ppa_entry,
35    release_filename_in_lists_from_deb_line,
36    obfuscate_private_ppa_details,
37    utf8,
38)
39from softwarecenter.enums import (
40    TransactionTypes,
41    PURCHASE_TRANSACTION_ID,
42)
43from softwarecenter.paths import APPORT_RECOVERABLE_ERROR
44
45from aptdaemon import client
46from aptdaemon import enums
47from aptdaemon import errors
48from aptsources.sourceslist import SourceEntry
49from aptdaemon import policykit1
50
51from defer import inline_callbacks, return_value
52
53from softwarecenter.db.application import Application
54from softwarecenter.backend.transactionswatcher import (
55    BaseTransactionsWatcher,
56    BaseTransaction,
57    TransactionFinishedResult,
58    TransactionProgress,
59)
60from softwarecenter.backend.installbackend import InstallBackend
61
62from gettext import gettext as _
63
64# its important that we only have a single dbus BusConnection
65# per address when using the fake dbus aptd
66buses = {}
67
68
69def get_dbus_bus():
70    if "SOFTWARE_CENTER_APTD_FAKE" in os.environ:
71        global buses
72        dbus_address = os.environ["SOFTWARE_CENTER_APTD_FAKE"]
73        if dbus_address in buses:
74            return buses[dbus_address]
75        bus = buses[dbus_address] = dbus.bus.BusConnection(dbus_address)
76    else:
77        bus = dbus.SystemBus()
78    return bus
79
80
81class FakePurchaseTransaction(object):
82
83    def __init__(self, app, iconname):
84        self.pkgname = app.pkgname
85        self.appname = app.appname
86        self.iconname = iconname
87        self.progress = 0
88        self.tid = PURCHASE_TRANSACTION_ID
89
90
91class AptdaemonTransaction(BaseTransaction):
92
93    def __init__(self, trans):
94        self._trans = trans
95
96    @property
97    def tid(self):
98        return self._trans.tid
99
100    @property
101    def status_details(self):
102        return self._trans.status_details
103
104    @property
105    def meta_data(self):
106        return self._trans.meta_data
107
108    @property
109    def cancellable(self):
110        return self._trans.cancellable
111
112    @property
113    def progress(self):
114        return self._trans.progress
115
116    def get_role_description(self, role=None):
117        role = self._trans.role if role is None else role
118        return enums.get_role_localised_present_from_enum(role)
119
120    def get_status_description(self, status=None):
121        status = self._trans.status if status is None else status
122        return enums.get_status_string_from_enum(status)
123
124    def is_waiting(self):
125        return self._trans.status == enums.STATUS_WAITING_LOCK
126
127    def is_downloading(self):
128        return self._trans.status == enums.STATUS_DOWNLOADING
129
130    def cancel(self):
131        return self._trans.cancel()
132
133    def connect(self, signal, handler, *args):
134        """ append the real handler to the arguments """
135        args = args + (handler, )
136        return self._trans.connect(signal, self._handler, *args)
137
138    def _handler(self, trans, *args):
139        """ translate trans to BaseTransaction type.
140        call the real handler after that
141        """
142        real_handler = args[-1]
143        args = tuple(args[:-1])
144        if isinstance(trans, client.AptTransaction):
145            trans = AptdaemonTransaction(trans)
146        return real_handler(trans, *args)
147
148
149class AptdaemonTransactionsWatcher(BaseTransactionsWatcher):
150    """
151    base class for objects that need to watch the aptdaemon
152    for transaction changes. it registers a handler for the daemon
153    going away and reconnects when it appears again
154    """
155
156    def __init__(self):
157        super(AptdaemonTransactionsWatcher, self).__init__()
158        # watch the daemon exit and (re)register the signal
159        bus = get_dbus_bus()
160        self._owner_watcher = bus.watch_name_owner(
161            "org.debian.apt", self._register_active_transactions_watch)
162
163    def _register_active_transactions_watch(self, connection):
164        #print "_register_active_transactions_watch", connection
165        bus = get_dbus_bus()
166        apt_daemon = client.get_aptdaemon(bus=bus)
167        apt_daemon.connect_to_signal("ActiveTransactionsChanged",
168                                     self._on_transactions_changed)
169        current, queued = apt_daemon.GetActiveTransactions()
170        self._on_transactions_changed(current, queued)
171
172    def _on_transactions_changed(self, current, queued):
173        self.emit("lowlevel-transactions-changed", current, queued)
174
175    def get_transaction(self, tid):
176        """ synchronously return a transaction """
177        try:
178            trans = client.get_transaction(tid)
179            return AptdaemonTransaction(trans)
180        except dbus.DBusException:
181            pass
182
183
184class AptdaemonBackend(GObject.GObject, InstallBackend):
185    """ software center specific code that interacts with aptdaemon """
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                    # emits a TransactionFinished object
195                    'transaction-stopped': (GObject.SIGNAL_RUN_FIRST,
196                                            GObject.TYPE_NONE,
197                                            (GObject.TYPE_PYOBJECT,)),
198                    # emits a TransactionFinished object
199                    'transaction-cancelled': (GObject.SIGNAL_RUN_FIRST,
200                                              GObject.TYPE_NONE,
201                                              (GObject.TYPE_PYOBJECT,)),
202                    # emits with a pending_transactions list object
203                    'transactions-changed': (GObject.SIGNAL_RUN_FIRST,
204                                             GObject.TYPE_NONE,
205                                             (GObject.TYPE_PYOBJECT, )),
206                    # emits pkgname, percent
207                    'transaction-progress-changed': (GObject.SIGNAL_RUN_FIRST,
208                                                     GObject.TYPE_NONE,
209                                                     (str, int,)),
210                    # the number/names of the available channels changed
211                    'channels-changed': (GObject.SIGNAL_RUN_FIRST,
212                                         GObject.TYPE_NONE,
213                                         (bool,)),
214                    # cache reload emits this specific signal as well
215                    'reload-finished': (GObject.SIGNAL_RUN_FIRST,
216                                        GObject.TYPE_NONE,
217                                        (GObject.TYPE_PYOBJECT, bool,)),
218                    }
219
220    LICENSE_KEY_SERVER = "ubuntu-production"
221
222    def __init__(self):
223        GObject.GObject.__init__(self)
224        InstallBackend.__init__(self)
225
226        bus = get_dbus_bus()
227        self.aptd_client = client.AptClient(bus=bus)
228        self.pending_transactions = {}
229        self._transactions_watcher = AptdaemonTransactionsWatcher()
230        self._transactions_watcher.connect("lowlevel-transactions-changed",
231            self._on_lowlevel_transactions_changed)
232        # dict of pkgname -> FakePurchaseTransaction
233        self.pending_purchases = {}
234        self._progress_signal = None
235        self._logger = logging.getLogger("softwarecenter.backend")
236        # the AptdaemonBackendUI code
237        self.ui = None
238
239    def _axi_finished(self, res):
240        self.emit("channels-changed", res)
241
242    # public methods
243    def update_xapian_index(self):
244        self._logger.debug("update_xapian_index")
245        system_bus = get_dbus_bus()
246        # axi is optional, so just do nothing if its not installed
247        try:
248            axi = dbus.Interface(
249                system_bus.get_object("org.debian.AptXapianIndex", "/"),
250                "org.debian.AptXapianIndex")
251        except dbus.DBusException as e:
252            self._logger.warning("axi can not be updated '%s'" % e)
253            return
254        axi.connect_to_signal("UpdateFinished", self._axi_finished)
255        # we don't really care for updates at this point
256        #axi.connect_to_signal("UpdateProgress", progress)
257        try:
258            # first arg is force, second update_only
259            axi.update_async(True, False)
260        except:
261            self._logger.warning("could not update axi")
262
263    @inline_callbacks
264    def fix_broken_depends(self):
265        trans = None
266        try:
267            trans = yield self.aptd_client.fix_broken_depends(defer=True)
268            self.emit("transaction-started", "", "", trans.tid,
269                TransactionTypes.REPAIR)
270            yield self._run_transaction(trans, None, None, None)
271        except Exception as error:
272            self._on_trans_error(error, trans)
273
274    @inline_callbacks
275    def fix_incomplete_install(self):
276        trans = None
277        try:
278            trans = yield self.aptd_client.fix_incomplete_install(defer=True)
279            self.emit("transaction-started", "", "", trans.tid,
280                      TransactionTypes.REPAIR)
281            yield self._run_transaction(trans, None, None, None)
282        except Exception as error:
283            self._on_trans_error(error, trans)
284
285    # FIXME: upgrade add-ons here
286    @inline_callbacks
287    def upgrade(self, app, iconname, addons_install=[], addons_remove=[],
288                metadata=None):
289        """ upgrade a single package """
290        pkgname = app.pkgname
291        appname = app.appname
292        trans = None
293        try:
294            trans = yield self.aptd_client.upgrade_packages([pkgname],
295                                                            defer=True)
296            self.emit("transaction-started", pkgname, appname, trans.tid,
297                TransactionTypes.UPGRADE)
298            yield self._run_transaction(trans, pkgname, appname, iconname,
299                metadata)
300        except Exception as error:
301            self._on_trans_error(error, trans, pkgname)
302
303# broken
304#    @inline_callbacks
305#    def _simulate_remove_multiple(self, pkgnames):
306#        try:
307#            trans = yield self.aptd_client.remove_packages(pkgnames,
308#                                                           defer=True)
309#            trans.connect("dependencies-changed",
310#                self._on_dependencies_changed)
311#        except Exception:
312#            logging.exception("simulate_remove")
313#        return_value(trans)
314#
315#   def _on_dependencies_changed(self, *args):
316#        print "_on_dependencies_changed", args
317#        self.have_dependencies = True
318#
319#    @inline_callbacks
320#    def simulate_remove_multiple(self, pkgnames):
321#        self.have_dependencies = False
322#        trans = yield self._simulate_remove_multiple(pkgnames)
323#        print trans
324#        while not self.have_dependencies:
325#            while gtk.events_pending():
326#                gtk.main_iteration()
327#            time.sleep(0.01)
328
329    @inline_callbacks
330    def remove(self, app, iconname, addons_install=[], addons_remove=[],
331               metadata=None):
332        """ remove a single package """
333        pkgname = app.pkgname
334        appname = app.appname
335        trans = None
336        try:
337            trans = yield self.aptd_client.remove_packages([pkgname],
338                                                           defer=True)
339            self.emit("transaction-started", pkgname, appname, trans.tid,
340                TransactionTypes.REMOVE)
341            yield self._run_transaction(trans, pkgname, appname, iconname,
342                metadata)
343        except Exception as error:
344            self._on_trans_error(error, trans, pkgname)
345
346    @inline_callbacks
347    def remove_multiple(self, apps, iconnames, addons_install=[],
348                        addons_remove=[], metadatas=None):
349        """ queue a list of packages for removal  """
350        if metadatas is None:
351            metadatas = []
352            for item in apps:
353                metadatas.append(None)
354        for app, iconname, metadata in zip(apps, iconnames, metadatas):
355            yield self.remove(app, iconname, metadata)
356
357    @inline_callbacks
358    def install(self, app, iconname, filename=None, addons_install=[],
359                addons_remove=[], metadata=None, force=False):
360        """Install a single package from the archive
361           If filename is given a local deb package is installed instead.
362        """
363        trans = None
364        pkgname = app.pkgname
365        appname = app.appname
366        # this will force aptdaemon to use the right archive suite on install
367        if app.archive_suite:
368            pkgname = "%s/%s" % (pkgname, app.archive_suite)
369        try:
370            if filename:
371                # force means on lintian failure
372                trans = yield self.aptd_client.install_file(
373                    filename, force=force, defer=True)
374                self.emit("transaction-started", pkgname, appname, trans.tid,
375                    TransactionTypes.INSTALL)
376                yield trans.set_meta_data(sc_filename=filename, defer=True)
377            else:
378                install = [pkgname] + addons_install
379                remove = addons_remove
380                reinstall = remove = purge = upgrade = downgrade = []
381                trans = yield self.aptd_client.commit_packages(
382                    install, reinstall, remove, purge, upgrade, downgrade,
383                    defer=True)
384                trans.set_allow_unauthenticated(True)
385                self.emit("transaction-started", pkgname, appname, trans.tid,
386                    TransactionTypes.INSTALL)
387            yield self._run_transaction(
388                trans, pkgname, appname, iconname, metadata)
389        except Exception as error:
390            self._on_trans_error(error, trans, pkgname)
391
392    @inline_callbacks
393    def install_multiple(self, apps, iconnames, addons_install=[],
394                         addons_remove=[], metadatas=None):
395        """ queue a list of packages for install  """
396        if metadatas is None:
397            metadatas = []
398            for item in apps:
399                metadatas.append(None)
400        for app, iconname, metadata in zip(apps, iconnames, metadatas):
401            yield self.install(app, iconname, metadata=metadata)
402
403    @inline_callbacks
404    def apply_changes(self, app, iconname, addons_install=[],
405                      addons_remove=[], metadata=None):
406        """ install and remove add-ons """
407        pkgname = app.pkgname
408        appname = app.appname
409        trans = None
410        try:
411            install = addons_install
412            remove = addons_remove
413            reinstall = remove = purge = upgrade = downgrade = []
414            trans = yield self.aptd_client.commit_packages(
415                install, reinstall, remove, purge, upgrade, downgrade,
416                defer=True)
417            self.emit("transaction-started", pkgname, appname, trans.tid,
418                TransactionTypes.APPLY)
419            yield self._run_transaction(trans, pkgname, appname, iconname)
420        except Exception as error:
421            self._on_trans_error(error, trans)
422
423    @inline_callbacks
424    def reload(self, sources_list=None, metadata=None):
425        """ reload package list """
426        # check if the sourcespart is there, if not, do a full reload
427        # this can happen when the "partner" repository is added, it
428        # will be in the main sources.list already and this means that
429        # aptsources will just enable it instead of adding a extra
430        # sources.list.d file (LP: #666956)
431        d = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
432        trans = None
433        if (not sources_list or
434                not os.path.exists(os.path.join(d, sources_list))):
435            sources_list = ""
436        try:
437            trans = yield self.aptd_client.update_cache(
438                sources_list=sources_list, defer=True)
439            yield self._run_transaction(trans, None, None, None, metadata)
440        except Exception as error:
441            self._on_trans_error(error, trans)
442        # note that the cache re-open will happen via the connected
443        # "transaction-finished" signal
444
445    @inline_callbacks
446    def enable_component(self, component):
447        self._logger.debug("enable_component: %s" % component)
448        trans = None
449        try:
450            trans = yield self.aptd_client.enable_distro_component(component)
451            # don't use self._run_transaction() here, to avoid sending unneeded
452            # signals
453            yield trans.run(defer=True)
454        except Exception as error:
455            self._on_trans_error(error, trans, component)
456            return_value(None)
457        # now update the cache
458        yield self.reload()
459
460    @inline_callbacks
461    def enable_channel(self, channelfile):
462        trans = None
463        # read channel file and add all relevant lines
464        for line in open(channelfile):
465            line = line.strip()
466            if not line:
467                continue
468            entry = SourceEntry(line)
469            if entry.invalid:
470                continue
471            sourcepart = os.path.basename(channelfile)
472            yield self.add_sources_list_entry(entry, sourcepart)
473            keyfile = channelfile.replace(".list", ".key")
474            if os.path.exists(keyfile):
475                trans = yield self.aptd_client.add_vendor_key_from_file(
476                    keyfile, wait=True)
477                # don't use self._run_transaction() here, to avoid sending
478                # unneeded signals
479                yield trans.run(defer=True)
480        yield self.reload(sourcepart)
481
482    @inline_callbacks
483    def add_vendor_key_from_keyserver(self, keyid,
484                                  keyserver="hkp://keyserver.ubuntu.com:80/",
485                                  metadata=None):
486        # strip the keysize
487        if "/" in keyid:
488            keyid = keyid.split("/")[1]
489        if not keyid.startswith("0x"):
490            keyid = "0x%s" % keyid
491        trans = None
492        try:
493            trans = yield self.aptd_client.add_vendor_key_from_keyserver(
494                keyid, keyserver, defer=True)
495            yield self._run_transaction(trans, None, None, None, metadata)
496        except Exception as error:
497            self._on_trans_error(error, trans)
498
499    @inline_callbacks
500    def add_sources_list_entry(self, source_entry, sourcepart=None):
501        if isinstance(source_entry, basestring):
502            entry = SourceEntry(source_entry)
503        elif isinstance(source_entry, SourceEntry):
504            entry = source_entry
505        else:
506            raise ValueError("Unsupported entry type %s" % type(source_entry))
507
508        if not sourcepart:
509            sourcepart = sources_filename_from_ppa_entry(entry)
510
511        args = (entry.type, entry.uri, entry.dist, entry.comps,
512                "Added by software-center", sourcepart)
513        trans = None
514        try:
515            trans = yield self.aptd_client.add_repository(*args, defer=True)
516            yield self._run_transaction(trans, None, None, None)
517        except errors.NotAuthorizedError, err:
518            self._logger.error("add_repository: '%s'" % err)
519            return_value(None)
520        return_value(sourcepart)
521
522    @inline_callbacks
523    def authenticate_for_purchase(self):
524        """
525        helper that authenticates with aptdaemon for a purchase operation
526        """
527        bus = get_dbus_bus()
528        name = bus.get_unique_name()
529        action = policykit1.PK_ACTION_INSTALL_PURCHASED_PACKAGES
530        flags = policykit1.CHECK_AUTH_ALLOW_USER_INTERACTION
531        yield policykit1.check_authorization_by_name(name, action, flags=flags)
532
533    @inline_callbacks
534    def add_license_key(self, license_key, license_key_path,
535                        license_key_oauth, pkgname):
536        """ add a license key for a purchase. """
537        self._logger.debug(
538            "adding license_key for pkg '%s' of len: %i" % (
539                pkgname, len(license_key)))
540        trans = None
541        # HOME based license keys
542        if license_key_path and license_key_path.startswith("~"):
543            # check if its inside HOME and if so, just create it
544            dest = os.path.expanduser(os.path.normpath(license_key_path))
545            dirname = os.path.dirname(dest)
546            if not os.path.exists(dirname):
547                os.makedirs(dirname)
548            if not os.path.exists(dest):
549                f = open(dest, "w")
550                f.write(license_key)
551                f.close()
552                os.chmod(dest, 0600)
553            else:
554                self._logger.warn("license file '%s' already exists" % dest)
555        else:
556            # system-wide keys
557            try:
558                self._logger.info("adding license key for '%s'" % pkgname)
559                trans = yield self.aptd_client.add_license_key(
560                    pkgname, license_key_oauth, self.LICENSE_KEY_SERVER)
561                yield self._run_transaction(trans, None, None, None)
562            except Exception as e:
563                self._logger.error("add_license_key: '%s'" % e)
564
565    @inline_callbacks
566    def add_repo_add_key_and_install_app(self,
567                                         deb_line,
568                                         signing_key_id,
569                                         app,
570                                         iconname,
571                                         license_key,
572                                         license_key_path,
573                                         json_oauth_token=None,
574                                         purchase=True):
575        """
576        a convenience method that combines all of the steps needed
577        to install a for-pay application, including adding the
578        source entry and the vendor key, reloading the package list,
579        and finally installing the specified application once the
580        package list reload has completed.
581        """
582        self._logger.info("add_repo_add_key_and_install_app() '%s' '%s' '%s'" %
583            (re.sub("deb https://.*@", "", deb_line),  # strip out password
584            signing_key_id,
585            app.pkgname))
586
587        if purchase:
588            # pre-authenticate
589            try:
590                yield self.authenticate_for_purchase()
591            except:
592                self._logger.exception("authenticate_for_purchase failed")
593                self._clean_pending_purchases(app.pkgname)
594                result = TransactionFinishedResult(None, False)
595                result.pkgname = app.pkgname
596                self.emit("transaction-stopped", result)
597                return
598            # done
599            fake_trans = FakePurchaseTransaction(app, iconname)
600            self.emit("transaction-started", app.pkgname, app.appname,
601                fake_trans.tid, TransactionTypes.INSTALL)
602            self.pending_purchases[app.pkgname] = fake_trans
603        else:
604            # FIXME: add authenticate_for_added_repo here
605            pass
606
607        # add the metadata early, add_sources_list_entry is a transaction
608        # too
609        trans_metadata = {
610            'sc_add_repo_and_install_appname': app.appname,
611            'sc_add_repo_and_install_pkgname': app.pkgname,
612            'sc_add_repo_and_install_deb_line': deb_line,
613            'sc_iconname': iconname,
614            'sc_add_repo_and_install_try': "1",
615            'sc_add_repo_and_install_license_key': license_key or "",
616            'sc_add_repo_and_install_license_key_path': license_key_path or "",
617            'sc_add_repo_and_install_license_key_token':
618                json_oauth_token or "",
619        }
620
621        self._logger.info("add_sources_list_entry()")
622        sourcepart = yield self.add_sources_list_entry(deb_line)
623        trans_metadata['sc_add_repo_and_install_sources_list'] = sourcepart
624
625        # metadata so that we know those the add-key and reload transactions
626        # are part of a group
627        self._logger.info("add_vendor_key_from_keyserver()")
628        yield self.add_vendor_key_from_keyserver(signing_key_id,
629                                                 metadata=trans_metadata)
630        self._logger.info("reload_for_commercial_repo()")
631        yield self._reload_for_commercial_repo(app, trans_metadata, sourcepart)
632
633    @inline_callbacks
634    def _reload_for_commercial_repo_defer(self, app, trans_metadata,
635                                          sources_list):
636        """
637        helper that reloads and registers a callback for when the reload is
638        finished
639        """
640        trans_metadata["sc_add_repo_and_install_ignore_errors"] = "1"
641        # and then queue the install only when the reload finished
642        # otherwise the daemon will fail because he does not know
643        # the new package name yet
644        self.connect("reload-finished",
645                     self._on_reload_for_add_repo_and_install_app_finished,
646                     trans_metadata, app)
647        # reload to ensure we have the new package data
648        yield self.reload(sources_list=sources_list, metadata=trans_metadata)
649
650    def _reload_for_commercial_repo(self, app, trans_metadata, sources_list):
651        """ this reloads a commercial repo in a glib timeout
652            See _reload_for_commercial_repo_inline() for the actual work
653            that is done
654        """
655        self._logger.info("_reload_for_commercial_repo() %s" % app)
656        # trigger inline_callbacked function
657        self._reload_for_commercial_repo_defer(
658            app, trans_metadata, sources_list)
659        # return False to stop the timeout (one shot only)
660        return False
661
662    @inline_callbacks
663    def _on_reload_for_add_repo_and_install_app_finished(self, backend, trans,
664                                                         result, metadata,
665                                                         app):
666        """
667        callback that is called once after reload was queued
668        and will trigger the install of the for-pay package itself
669        (after that it will automatically de-register)
670        """
671        #print "_on_reload_for_add_repo_and_install_app_finished", trans, \
672        #    result, backend, self._reload_signal_id
673        self._logger.info("_on_reload_for_add_repo_and_install_app_finished() "
674            "%s %s %s" % (trans, result, app))
675
676        # check if this is the transaction we waiting for
677        key = "sc_add_repo_and_install_pkgname"
678        if not (key in trans.meta_data and
679                trans.meta_data[key] == app.pkgname):
680            return_value(None)
681
682        # get the debline and check if we have a release.gpg file
683        deb_line = trans.meta_data["sc_add_repo_and_install_deb_line"]
684        license_key = trans.meta_data["sc_add_repo_and_install_license_key"]
685        license_key_path = trans.meta_data[
686            "sc_add_repo_and_install_license_key_path"]
687        license_key_oauth = trans.meta_data[
688            "sc_add_repo_and_install_license_key_token"]
689        release_filename = release_filename_in_lists_from_deb_line(deb_line)
690        lists_dir = apt_pkg.config.find_dir("Dir::State::lists")
691        release_signature = os.path.join(lists_dir, release_filename) + ".gpg"
692        self._logger.info("looking for '%s'" % release_signature)
693        # no Release.gpg in the newly added repository, try again,
694        # this can happen e.g. on odd network proxies
695        if not os.path.exists(release_signature):
696            self._logger.warn("no %s found, re-trying" % release_signature)
697            result = False
698
699        # disconnect again, this is only a one-time operation
700        self.disconnect_by_func(
701            self._on_reload_for_add_repo_and_install_app_finished)
702
703        # FIXME: this logic will *fail* if the sources.list of the user
704        #        was broken before
705
706        # run install action if the repo was added successfully
707        if result:
708            self.emit("channels-changed", True)
709
710            # we use aptd_client.install_packages() here instead
711            # of just
712            #  self.install(app, "", metadata=metadata)
713            # go get less authentication prompts (because of the
714            # 03_auth_me_less patch in aptdaemon)
715            try:
716                self._logger.info("install_package()")
717                trans = yield self.aptd_client.install_packages(
718                    [app.pkgname], defer=True)
719                self._logger.info("run_transaction()")
720                # notify about the install so that the unity-launcher
721                # integration works
722                self.emit("transaction-started",
723                          app.pkgname, app.appname, trans.tid,
724                          TransactionTypes.INSTALL)
725                yield self._run_transaction(trans, app.pkgname, app.appname,
726                                            "", metadata)
727            except Exception as error:
728                self._on_trans_error(error, trans, app.pkgname)
729            # add license_key
730            # FIXME: aptd fails if there is a license_key_path already
731            #        but I wonder if we should ease that restriction
732            if license_key and not os.path.exists(license_key_path):
733                yield self.add_license_key(
734                    license_key, license_key_path, license_key_oauth,
735                    app.pkgname)
736
737        else:
738            # download failure
739            # ok, here is the fun! we can not reload() immediately, because
740            # there is a delay of up to 5(!) minutes between s-c-agent telling
741            # us that we can download software and actually being able to
742            # download it
743            retry = int(trans.meta_data['sc_add_repo_and_install_try'])
744            if retry > 10:
745                self._logger.error("failed to add repo after 10 tries")
746                self._clean_pending_purchases(
747                    trans.meta_data['sc_add_repo_and_install_pkgname'])
748                self._show_transaction_failed_dialog(trans, result)
749                return_value(False)
750            # this just sets the meta_data locally, but that is ok, the
751            # whole re-try machinery will not survive anyway if the local
752            # s-c instance is closed
753            self._logger.info("queuing reload in 30s")
754            trans.meta_data["sc_add_repo_and_install_try"] = str(retry + 1)
755            sourcepart = trans.meta_data[
756                "sc_add_repo_and_install_sources_list"]
757            GLib.timeout_add_seconds(30, self._reload_for_commercial_repo,
758                                     app, trans.meta_data, sourcepart)
759
760    # internal helpers
761    def _on_lowlevel_transactions_changed(self, watcher, current, pending):
762        # cleanup progress signal (to be sure to not leave dbus
763        # matchers around)
764        if self._progress_signal:
765            GLib.source_remove(self._progress_signal)
766            self._progress_signal = None
767        # attach progress-changed signal for current transaction
768        if current:
769            try:
770                trans = client.get_transaction(current)
771                self._progress_signal = trans.connect("progress-changed",
772                    self._on_progress_changed)
773            except dbus.DBusException:
774                pass
775
776        # now update pending transactions
777        self.pending_transactions.clear()
778        for tid in [current] + pending:
779            if not tid:
780                continue
781            try:
782                trans = client.get_transaction(tid,
783                    error_handler=lambda x: True)
784            except dbus.DBusException:
785                continue
786            trans_progress = TransactionProgress(trans)
787            try:
788                self.pending_transactions[trans_progress.pkgname] = \
789                    trans_progress
790            except KeyError:
791                # if its not a transaction from us (sc_pkgname) still
792                # add it with the tid as key to get accurate results
793                # (the key of pending_transactions is never directly
794                #  exposed in the UI)
795                self.pending_transactions[trans.tid] = trans_progress
796        # emit signal
797        self.inject_fake_transactions_and_emit_changed_signal()
798
799    def inject_fake_transactions_and_emit_changed_signal(self):
800        """
801        ensures that the fake transactions are considered and emits
802        transactions-changed signal with the right pending transactions
803        """
804        # inject a bunch of FakePurchaseTransaction into the transactions dict
805        for pkgname in self.pending_purchases:
806            self.pending_transactions[pkgname] = \
807                self.pending_purchases[pkgname]
808        # and emit the signal
809        self.emit("transactions-changed", self.pending_transactions)
810
811    def _on_progress_changed(self, trans, progress):
812        """
813        internal helper that gets called on our package transaction progress
814        (only showing pkg progress currently)
815        """
816        try:
817            pkgname = trans.meta_data["sc_pkgname"]
818            self.pending_transactions[pkgname].progress = progress
819            self.emit("transaction-progress-changed", pkgname, progress)
820        except KeyError:
821            pass
822
823    def _show_transaction_failed_dialog(self, trans, enum,
824                                        alternative_action=None):
825        # daemon died are messages that result from broken
826        # cancel handling in aptdaemon (LP: #440941)
827        # FIXME: this is not a proper fix, just a workaround
828        if trans.error_code == enums.ERROR_DAEMON_DIED:
829            self._logger.warn("daemon dies, ignoring: %s %s" % (trans, enum))
830            return
831        # hide any private ppa details in the error message since it may
832        # appear in the logs for LP bugs and potentially in screenshots as well
833        cleaned_error_details = obfuscate_private_ppa_details(
834            trans.error_details)
835        msg = utf8("%s: %s\n%s\n\n%s") % (
836            utf8(_("Error")),
837            utf8(enums.get_error_string_from_enum(trans.error_code)),
838            utf8(enums.get_error_description_from_enum(trans.error_code)),
839            utf8(cleaned_error_details))
840        self._logger.error("error in _on_trans_finished '%s'" % msg)
841        # show dialog to the user and exit (no need to reopen the cache)
842        if not trans.error_code:
843            # sometimes aptdaemon doesn't return a value for error_code
844            # when the network connection has become unavailable; in
845            # that case, we will assume it's a failure during a package
846            # download because that is the only case where we see this
847            # happening - this avoids display of an empty error dialog
848            # and correctly prompts the user to check their network
849            # connection (see LP: #747172)
850            # FIXME: fix aptdaemon to return a valid error_code under
851            # all conditions
852            trans.error_code = enums.ERROR_PACKAGE_DOWNLOAD_FAILED
853        # show dialog to the user and exit (no need to reopen
854        # the cache)
855        res = self.ui.error(None,
856            utf8(enums.get_error_string_from_enum(trans.error_code)),
857            utf8(enums.get_error_description_from_enum(trans.error_code)),
858            utf8(cleaned_error_details),
859            utf8(alternative_action))
860        return res
861
862    def _get_app_and_icon_and_deb_from_trans(self, trans):
863        meta_copy = trans.meta_data.copy()
864        app = Application(meta_copy.pop("sc_appname", None),
865                          meta_copy.pop("sc_pkgname"))
866        iconname = meta_copy.pop("sc_iconname", None)
867        filename = meta_copy.pop("sc_filename", "")
868        return app, iconname, filename, meta_copy
869
870    def _on_trans_finished(self, trans, enum):
871        """callback when a aptdaemon transaction finished"""
872        self._logger.debug("_on_transaction_finished: %s %s %s" % (
873                trans, enum, trans.meta_data))
874
875        # first check if there has been a cancellation of
876        # the install and fire a transaction-cancelled signal
877        # (see LP: #1027209)
878        if enum == enums.EXIT_CANCELLED:
879            result = TransactionFinishedResult(trans, False)
880            self.emit("transaction-cancelled", result)
881            return
882
883        # show error
884        if enum == enums.EXIT_FAILED:
885            # Handle invalid packages separately
886            if (trans.error and
887                    trans.error.code == enums.ERROR_INVALID_PACKAGE_FILE):
888                action = _("_Ignore and install")
889                res = self._show_transaction_failed_dialog(
890                    trans, enum, action)
891                if res == "yes":
892                    # Reinject the transaction
893                    app, iconname, filename, meta_copy = \
894                        self._get_app_and_icon_and_deb_from_trans(trans)
895                    self.install(app, iconname, filename, [], [],
896                                 metadata=meta_copy, force=True)
897                    return
898            # on unauthenticated errors, try a "repair" using the
899            # reload functionality
900            elif (trans.error and
901                  trans.error.code == enums.ERROR_PACKAGE_UNAUTHENTICATED):
902                action = _("Repair")
903                res = self._show_transaction_failed_dialog(
904                    trans, enum, action)
905                if res == "yes":
906                    app, iconname, filename, meta_copy = \
907                        self._get_app_and_icon_and_deb_from_trans(trans)
908                    self.reload()
909                    self.install(app, iconname, filename, [], [],
910                                 metadata=meta_copy)
911                    return
912            # Finish a cancelled installation before resuming. If the
913            # user e.g. rebooted during a debconf question apt
914            # will hang and the user is required to call
915            # dpkg --configure -a, see LP#659438
916            elif (trans.error and
917                  trans.error.code == enums.ERROR_INCOMPLETE_INSTALL):
918                action = _("Repair")
919                res = self._show_transaction_failed_dialog(trans, enum,
920                                                           action)
921                if res == "yes":
922                    self.fix_incomplete_install()
923                    return
924
925            elif (not "sc_add_repo_and_install_ignore_errors" in
926                  trans.meta_data):
927                self._show_transaction_failed_dialog(trans, enum)
928
929        # send finished signal, use "" here instead of None, because
930        # dbus mangles a None to a str("None")
931        pkgname = ""
932        try:
933            pkgname = trans.meta_data["sc_pkgname"]
934            del self.pending_transactions[pkgname]
935            self.emit("transaction-progress-changed", pkgname, 100)
936        except KeyError:
937            pass
938        # if it was a cache-reload, trigger a-x-i update
939        if trans.role == enums.ROLE_UPDATE_CACHE:
940            if enum == enums.EXIT_SUCCESS:
941                self.update_xapian_index()
942            self.emit("reload-finished", trans, enum != enums.EXIT_FAILED)
943        # send appropriate signals
944        self.inject_fake_transactions_and_emit_changed_signal()
945        self.emit("transaction-finished", TransactionFinishedResult(trans,
946            enum != enums.EXIT_FAILED))
947
948    @inline_callbacks
949    def _config_file_conflict(self, transaction, old, new):
950        reply = self.ui.ask_config_file_conflict(old, new)
951        if reply == "replace":
952            yield transaction.resolve_config_file_conflict(old, "replace",
953                                                           defer=True)
954        elif reply == "keep":
955            yield transaction.resolve_config_file_conflict(old, "keep",
956                                                           defer=True)
957        else:
958            raise Exception(
959                "unknown reply: '%s' in _ask_config_file_conflict " % reply)
960
961    @inline_callbacks
962    def _medium_required(self, transaction, medium, drive):
963        res = self.ui.ask_medium_required(medium, drive)
964        if res:
965            yield transaction.provide_medium(medium, defer=True)
966        else:
967            yield transaction.cancel(defer=True)
968
969    @inline_callbacks
970    def _run_transaction(self, trans, pkgname, appname, iconname,
971                         metadata=None):
972        # connect signals
973        trans.connect("config-file-conflict", self._config_file_conflict)
974        trans.connect("medium-required", self._medium_required)
975        trans.connect("finished", self._on_trans_finished)
976        try:
977            # set appname/iconname/pkgname only if we actually have one
978            if appname:
979                yield trans.set_meta_data(sc_appname=appname, defer=True)
980            if iconname:
981                yield trans.set_meta_data(sc_iconname=iconname, defer=True)
982            # we do not always have a pkgname, e.g. "cache_update" does not
983            if pkgname:
984                # ensure the metadata is just the pkgname
985                sc_pkgname = pkgname.split("/")[0].split("=")[0]
986                yield trans.set_meta_data(sc_pkgname=sc_pkgname, defer=True)
987                # setup debconf only if we have a pkg
988                yield trans.set_debconf_frontend("gnome", defer=True)
989                trans.set_remove_obsoleted_depends(True, defer=True)
990                self._progress_signal = trans.connect("progress-changed",
991                    self._on_progress_changed)
992                self.pending_transactions[pkgname] = TransactionProgress(trans)
993            # generic metadata
994            if metadata:
995                yield trans.set_meta_data(defer=True, **metadata)
996            yield trans.run(defer=True)
997        except Exception as error:
998            self._on_trans_error(error, trans, pkgname)
999            # on error we need to clean the pending purchases
1000            self._clean_pending_purchases(pkgname)
1001        # on success the pending purchase is cleaned when the package
1002        # that was purchased finished installing
1003        if trans.role == enums.ROLE_INSTALL_PACKAGES:
1004            self._clean_pending_purchases(pkgname)
1005
1006    def _clean_pending_purchases(self, pkgname):
1007        if pkgname and pkgname in self.pending_purchases:
1008            del self.pending_purchases[pkgname]
1009
1010    def _on_trans_error(self, error, trans, pkgname=None):
1011        self._logger.warn("_on_trans_error: '%r' '%s' '%s'" % (
1012                error, trans, pkgname))
1013        # re-enable the action button again if anything went wrong
1014        result = TransactionFinishedResult(None, False)
1015        result.pkgname = pkgname
1016
1017        # clean up pending transactions
1018        if pkgname and pkgname in self.pending_transactions:
1019            del self.pending_transactions[pkgname]
1020
1021        # calculate a dupes_signature here to have different buckets
1022        # on errors.ubuntu.com for the different crash types
1023        if trans:
1024            error_code = trans.error_code
1025        else:
1026            error_code = "no-transaction"
1027        dupes_signature = "software-center:trans-failed:%s" % error_code
1028
1029        self.emit("transaction-stopped", result)
1030        if isinstance(error, dbus.DBusException):
1031            # ignore errors that the user knows about already (like
1032            # that he entered a wrong password or that he does not
1033            # care about (like no-reply)
1034            name = error.get_dbus_name()
1035            if name in ["org.freedesktop.PolicyKit.Error.NotAuthorized",
1036                        "org.freedesktop.DBus.Error.NoReply"]:
1037                return
1038            # we want to give some advice here to the user but also
1039            # report this via apport
1040            if name in ["org.freedesktop.PolicyKit.Error.Failed"]:
1041                summary = _("Authentication Error")
1042                text = _("Software can't be installed or removed because "
1043                         "the authentication service is not available. "
1044                         "(%s") % error
1045                # send to apport for reporting
1046                self._call_apport_recoverable_error(
1047                    text, error, dupes_signature)
1048                # ... and display as a dialog
1049                self.ui.error(None, summary, text)
1050                return
1051
1052        # lintian errors are ignored and not send to apport_recoverable_error
1053        # and dpkg errors as well as they will already be recorded separately
1054        # by apt itself
1055        if error_code in (enums.ERROR_INVALID_PACKAGE_FILE,
1056                                enums.ERROR_PACKAGE_MANAGER_FAILED):
1057            return
1058
1059        # show a apport recoverable error dialog to the user as we want
1060        # to know about these issues
1061        self._call_apport_recoverable_error(
1062            _("There was an error submitting the transaction"),
1063            error,
1064            dupes_signature)
1065
1066    def _call_apport_recoverable_error(self, text, error, dupes_signature):
1067        """Call apport's recoverable_problem dialog """
1068
1069        # ensure we have a proper exception string in the report
1070        if isinstance(error, Exception):
1071            error = traceback.format_exc(error)
1072
1073        # mvo: I don't think we need to send "Package\0software-center",
1074        #      apport should figure this out itself
1075        data = ("DialogBody\0%(text)s\0"
1076                "Traceback\0%(error)s\0"
1077                "DuplicateSignature\0%(dupes_signature)s" % {
1078                'text': text,
1079                'error': error,
1080                'dupes_signature': dupes_signature,
1081                })
1082        # This will be quick as it just writes the report file. Then
1083        # the report gets picked up asynchronously by a inotify watch
1084        # and displayed to the user in a separate process.
1085        p = Popen(
1086            [APPORT_RECOVERABLE_ERROR], stdin=PIPE, stdout=PIPE, stderr=PIPE)
1087
1088        (stdout, stderr) = p.communicate(input=data)
1089        if p.returncode != 0:
1090            logging.warn("%s returned '%s' ('%s', '%s')" % (
1091                    APPORT_RECOVERABLE_ERROR, p.returncode, stdout, stderr))
1092
1093
1094if __name__ == "__main__":
1095    #c = client.AptClient()
1096    #c.remove_packages(["4g8"], remove_unused_dependencies=True)
1097    backend = AptdaemonBackend()
1098    #backend.reload()
1099    backend.enable_component("multiverse")
1100    from gi.repository import Gtk
1101    Gtk.main()
Note: See TracBrowser for help on using the repository browser.