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

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

Added 16.01+16.04.20160119 sources

File size: 46.0 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                self.emit("transaction-started", pkgname, appname, trans.tid,
385                    TransactionTypes.INSTALL)
386            yield self._run_transaction(
387                trans, pkgname, appname, iconname, metadata)
388        except Exception as error:
389            self._on_trans_error(error, trans, pkgname)
390
391    @inline_callbacks
392    def install_multiple(self, apps, iconnames, addons_install=[],
393                         addons_remove=[], metadatas=None):
394        """ queue a list of packages for install  """
395        if metadatas is None:
396            metadatas = []
397            for item in apps:
398                metadatas.append(None)
399        for app, iconname, metadata in zip(apps, iconnames, metadatas):
400            yield self.install(app, iconname, metadata=metadata)
401
402    @inline_callbacks
403    def apply_changes(self, app, iconname, addons_install=[],
404                      addons_remove=[], metadata=None):
405        """ install and remove add-ons """
406        pkgname = app.pkgname
407        appname = app.appname
408        trans = None
409        try:
410            install = addons_install
411            remove = addons_remove
412            reinstall = remove = purge = upgrade = downgrade = []
413            trans = yield self.aptd_client.commit_packages(
414                install, reinstall, remove, purge, upgrade, downgrade,
415                defer=True)
416            self.emit("transaction-started", pkgname, appname, trans.tid,
417                TransactionTypes.APPLY)
418            yield self._run_transaction(trans, pkgname, appname, iconname)
419        except Exception as error:
420            self._on_trans_error(error, trans)
421
422    @inline_callbacks
423    def reload(self, sources_list=None, metadata=None):
424        """ reload package list """
425        # check if the sourcespart is there, if not, do a full reload
426        # this can happen when the "partner" repository is added, it
427        # will be in the main sources.list already and this means that
428        # aptsources will just enable it instead of adding a extra
429        # sources.list.d file (LP: #666956)
430        d = apt_pkg.config.find_dir("Dir::Etc::sourceparts")
431        trans = None
432        if (not sources_list or
433                not os.path.exists(os.path.join(d, sources_list))):
434            sources_list = ""
435        try:
436            trans = yield self.aptd_client.update_cache(
437                sources_list=sources_list, defer=True)
438            yield self._run_transaction(trans, None, None, None, metadata)
439        except Exception as error:
440            self._on_trans_error(error, trans)
441        # note that the cache re-open will happen via the connected
442        # "transaction-finished" signal
443
444    @inline_callbacks
445    def enable_component(self, component):
446        self._logger.debug("enable_component: %s" % component)
447        trans = None
448        try:
449            trans = yield self.aptd_client.enable_distro_component(component)
450            # don't use self._run_transaction() here, to avoid sending unneeded
451            # signals
452            yield trans.run(defer=True)
453        except Exception as error:
454            self._on_trans_error(error, trans, component)
455            return_value(None)
456        # now update the cache
457        yield self.reload()
458
459    @inline_callbacks
460    def enable_channel(self, channelfile):
461        trans = None
462        # read channel file and add all relevant lines
463        for line in open(channelfile):
464            line = line.strip()
465            if not line:
466                continue
467            entry = SourceEntry(line)
468            if entry.invalid:
469                continue
470            sourcepart = os.path.basename(channelfile)
471            yield self.add_sources_list_entry(entry, sourcepart)
472            keyfile = channelfile.replace(".list", ".key")
473            if os.path.exists(keyfile):
474                trans = yield self.aptd_client.add_vendor_key_from_file(
475                    keyfile, wait=True)
476                # don't use self._run_transaction() here, to avoid sending
477                # unneeded signals
478                yield trans.run(defer=True)
479        yield self.reload(sourcepart)
480
481    @inline_callbacks
482    def add_vendor_key_from_keyserver(self, keyid,
483                                  keyserver="hkp://keyserver.ubuntu.com:80/",
484                                  metadata=None):
485        # strip the keysize
486        if "/" in keyid:
487            keyid = keyid.split("/")[1]
488        if not keyid.startswith("0x"):
489            keyid = "0x%s" % keyid
490        trans = None
491        try:
492            trans = yield self.aptd_client.add_vendor_key_from_keyserver(
493                keyid, keyserver, defer=True)
494            yield self._run_transaction(trans, None, None, None, metadata)
495        except Exception as error:
496            self._on_trans_error(error, trans)
497
498    @inline_callbacks
499    def add_sources_list_entry(self, source_entry, sourcepart=None):
500        if isinstance(source_entry, basestring):
501            entry = SourceEntry(source_entry)
502        elif isinstance(source_entry, SourceEntry):
503            entry = source_entry
504        else:
505            raise ValueError("Unsupported entry type %s" % type(source_entry))
506
507        if not sourcepart:
508            sourcepart = sources_filename_from_ppa_entry(entry)
509
510        args = (entry.type, entry.uri, entry.dist, entry.comps,
511                "Added by software-center", sourcepart)
512        trans = None
513        try:
514            trans = yield self.aptd_client.add_repository(*args, defer=True)
515            yield self._run_transaction(trans, None, None, None)
516        except errors.NotAuthorizedError, err:
517            self._logger.error("add_repository: '%s'" % err)
518            return_value(None)
519        return_value(sourcepart)
520
521    @inline_callbacks
522    def authenticate_for_purchase(self):
523        """
524        helper that authenticates with aptdaemon for a purchase operation
525        """
526        bus = get_dbus_bus()
527        name = bus.get_unique_name()
528        action = policykit1.PK_ACTION_INSTALL_PURCHASED_PACKAGES
529        flags = policykit1.CHECK_AUTH_ALLOW_USER_INTERACTION
530        yield policykit1.check_authorization_by_name(name, action, flags=flags)
531
532    @inline_callbacks
533    def add_license_key(self, license_key, license_key_path,
534                        license_key_oauth, pkgname):
535        """ add a license key for a purchase. """
536        self._logger.debug(
537            "adding license_key for pkg '%s' of len: %i" % (
538                pkgname, len(license_key)))
539        trans = None
540        # HOME based license keys
541        if license_key_path and license_key_path.startswith("~"):
542            # check if its inside HOME and if so, just create it
543            dest = os.path.expanduser(os.path.normpath(license_key_path))
544            dirname = os.path.dirname(dest)
545            if not os.path.exists(dirname):
546                os.makedirs(dirname)
547            if not os.path.exists(dest):
548                f = open(dest, "w")
549                f.write(license_key)
550                f.close()
551                os.chmod(dest, 0600)
552            else:
553                self._logger.warn("license file '%s' already exists" % dest)
554        else:
555            # system-wide keys
556            try:
557                self._logger.info("adding license key for '%s'" % pkgname)
558                trans = yield self.aptd_client.add_license_key(
559                    pkgname, license_key_oauth, self.LICENSE_KEY_SERVER)
560                yield self._run_transaction(trans, None, None, None)
561            except Exception as e:
562                self._logger.error("add_license_key: '%s'" % e)
563
564    @inline_callbacks
565    def add_repo_add_key_and_install_app(self,
566                                         deb_line,
567                                         signing_key_id,
568                                         app,
569                                         iconname,
570                                         license_key,
571                                         license_key_path,
572                                         json_oauth_token=None,
573                                         purchase=True):
574        """
575        a convenience method that combines all of the steps needed
576        to install a for-pay application, including adding the
577        source entry and the vendor key, reloading the package list,
578        and finally installing the specified application once the
579        package list reload has completed.
580        """
581        self._logger.info("add_repo_add_key_and_install_app() '%s' '%s' '%s'" %
582            (re.sub("deb https://.*@", "", deb_line),  # strip out password
583            signing_key_id,
584            app.pkgname))
585
586        if purchase:
587            # pre-authenticate
588            try:
589                yield self.authenticate_for_purchase()
590            except:
591                self._logger.exception("authenticate_for_purchase failed")
592                self._clean_pending_purchases(app.pkgname)
593                result = TransactionFinishedResult(None, False)
594                result.pkgname = app.pkgname
595                self.emit("transaction-stopped", result)
596                return
597            # done
598            fake_trans = FakePurchaseTransaction(app, iconname)
599            self.emit("transaction-started", app.pkgname, app.appname,
600                fake_trans.tid, TransactionTypes.INSTALL)
601            self.pending_purchases[app.pkgname] = fake_trans
602        else:
603            # FIXME: add authenticate_for_added_repo here
604            pass
605
606        # add the metadata early, add_sources_list_entry is a transaction
607        # too
608        trans_metadata = {
609            'sc_add_repo_and_install_appname': app.appname,
610            'sc_add_repo_and_install_pkgname': app.pkgname,
611            'sc_add_repo_and_install_deb_line': deb_line,
612            'sc_iconname': iconname,
613            'sc_add_repo_and_install_try': "1",
614            'sc_add_repo_and_install_license_key': license_key or "",
615            'sc_add_repo_and_install_license_key_path': license_key_path or "",
616            'sc_add_repo_and_install_license_key_token':
617                json_oauth_token or "",
618        }
619
620        self._logger.info("add_sources_list_entry()")
621        sourcepart = yield self.add_sources_list_entry(deb_line)
622        trans_metadata['sc_add_repo_and_install_sources_list'] = sourcepart
623
624        # metadata so that we know those the add-key and reload transactions
625        # are part of a group
626        self._logger.info("add_vendor_key_from_keyserver()")
627        yield self.add_vendor_key_from_keyserver(signing_key_id,
628                                                 metadata=trans_metadata)
629        self._logger.info("reload_for_commercial_repo()")
630        yield self._reload_for_commercial_repo(app, trans_metadata, sourcepart)
631
632    @inline_callbacks
633    def _reload_for_commercial_repo_defer(self, app, trans_metadata,
634                                          sources_list):
635        """
636        helper that reloads and registers a callback for when the reload is
637        finished
638        """
639        trans_metadata["sc_add_repo_and_install_ignore_errors"] = "1"
640        # and then queue the install only when the reload finished
641        # otherwise the daemon will fail because he does not know
642        # the new package name yet
643        self.connect("reload-finished",
644                     self._on_reload_for_add_repo_and_install_app_finished,
645                     trans_metadata, app)
646        # reload to ensure we have the new package data
647        yield self.reload(sources_list=sources_list, metadata=trans_metadata)
648
649    def _reload_for_commercial_repo(self, app, trans_metadata, sources_list):
650        """ this reloads a commercial repo in a glib timeout
651            See _reload_for_commercial_repo_inline() for the actual work
652            that is done
653        """
654        self._logger.info("_reload_for_commercial_repo() %s" % app)
655        # trigger inline_callbacked function
656        self._reload_for_commercial_repo_defer(
657            app, trans_metadata, sources_list)
658        # return False to stop the timeout (one shot only)
659        return False
660
661    @inline_callbacks
662    def _on_reload_for_add_repo_and_install_app_finished(self, backend, trans,
663                                                         result, metadata,
664                                                         app):
665        """
666        callback that is called once after reload was queued
667        and will trigger the install of the for-pay package itself
668        (after that it will automatically de-register)
669        """
670        #print "_on_reload_for_add_repo_and_install_app_finished", trans, \
671        #    result, backend, self._reload_signal_id
672        self._logger.info("_on_reload_for_add_repo_and_install_app_finished() "
673            "%s %s %s" % (trans, result, app))
674
675        # check if this is the transaction we waiting for
676        key = "sc_add_repo_and_install_pkgname"
677        if not (key in trans.meta_data and
678                trans.meta_data[key] == app.pkgname):
679            return_value(None)
680
681        # get the debline and check if we have a release.gpg file
682        deb_line = trans.meta_data["sc_add_repo_and_install_deb_line"]
683        license_key = trans.meta_data["sc_add_repo_and_install_license_key"]
684        license_key_path = trans.meta_data[
685            "sc_add_repo_and_install_license_key_path"]
686        license_key_oauth = trans.meta_data[
687            "sc_add_repo_and_install_license_key_token"]
688        release_filename = release_filename_in_lists_from_deb_line(deb_line)
689        lists_dir = apt_pkg.config.find_dir("Dir::State::lists")
690        release_signature = os.path.join(lists_dir, release_filename) + ".gpg"
691        self._logger.info("looking for '%s'" % release_signature)
692        # no Release.gpg in the newly added repository, try again,
693        # this can happen e.g. on odd network proxies
694        if not os.path.exists(release_signature):
695            self._logger.warn("no %s found, re-trying" % release_signature)
696            result = False
697
698        # disconnect again, this is only a one-time operation
699        self.disconnect_by_func(
700            self._on_reload_for_add_repo_and_install_app_finished)
701
702        # FIXME: this logic will *fail* if the sources.list of the user
703        #        was broken before
704
705        # run install action if the repo was added successfully
706        if result:
707            self.emit("channels-changed", True)
708
709            # we use aptd_client.install_packages() here instead
710            # of just
711            #  self.install(app, "", metadata=metadata)
712            # go get less authentication prompts (because of the
713            # 03_auth_me_less patch in aptdaemon)
714            try:
715                self._logger.info("install_package()")
716                trans = yield self.aptd_client.install_packages(
717                    [app.pkgname], defer=True)
718                self._logger.info("run_transaction()")
719                # notify about the install so that the unity-launcher
720                # integration works
721                self.emit("transaction-started",
722                          app.pkgname, app.appname, trans.tid,
723                          TransactionTypes.INSTALL)
724                yield self._run_transaction(trans, app.pkgname, app.appname,
725                                            "", metadata)
726            except Exception as error:
727                self._on_trans_error(error, trans, app.pkgname)
728            # add license_key
729            # FIXME: aptd fails if there is a license_key_path already
730            #        but I wonder if we should ease that restriction
731            if license_key and not os.path.exists(license_key_path):
732                yield self.add_license_key(
733                    license_key, license_key_path, license_key_oauth,
734                    app.pkgname)
735
736        else:
737            # download failure
738            # ok, here is the fun! we can not reload() immediately, because
739            # there is a delay of up to 5(!) minutes between s-c-agent telling
740            # us that we can download software and actually being able to
741            # download it
742            retry = int(trans.meta_data['sc_add_repo_and_install_try'])
743            if retry > 10:
744                self._logger.error("failed to add repo after 10 tries")
745                self._clean_pending_purchases(
746                    trans.meta_data['sc_add_repo_and_install_pkgname'])
747                self._show_transaction_failed_dialog(trans, result)
748                return_value(False)
749            # this just sets the meta_data locally, but that is ok, the
750            # whole re-try machinery will not survive anyway if the local
751            # s-c instance is closed
752            self._logger.info("queuing reload in 30s")
753            trans.meta_data["sc_add_repo_and_install_try"] = str(retry + 1)
754            sourcepart = trans.meta_data[
755                "sc_add_repo_and_install_sources_list"]
756            GLib.timeout_add_seconds(30, self._reload_for_commercial_repo,
757                                     app, trans.meta_data, sourcepart)
758
759    # internal helpers
760    def _on_lowlevel_transactions_changed(self, watcher, current, pending):
761        # cleanup progress signal (to be sure to not leave dbus
762        # matchers around)
763        if self._progress_signal:
764            GLib.source_remove(self._progress_signal)
765            self._progress_signal = None
766        # attach progress-changed signal for current transaction
767        if current:
768            try:
769                trans = client.get_transaction(current)
770                self._progress_signal = trans.connect("progress-changed",
771                    self._on_progress_changed)
772            except dbus.DBusException:
773                pass
774
775        # now update pending transactions
776        self.pending_transactions.clear()
777        for tid in [current] + pending:
778            if not tid:
779                continue
780            try:
781                trans = client.get_transaction(tid,
782                    error_handler=lambda x: True)
783            except dbus.DBusException:
784                continue
785            trans_progress = TransactionProgress(trans)
786            try:
787                self.pending_transactions[trans_progress.pkgname] = \
788                    trans_progress
789            except KeyError:
790                # if its not a transaction from us (sc_pkgname) still
791                # add it with the tid as key to get accurate results
792                # (the key of pending_transactions is never directly
793                #  exposed in the UI)
794                self.pending_transactions[trans.tid] = trans_progress
795        # emit signal
796        self.inject_fake_transactions_and_emit_changed_signal()
797
798    def inject_fake_transactions_and_emit_changed_signal(self):
799        """
800        ensures that the fake transactions are considered and emits
801        transactions-changed signal with the right pending transactions
802        """
803        # inject a bunch of FakePurchaseTransaction into the transactions dict
804        for pkgname in self.pending_purchases:
805            self.pending_transactions[pkgname] = \
806                self.pending_purchases[pkgname]
807        # and emit the signal
808        self.emit("transactions-changed", self.pending_transactions)
809
810    def _on_progress_changed(self, trans, progress):
811        """
812        internal helper that gets called on our package transaction progress
813        (only showing pkg progress currently)
814        """
815        try:
816            pkgname = trans.meta_data["sc_pkgname"]
817            self.pending_transactions[pkgname].progress = progress
818            self.emit("transaction-progress-changed", pkgname, progress)
819        except KeyError:
820            pass
821
822    def _show_transaction_failed_dialog(self, trans, enum,
823                                        alternative_action=None):
824        # daemon died are messages that result from broken
825        # cancel handling in aptdaemon (LP: #440941)
826        # FIXME: this is not a proper fix, just a workaround
827        if trans.error_code == enums.ERROR_DAEMON_DIED:
828            self._logger.warn("daemon dies, ignoring: %s %s" % (trans, enum))
829            return
830        # hide any private ppa details in the error message since it may
831        # appear in the logs for LP bugs and potentially in screenshots as well
832        cleaned_error_details = obfuscate_private_ppa_details(
833            trans.error_details)
834        msg = utf8("%s: %s\n%s\n\n%s") % (
835            utf8(_("Error")),
836            utf8(enums.get_error_string_from_enum(trans.error_code)),
837            utf8(enums.get_error_description_from_enum(trans.error_code)),
838            utf8(cleaned_error_details))
839        self._logger.error("error in _on_trans_finished '%s'" % msg)
840        # show dialog to the user and exit (no need to reopen the cache)
841        if not trans.error_code:
842            # sometimes aptdaemon doesn't return a value for error_code
843            # when the network connection has become unavailable; in
844            # that case, we will assume it's a failure during a package
845            # download because that is the only case where we see this
846            # happening - this avoids display of an empty error dialog
847            # and correctly prompts the user to check their network
848            # connection (see LP: #747172)
849            # FIXME: fix aptdaemon to return a valid error_code under
850            # all conditions
851            trans.error_code = enums.ERROR_PACKAGE_DOWNLOAD_FAILED
852        # show dialog to the user and exit (no need to reopen
853        # the cache)
854        res = self.ui.error(None,
855            utf8(enums.get_error_string_from_enum(trans.error_code)),
856            utf8(enums.get_error_description_from_enum(trans.error_code)),
857            utf8(cleaned_error_details),
858            utf8(alternative_action))
859        return res
860
861    def _get_app_and_icon_and_deb_from_trans(self, trans):
862        meta_copy = trans.meta_data.copy()
863        app = Application(meta_copy.pop("sc_appname", None),
864                          meta_copy.pop("sc_pkgname"))
865        iconname = meta_copy.pop("sc_iconname", None)
866        filename = meta_copy.pop("sc_filename", "")
867        return app, iconname, filename, meta_copy
868
869    def _on_trans_finished(self, trans, enum):
870        """callback when a aptdaemon transaction finished"""
871        self._logger.debug("_on_transaction_finished: %s %s %s" % (
872                trans, enum, trans.meta_data))
873
874        # first check if there has been a cancellation of
875        # the install and fire a transaction-cancelled signal
876        # (see LP: #1027209)
877        if enum == enums.EXIT_CANCELLED:
878            result = TransactionFinishedResult(trans, False)
879            self.emit("transaction-cancelled", result)
880            return
881
882        # show error
883        if enum == enums.EXIT_FAILED:
884            # Handle invalid packages separately
885            if (trans.error and
886                    trans.error.code == enums.ERROR_INVALID_PACKAGE_FILE):
887                action = _("_Ignore and install")
888                res = self._show_transaction_failed_dialog(
889                    trans, enum, action)
890                if res == "yes":
891                    # Reinject the transaction
892                    app, iconname, filename, meta_copy = \
893                        self._get_app_and_icon_and_deb_from_trans(trans)
894                    self.install(app, iconname, filename, [], [],
895                                 metadata=meta_copy, force=True)
896                    return
897            # on unauthenticated errors, try a "repair" using the
898            # reload functionality
899            elif (trans.error and
900                  trans.error.code == enums.ERROR_PACKAGE_UNAUTHENTICATED):
901                action = _("Repair")
902                res = self._show_transaction_failed_dialog(
903                    trans, enum, action)
904                if res == "yes":
905                    app, iconname, filename, meta_copy = \
906                        self._get_app_and_icon_and_deb_from_trans(trans)
907                    self.reload()
908                    self.install(app, iconname, filename, [], [],
909                                 metadata=meta_copy)
910                    return
911            # Finish a cancelled installation before resuming. If the
912            # user e.g. rebooted during a debconf question apt
913            # will hang and the user is required to call
914            # dpkg --configure -a, see LP#659438
915            elif (trans.error and
916                  trans.error.code == enums.ERROR_INCOMPLETE_INSTALL):
917                action = _("Repair")
918                res = self._show_transaction_failed_dialog(trans, enum,
919                                                           action)
920                if res == "yes":
921                    self.fix_incomplete_install()
922                    return
923
924            elif (not "sc_add_repo_and_install_ignore_errors" in
925                  trans.meta_data):
926                self._show_transaction_failed_dialog(trans, enum)
927
928        # send finished signal, use "" here instead of None, because
929        # dbus mangles a None to a str("None")
930        pkgname = ""
931        try:
932            pkgname = trans.meta_data["sc_pkgname"]
933            del self.pending_transactions[pkgname]
934            self.emit("transaction-progress-changed", pkgname, 100)
935        except KeyError:
936            pass
937        # if it was a cache-reload, trigger a-x-i update
938        if trans.role == enums.ROLE_UPDATE_CACHE:
939            if enum == enums.EXIT_SUCCESS:
940                self.update_xapian_index()
941            self.emit("reload-finished", trans, enum != enums.EXIT_FAILED)
942        # send appropriate signals
943        self.inject_fake_transactions_and_emit_changed_signal()
944        self.emit("transaction-finished", TransactionFinishedResult(trans,
945            enum != enums.EXIT_FAILED))
946
947    @inline_callbacks
948    def _config_file_conflict(self, transaction, old, new):
949        reply = self.ui.ask_config_file_conflict(old, new)
950        if reply == "replace":
951            yield transaction.resolve_config_file_conflict(old, "replace",
952                                                           defer=True)
953        elif reply == "keep":
954            yield transaction.resolve_config_file_conflict(old, "keep",
955                                                           defer=True)
956        else:
957            raise Exception(
958                "unknown reply: '%s' in _ask_config_file_conflict " % reply)
959
960    @inline_callbacks
961    def _medium_required(self, transaction, medium, drive):
962        res = self.ui.ask_medium_required(medium, drive)
963        if res:
964            yield transaction.provide_medium(medium, defer=True)
965        else:
966            yield transaction.cancel(defer=True)
967
968    @inline_callbacks
969    def _run_transaction(self, trans, pkgname, appname, iconname,
970                         metadata=None):
971        # connect signals
972        trans.connect("config-file-conflict", self._config_file_conflict)
973        trans.connect("medium-required", self._medium_required)
974        trans.connect("finished", self._on_trans_finished)
975        try:
976            # set appname/iconname/pkgname only if we actually have one
977            if appname:
978                yield trans.set_meta_data(sc_appname=appname, defer=True)
979            if iconname:
980                yield trans.set_meta_data(sc_iconname=iconname, defer=True)
981            # we do not always have a pkgname, e.g. "cache_update" does not
982            if pkgname:
983                # ensure the metadata is just the pkgname
984                sc_pkgname = pkgname.split("/")[0].split("=")[0]
985                yield trans.set_meta_data(sc_pkgname=sc_pkgname, defer=True)
986                # setup debconf only if we have a pkg
987                yield trans.set_debconf_frontend("gnome", defer=True)
988                trans.set_remove_obsoleted_depends(True, defer=True)
989                self._progress_signal = trans.connect("progress-changed",
990                    self._on_progress_changed)
991                self.pending_transactions[pkgname] = TransactionProgress(trans)
992            # generic metadata
993            if metadata:
994                yield trans.set_meta_data(defer=True, **metadata)
995            yield trans.run(defer=True)
996        except Exception as error:
997            self._on_trans_error(error, trans, pkgname)
998            # on error we need to clean the pending purchases
999            self._clean_pending_purchases(pkgname)
1000        # on success the pending purchase is cleaned when the package
1001        # that was purchased finished installing
1002        if trans.role == enums.ROLE_INSTALL_PACKAGES:
1003            self._clean_pending_purchases(pkgname)
1004
1005    def _clean_pending_purchases(self, pkgname):
1006        if pkgname and pkgname in self.pending_purchases:
1007            del self.pending_purchases[pkgname]
1008
1009    def _on_trans_error(self, error, trans, pkgname=None):
1010        self._logger.warn("_on_trans_error: '%r' '%s' '%s'" % (
1011                error, trans, pkgname))
1012        # re-enable the action button again if anything went wrong
1013        result = TransactionFinishedResult(None, False)
1014        result.pkgname = pkgname
1015
1016        # clean up pending transactions
1017        if pkgname and pkgname in self.pending_transactions:
1018            del self.pending_transactions[pkgname]
1019
1020        # calculate a dupes_signature here to have different buckets
1021        # on errors.ubuntu.com for the different crash types
1022        if trans:
1023            error_code = trans.error_code
1024        else:
1025            error_code = "no-transaction"
1026        dupes_signature = "software-center:trans-failed:%s" % error_code
1027
1028        self.emit("transaction-stopped", result)
1029        if isinstance(error, dbus.DBusException):
1030            # ignore errors that the user knows about already (like
1031            # that he entered a wrong password or that he does not
1032            # care about (like no-reply)
1033            name = error.get_dbus_name()
1034            if name in ["org.freedesktop.PolicyKit.Error.NotAuthorized",
1035                        "org.freedesktop.DBus.Error.NoReply"]:
1036                return
1037            # we want to give some advice here to the user but also
1038            # report this via apport
1039            if name in ["org.freedesktop.PolicyKit.Error.Failed"]:
1040                summary = _("Authentication Error")
1041                text = _("Software can't be installed or removed because "
1042                         "the authentication service is not available. "
1043                         "(%s") % error
1044                # send to apport for reporting
1045                self._call_apport_recoverable_error(
1046                    text, error, dupes_signature)
1047                # ... and display as a dialog
1048                self.ui.error(None, summary, text)
1049                return
1050
1051        # lintian errors are ignored and not send to apport_recoverable_error
1052        # and dpkg errors as well as they will already be recorded separately
1053        # by apt itself
1054        if error_code in (enums.ERROR_INVALID_PACKAGE_FILE,
1055                                enums.ERROR_PACKAGE_MANAGER_FAILED):
1056            return
1057
1058        # show a apport recoverable error dialog to the user as we want
1059        # to know about these issues
1060        self._call_apport_recoverable_error(
1061            _("There was an error submitting the transaction"),
1062            error,
1063            dupes_signature)
1064
1065    def _call_apport_recoverable_error(self, text, error, dupes_signature):
1066        """Call apport's recoverable_problem dialog """
1067
1068        # ensure we have a proper exception string in the report
1069        if isinstance(error, Exception):
1070            error = traceback.format_exc(error)
1071
1072        # mvo: I don't think we need to send "Package\0software-center",
1073        #      apport should figure this out itself
1074        data = ("DialogBody\0%(text)s\0"
1075                "Traceback\0%(error)s\0"
1076                "DuplicateSignature\0%(dupes_signature)s" % {
1077                'text': text,
1078                'error': error,
1079                'dupes_signature': dupes_signature,
1080                })
1081        # This will be quick as it just writes the report file. Then
1082        # the report gets picked up asynchronously by a inotify watch
1083        # and displayed to the user in a separate process.
1084        p = Popen(
1085            [APPORT_RECOVERABLE_ERROR], stdin=PIPE, stdout=PIPE, stderr=PIPE)
1086
1087        (stdout, stderr) = p.communicate(input=data)
1088        if p.returncode != 0:
1089            logging.warn("%s returned '%s' ('%s', '%s')" % (
1090                    APPORT_RECOVERABLE_ERROR, p.returncode, stdout, stderr))
1091
1092
1093if __name__ == "__main__":
1094    #c = client.AptClient()
1095    #c.remove_packages(["4g8"], remove_unused_dependencies=True)
1096    backend = AptdaemonBackend()
1097    #backend.reload()
1098    backend.enable_component("multiverse")
1099    from gi.repository import Gtk
1100    Gtk.main()
Note: See TracBrowser for help on using the repository browser.