diff options
Diffstat (limited to '')
-rwxr-xr-x | aptd | 41 | ||||
-rw-r--r-- | aptdaemon/__init__.py | 28 | ||||
-rw-r--r-- | aptdaemon/client.py | 1714 | ||||
-rw-r--r-- | aptdaemon/config.py | 243 | ||||
-rw-r--r-- | aptdaemon/console.py | 691 | ||||
-rw-r--r-- | aptdaemon/core.py | 2204 | ||||
-rw-r--r-- | aptdaemon/crash.py | 79 | ||||
-rw-r--r-- | aptdaemon/debconf.py | 180 | ||||
-rw-r--r-- | aptdaemon/enums.py | 718 | ||||
-rw-r--r-- | aptdaemon/errors.py | 235 | ||||
-rw-r--r-- | aptdaemon/gtk3widgets.py | 1206 | ||||
-rw-r--r-- | aptdaemon/lock.py | 198 | ||||
-rw-r--r-- | aptdaemon/logger.py | 77 | ||||
-rw-r--r-- | aptdaemon/loop.py | 33 | ||||
-rw-r--r-- | aptdaemon/networking.py | 267 | ||||
-rw-r--r-- | aptdaemon/pkenums.py | 969 | ||||
-rw-r--r-- | aptdaemon/pkutils.py | 46 | ||||
-rw-r--r-- | aptdaemon/policykit1.py | 175 | ||||
-rw-r--r-- | aptdaemon/progress.py | 829 | ||||
-rw-r--r-- | aptdaemon/test.py | 312 | ||||
-rw-r--r-- | aptdaemon/utils.py | 131 | ||||
-rw-r--r-- | aptdaemon/worker/__init__.py | 334 | ||||
-rw-r--r-- | aptdaemon/worker/aptworker.py | 1537 | ||||
-rw-r--r-- | aptdaemon/worker/pkworker.py | 1353 | ||||
-rwxr-xr-x | aptdcon | 30 |
25 files changed, 13630 insertions, 0 deletions
@@ -0,0 +1,41 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +aptd - apt daemon +""" +# Copyright (C) 2008 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" +__state__ = "experimental" + +import os +import sys + + +if __name__ == "__main__": + # Ensure that the default encoding is set since Python's setlocale doesn't + # allow to change it. This can be the case if D-Bus activation is used, + # see LP: #1058038 and http://bugs.python.org/issue16162 + if sys.getfilesystemencoding() == "ascii" and not "LANG" in os.environ: + os.environ["LANG"] = "C.UTF-8" + os.execv(sys.argv[0], sys.argv) + + import aptdaemon.core + + aptdaemon.core.main() + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/__init__.py b/aptdaemon/__init__.py new file mode 100644 index 0000000..6a32756 --- /dev/null +++ b/aptdaemon/__init__.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Transaction based daemon and clients for package management tasks.""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" +__state__ = "development" +__version__ = '1.1.1' + +__all__ = ("client", "console", "core", "debconf", "defer", "enums", + "errors", "gtk3widgets", "loop", "policykit1", "progress", + "test", "utils", "worker") + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/client.py b/aptdaemon/client.py new file mode 100644 index 0000000..6e411f2 --- /dev/null +++ b/aptdaemon/client.py @@ -0,0 +1,1714 @@ +#!/usr/bin/python +""" +The module provides a client to the PackageKit DBus interface. It allows to +perform basic package manipulation tasks in a cross distribution way, e.g. +to search for packages, install packages or codecs. +""" +# Copyright (C) 2008 Canonical Ltd. +# Copyright (C) 2008 Aidan Skinner <aidan@skinner.me.uk> +# Copyright (C) 2008 Martin Pitt <martin.pitt@ubuntu.com> +# Copyright (C) 2008 Tim Lauridsen <timlau@fedoraproject.org> +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# Licensed under the GNU General Public License Version 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +import locale +import os.path +import shutil +import weakref +import sys + +import dbus +import dbus.mainloop.glib +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + +from gi.repository import GObject, GLib + +from . import enums +from . import debconf +import defer +from defer.utils import deferable +from .errors import convert_dbus_exception, TransactionFailed + +__all__ = ("AptTransaction", "AptClient", "get_transaction", "get_aptdaemon") + + +# the default timeout for dbus method calls +_APTDAEMON_DBUS_TIMEOUT = 86400 + + +class AptTransaction(GObject.Object): + + """Represents an aptdaemon transaction. + + .. note:: This class cannot be inherited since it makes use of + a metaclass. + + .. signal:: allow-unauthenticated -> allow + + The signal is emitted when :attr:`allow_unauthenticated` changed. + + :param allow: If unauthenticated packages are allowed to be installed. + + .. signal:: cancellable-changed -> cancellable + + The signal is emitted when :attr:`cancellable` changed. + + :param cancellable: If the transaction can be cancelled now. + + .. signal:: config-file-conflict -> cur, new + + The signal is emitted when :attr:`config_file_conflict` changed. + + :param cur: The path to the current configuration file. + :param new: The path to the new configuration file. + + .. signal:: debconf-socket-changed -> path + + The signal is emitted when :attr:`debconf_socket` changed. + + :param path: The path to the socket which will be used to forward + debconf communication to the user session. + + .. signal:: dependencies-changed -> installs, re-installs, removals, \ + purges, upgrades, downgrades, kepts + + The signal is emitted when :attr:`dependencies` changed. + + Most likely after :meth:`simulate()` was called. + + :param installs: List of package which will be installed. + :param reinstalls: List of package which will be re-installed. + :param removals: List of package which will be removed, + :param purges: List of package which will be removed including + configuration files. + :param upgrades: List of package which will be upgraded. + :param downgrades: List of package which will be downgraded to an older + version. + :param kepts: List of package which will be skipped from upgrading. + + .. signal:: download-changed -> download + + The signal is emitted when :attr:`download` changed. + + :param download: Download size integer in Bytes. + + .. signal:: error -> error_code, error_details + + The signal is emitted when an error occured. + + :param error_code: The error code enumeration, e.g. + :data:`aptdaemon.enums.ERROR_NO_CACHE`. + :param error_details: The error description string. + + .. signal:: finished -> exit_state + + The signal is emitted when the transaction is completed or has + failed. + + :param exit_state: The exit status enumeration string. + + .. signal:: http-proxy-changed -> uri + + The signal is emitted when :attr:`http_proxy` changed. + + :param uri: The URI of the proxy server, e.g. "http://proxy:8080". + + .. signal:: locale-changed -> locale + + The signal is emitted when :attr:`locale` changed. + + :param locale: The language which should be used for messages, + eg. "de_DE". + + .. signal:: meta-data-changed -> meta_data + + The signal is emitted when :attr:`meta_data` changed. + + :param meta_data: The latest meta data dictionary. + + .. signal:: medium-required -> name, device + + The signal is emitted when :attr:`required_medium` changed. + + :param name: The name of the volume. + :param device: The path of the device in which the volume should + be inserted. + + .. signal:: remove-obsoleted-depends-changed -> remove + + The signal is emitted when :attr:`remove_obsoleted_depends` changed. + + :param remove: If obsolete dependencies should also be removed. + + .. signal:: role-changed -> role + + The signal is emitted when :attr:`role` changed. + + :param role: The new role enum, e.g. + :data:`~aptdaemon.enums.ROLE_UPDATE_CACHE`. + + .. signal:: space-changed -> space + + The signal is emitted when :attr:`space` changed. + Most likely after :meth:`simulate()` was called. + + :param space: Required disk space integer in Bytes. Can be negative + if disk space will be freed. + + .. signal:: packages-changed -> installs, re-installs, removals, \ + purges, upgrades, downgrades + + The signal is emitted when :attr:`packages` changed. + + :param installs: List of package which will be installed. + :param reinstalls: List of package which will be re-installed. + :param removals: List of package which will be removed, + :param purges: List of package which will be removed including + configuration files. + :param upgrades: List of package which will be upgraded. + :param downgrades: List of package which will be downgraded to an older + version. + + .. signal:: paused + + The signal is emitted when the transaction was paused. + See :attr:`paused` and :sig:`resumed`. + + .. signal:: progress-changed -> progress + + The signal is emitted when :attr:`progress` changed. + + :param progress: The progress integer. + + .. signal:: progress-details-changed -> current_items, total_items, \ + currenty_bytes, total_bytes, \ + current_cps, eta + + The signal is emitted when detailed information of the progress + is available. + + :param current_items: The number of already processed items. + :param total_items: The number of all items. + :param current_bytes: The number of already downloaded byte. + :param total_bytes: The number of bytes which have to be downloaded + totally. + :param current_cps: The current download speed in bytes per second. + :param eta: The elapsed time in seconds to accomplish the task. + + .. signal:: progress-download-changed -> uri, short_desc, total_size, \ + current_size, msg + + The signal is emitted when progress information about a single + download is available. + + :param uri: The URI of the file which is downloaded. + :param status: The status of the downloade, e.g. + :data:`~aptdaemon.enums.DOWNLOAD_AUTH_FAILED`. + :param short_desc: A short description of the file. + :param total_size: The size of the file in Bytes. + :param current_size: How much of the file in Bytes has already be + downloaded. + :param msg: The status or error description. + + .. signal:: resumed + + The signal is emitted when a paused transaction was resumed. + See :attr:`paused` and :sig:`paused`. + + .. signal:: terminal-changed -> path + + The signal is emitted when :attr:`terminal` changed. + + :param path: The path to the slave end of the controlling terminal + for the underlying dpkg call. + + .. signal:: terminal-attached-changed -> attached + + The signal is emitted when :attr:`term_attached` changed. + + :param attached: If the controlling terminal can be used. + + .. signal:: unauthenticated-changed -> unauthenticated + + The signal is emitted when :attr:`unauthenticated` changed. + + :param unauthenticated: List of unauthenticated packages. + + .. attribute:: cancellable + + If the transaction can be currently cancelled. + + .. attribute:: config_file_conflict + + If there is a conflict in the configuration file handling during + an installation this attribute contains a tuple of the path to the + current and the new temporary configuration file. + + The :meth:`resolve_config_file_conflict()` can be used to + resolve the conflict and continue the processing of the + transaction. + + .. attribute:: dependencies + + List of dependencies lists in the following order: packages to + install, to re-install, to remove, to purge, to upgrade, + to downgrade and to keep. + + You have to call :meth:`simulate()` to calculate the + dependencies before the transaction will be executed. + + .. attribute:: download + + The number of Bytes which have to be downloaed. + + You have to call :meth:`simulate()` to calculate the + download size before the transaction will be executed. + + .. attribute:: error + + In the case of a failed transaction this attribute holds the + corresponding :exc:`errors.TransactionFailed` instance. + + .. attribute:: error_code + + In the case of a failed transaction this attribute is set to the + underlying error code, e.g. + :data:`enums.ERROR_PACKAGE_DOWNLOAD_FAILED`. + + .. attribute:: error_details + + In the case of a failed transaction this attribute contains a + detailed error message in the language of the transaction. + + .. attribute:: exit + + Contains the exit status enum if the transaction has been completed, + e.g. :data:`enums.EXIT_SUCCESS` or :data:`enums.EXIT_FAILED`. + + .. attribute:: http_proxy + + The URI to the http proxy server which should be used only for this + transaction, e.g. "http://proxy:8080". It is recommended to set + the system wide proxy server instead of setting this attribute + for every transaction. + + See :meth:`set_http_proxy()`. + + .. attribute:: meta_data + + Dictionary of optional meta data which can be set by client + applications. See :meth:`set_meta_data()`. + + .. attribute:: packages + + List of package lists which will be explicitly changed in the + following order: packages to install, to re-install, to remove, + to purge, to upgrade, to downgrade. + + .. attribute:: paused + + If the transaction is currently paused, e.g. it is required to + insert a medium to install from. + + .. attribute:: progress + + An integer ranging from 0 to 101 to describe the progress of the + transaction. + + .. note:: A value of 101 indicates that there cannot be made any + assumptions on the progress of the transaction. + + .. attribute:: remove_obsoleted_depends + + If dependencies which have been required by a removed package only + should be removed, too. + + .. attribute:: required_medium + + If a medium should be inserted to continue the fetch phase of a + transaction, this attribute contains a tuple of the device path of + of the drive which should be used and secondly of the name of the + medium. + + The :func:`provide_medium()` method should be used to notify aptdaemon + about an inserted medium and to continue processing the transaction. + + .. attribute:: role + + The kind of action which is performed by the transaction, e.g. + :data:`enums.ROLE_UPGRADE_SYSTEM`. + + .. attribute:: space + + The required disk space in Bytes. Will be negative if space is + freed. + + You have to call :meth:`simulate()` to calculate the + download size before the transaction will be executed. + + .. attribute:: status + + The enum of the current status, e.g. + :data:`enums.STATUS_DOWNLOADING`. + + .. attribute:: status_details + + A string describing the current status of the transaction. + + .. attribute:: tid + + The unique identifier of the transaction. It is also the D-Bus path + of the corresponding transaction object. + + .. attribute:: term_attached + + If the the package manager can be controlled using the controlling + terminal specified by :func:`set_terminal()`. + + .. attribute:: unauthenticated + + List of packages which are going to be installed but are not + downloaded from an authenticated repository. + + You have to call :meth:`simulate()` to calculate the + dependencies before the transaction will be executed. + """ + + __gsignals__ = {"finished": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "dependencies-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT)), + "download-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_INT64,)), + "space-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_INT64,)), + "error": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING, GObject.TYPE_STRING)), + "role-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "terminal-attached-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_BOOLEAN,)), + "cancellable-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_BOOLEAN,)), + "meta-data-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_PYOBJECT,)), + "status-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "status-details-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "progress-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_INT,)), + "progress-details-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_INT, + GObject.TYPE_INT, + GObject.TYPE_INT64, + GObject.TYPE_INT64, + GObject.TYPE_INT, + GObject.TYPE_INT64)), + "progress-download-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING, + GObject.TYPE_STRING, + GObject.TYPE_STRING, + GObject.TYPE_INT64, + GObject.TYPE_INT64, + GObject.TYPE_STRING)), + "packages-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT, + GObject.TYPE_PYOBJECT)), + "unauthenticated-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_PYOBJECT,)), + "paused": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + ()), + "resumed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + ()), + "allow-unauthenticated-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_BOOLEAN,)), + "remove-obsoleted-depends-changed": ( + GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_BOOLEAN,)), + "locale-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "terminal-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "debconf-socket-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "http-proxy-changed": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING,)), + "medium-required": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING, + GObject.TYPE_STRING)), + "config-file-conflict": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, + (GObject.TYPE_STRING, + GObject.TYPE_STRING)), + } + + _tid_cache = weakref.WeakValueDictionary() + + def __new__(cls, tid, *args, **kwargs): + """Cache transactions with identical tid.""" + try: + return AptTransaction._tid_cache[tid] + except KeyError: + value = GObject.Object.__new__(cls, tid, *args, **kwargs) + AptTransaction._tid_cache[tid] = value + return value + + def __init__(self, tid, bus=None): + GObject.GObject.__init__(self) + self.tid = tid + self.role = enums.ROLE_UNSET + self.error = None + self.error_code = None + self.error_details = None + self.exit = enums.EXIT_UNFINISHED + self.cancellable = False + self.term_attached = False + self.required_medium = None + self.config_file_conflict = None + self.status = None + self.status_details = "" + self.progress = 0 + self.paused = False + self.http_proxy = None + self.dependencies = [[], [], [], [], [], [], []] + self.packages = [[], [], [], [], []] + self.unauthenticated = [] + self.meta_data = {} + self.remove_obsoleted_depends = False + self.download = 0 + self.downloads = {} + self.space = 0 + self.locale = "" + self._method = None + self._args = [] + self._debconf_helper = None + # Connect the signal handlers to the DBus iface + if not bus: + bus = dbus.SystemBus() + self._proxy = bus.get_object("org.debian.apt", tid) + self._iface = dbus.Interface(self._proxy, "org.debian.apt.transaction") + # Watch for a crashed daemon which orphaned the dbus object + self._owner_watcher = bus.watch_name_owner("org.debian.apt", + self._on_name_owner_changed) + # main signals + self._signal_matcher = \ + self._iface.connect_to_signal("PropertyChanged", + self._on_property_changed) + + def _on_name_owner_changed(self, connection): + """Fail the transaction if the daemon died.""" + if connection == "" and self.exit == enums.EXIT_UNFINISHED: + self._on_property_changed("Error", (enums.ERROR_DAEMON_DIED, + "It seems that the daemon " + "died.")) + self._on_property_changed("Cancellable", False) + self._on_property_changed("TerminalAttached", False) + self._on_property_changed("ExitState", enums.EXIT_FAILED) + + def _on_property_changed(self, property_name, value): + """Callback for the PropertyChanged signal.""" + if property_name == "TerminalAttached": + self.term_attached = value + self.emit("terminal-attached-changed", value) + elif property_name == "Cancellable": + self.cancellable = value + self.emit("cancellable-changed", value) + elif property_name == "DebconfSocket": + self.emit("debconf-socket-changed", value) + elif property_name == "RemoveObsoletedDepends": + self.emit("remove-obsoleted-depends-changed", value) + self.remove_obsoleted_depends = value + elif property_name == "AllowUnauthenticated": + self.emit("allow-unauthenticated-changed", value) + elif property_name == "Terminal": + self.emit("terminal-changed", value) + elif property_name == "Dependencies": + self.dependencies = value + self.emit("dependencies-changed", *value) + elif property_name == "Packages": + self.packages = value + self.emit("packages-changed", *value) + elif property_name == "Unauthenticated": + self.unauthenticated = value + self.emit("unauthenticated-changed", value) + elif property_name == "Locale": + self.locale = value + self.emit("locale-changed", value) + elif property_name == "Role": + self.role = value + self.emit("role-changed", value) + elif property_name == "Status": + self.status = value + self.emit("status-changed", value) + elif property_name == "StatusDetails": + self.status_details = value + self.emit("status-details-changed", value) + elif property_name == "ProgressDownload": + uri, status, desc, size, download, msg = value + if uri: + self.downloads[uri] = (status, desc, size, download, msg) + self.emit("progress-download-changed", *value) + elif property_name == "Progress": + self.progress = value + self.emit("progress-changed", value) + elif property_name == "ConfigFileConflict": + self.config_file_conflict = value + if value != ("", ""): + self.emit("config-file-conflict", *value) + elif property_name == "MetaData": + self.meta_data = value + self.emit("meta-data-changed", value) + elif property_name == "Paused": + self.paused = value + if value: + self.emit("paused") + else: + self.emit("resumed") + elif property_name == "RequiredMedium": + self.required_medium = value + if value != ("", ""): + self.emit("medium-required", *value) + elif property_name == "ProgressDetails": + self.emit("progress-details-changed", *value) + elif property_name == "Download": + self.download = value + self.emit("download-changed", value) + elif property_name == "Space": + self.space = value + self.emit("space-changed", value) + elif property_name == "HttpProxy": + self.http_proxy = value + self.emit("http-proxy-changed", value) + elif property_name == "Error": + self.error_code, self.error_details = value + if self.error_code != "": + self.error = TransactionFailed(self.error_code, + self.error_details) + self.emit("error", *value) + elif property_name == "ExitState": + if value != enums.EXIT_UNFINISHED and value != self.exit: + self.exit = value + if self._debconf_helper: + self._debconf_helper.stop() + self._disconnect_from_dbus() + # Finally sync all properties a last time. We cannot ensure + # that the ExitState signal is the last one, so some + # other PropertyChanged signals could be lost, see LP#747172 + self.sync(reply_handler=self._on_final_sync_done, + error_handler=self._on_final_sync_done) + + def _on_final_sync_done(self, data): + self._owner_watcher.cancel() + self.emit("finished", self.exit) + + @deferable + @convert_dbus_exception + def sync(self, reply_handler=None, error_handler=None): + """Sync the properties of the transaction with the daemon. + + This method is called automatically on the creation of the + AptTransaction instance. + + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + """ + def sync_properties(prop_dict): + for property_name, value in prop_dict.items(): + self._on_property_changed(property_name, value) + if reply_handler: + reply_handler(self) + if reply_handler and error_handler: + self._proxy.GetAll("org.debian.apt.transaction", + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=sync_properties, + error_handler=error_handler) + else: + properties = self._proxy.GetAll( + "org.debian.apt.transaction", + dbus_interface=dbus.PROPERTIES_IFACE) + sync_properties(properties) + + @deferable + @convert_dbus_exception + def run_after(self, transaction, reply_handler=None, error_handler=None): + """Chain this transaction after the given one. The transaction will + fail if the previous one fails. + + To start processing of the chain you have to call :meth:`run()` + of the first transaction. The others will be queued after it + automatically. + + :param transaction: An AptTransaction on which this one depends. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + """ + try: + return self._iface.RunAfter(transaction.tid, + error_handler=error_handler, + reply_handler=reply_handler, + timeout=_APTDAEMON_DBUS_TIMEOUT) + except Exception as error: + if error_handler: + error_handler(error) + else: + raise + + @deferable + @convert_dbus_exception + def run(self, reply_handler=None, error_handler=None): + """Queue the transaction for processing. + + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.TransactionFailed, dbus.DBusException + """ + try: + return self._iface.Run(error_handler=error_handler, + reply_handler=reply_handler, + timeout=_APTDAEMON_DBUS_TIMEOUT) + except Exception as error: + if error_handler: + error_handler(error) + else: + raise + + @deferable + @convert_dbus_exception + def simulate(self, reply_handler=None, error_handler=None): + """Simulate the transaction to calculate the dependencies, the + required download size and the required disk space. + + The corresponding properties of the AptTransaction will be updated. + + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.TransactionFailed, dbus.DBusException + """ + self._iface.Simulate(reply_handler=reply_handler, + error_handler=error_handler) + + @deferable + @convert_dbus_exception + def cancel(self, reply_handler=None, error_handler=None): + """Cancel the running transaction. + + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.NotAuthorizedError, dbus.DBusException + """ + self._iface.Cancel(reply_handler=reply_handler, + error_handler=error_handler) + + @deferable + @convert_dbus_exception + def set_http_proxy(self, proxy, reply_handler=None, error_handler=None): + """Use the given http proxy for downloading packages in this + transaction. + + :param proxy: The URL of the proxy server, e.g. "http://proxy:8080" + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.NotAuthorizedError, dbus.DBusException + aptdaemon.errors.ForeignTransaction, + """ + if reply_handler: + _reply_handler = lambda: reply_handler(self) + else: + _reply_handler = None + self._proxy.Set("org.debian.apt.transaction", "HttpProxy", proxy, + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=_reply_handler, + error_handler=error_handler) + + @deferable + @convert_dbus_exception + def set_remove_obsoleted_depends(self, remove_obsoleted_depends, + reply_handler=None, error_handler=None): + """Include no longer required dependencies which have been installed + automatically when removing packages. + + :param remove_obsoleted_depends: If obsolete dependencies should be + also removed. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.ForeignTransaction, dbus.DBusException + """ + if reply_handler: + _reply_handler = lambda: reply_handler(self) + else: + _reply_handler = None + self._proxy.Set("org.debian.apt.transaction", + "RemoveObsoletedDepends", remove_obsoleted_depends, + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=_reply_handler, + error_handler=error_handler) + + @deferable + @convert_dbus_exception + def set_allow_unauthenticated(self, allow_unauthenticated, + reply_handler=None, error_handler=None): + """Allow to install unauthenticated packages. + + Unauthenticated packages are from the repository of a vendor whose + key hasn't been installed. By default this is not allowed. + + :param allow_unauthenticated: If unauthenticated packages can be + installed. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.ForeignTransaction, dbus.DBusException + """ + if reply_handler: + _reply_handler = lambda: reply_handler(self) + else: + _reply_handler = None + self._proxy.Set("org.debian.apt.transaction", + "AllowUnauthenticated", allow_unauthenticated, + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=_reply_handler, + error_handler=error_handler) + + @deferable + @convert_dbus_exception + def set_debconf_frontend(self, frontend, reply_handler=None, + error_handler=None): + """Setup a debconf frontend to answer questions of the maintainer + scripts. + + Debian allows packages to interact with the user during installation, + configuration and removal phase via debconf. Aptdaemon forwards the + communication to a debconf instance running as the user of the + client application. + + :param frontend: The name of the debconf frontend which should be + launched, e.g. gnome or kde. Defaults to gnome. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.ForeignTransaction, dbus.DBusException + """ + if reply_handler: + _reply_handler = lambda: reply_handler(self) + else: + _reply_handler = None + + pk_socket = "/run/user/%d/pk-debconf-socket" % os.getuid() + if os.path.exists(pk_socket): + self._proxy.Set("org.debian.apt.transaction", "DebconfSocket", + pk_socket, + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=_reply_handler, + error_handler=error_handler) + return + + self._debconf_helper = debconf.DebconfProxy(frontend) + self._proxy.Set("org.debian.apt.transaction", "DebconfSocket", + self._debconf_helper.socket_path, + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=_reply_handler, + error_handler=error_handler) + self._debconf_helper.start() + + @deferable + @convert_dbus_exception + def set_meta_data(self, **kwargs): + """Store additional meta information of the transaction in the + MetaData property of the transaction. + + The method accepts key=value pairs. The key has to be prefixed with + an underscore separated identifier of the client application. + + In the following example Software-Center sets an application name + and icon: + + >>> Transaction.set_meta_data(sc_icon="shiny", sc_app="xterm") + + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.ForeignTransaction, dbus.DBusException + """ + reply_handler = kwargs.pop("reply_handler", None) + error_handler = kwargs.pop("error_handler", None) + if reply_handler: + _reply_handler = lambda: reply_handler(self) + else: + _reply_handler = None + meta_data = dbus.Dictionary(kwargs, signature="sv") + self._proxy.Set("org.debian.apt.transaction", "MetaData", meta_data, + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=_reply_handler, + error_handler=error_handler) + + @deferable + @convert_dbus_exception + def set_terminal(self, ttyname, reply_handler=None, error_handler=None): + """Allow to set a controlling terminal for the underlying dpkg call. + + See the source code of gtk3widgets.AptTerminal or console.ConsoleClient + as example. + + >>> master, slave = pty.openpty() + >>> transaction.set_terminal(os.ttyname(slave)) + + :param terminal: The slave end of a tty. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.ForeignTransaction, dbus.DBusException + """ + if reply_handler: + _reply_handler = lambda: reply_handler(self) + else: + _reply_handler = None + self._proxy.Set("org.debian.apt.transaction", "Terminal", ttyname, + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=_reply_handler, + error_handler=error_handler) + + def _disconnect_from_dbus(self): + """Stop monitoring the progress of the transaction.""" + if hasattr(self, "_signal_matcher"): + self._signal_matcher.remove() + del self._signal_matcher + + @deferable + @convert_dbus_exception + def set_locale(self, locale_name, reply_handler=None, error_handler=None): + """Set the language for status and error messages. + + :param locale: The locale name, e.g. de_DE@UTF-8. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.ForeignTransaction, dbus.DBusException + """ + if reply_handler: + _reply_handler = lambda: reply_handler(self) + else: + _reply_handler = None + self._proxy.Set("org.debian.apt.transaction", "Locale", locale_name, + dbus_interface=dbus.PROPERTIES_IFACE, + reply_handler=_reply_handler, + error_handler=error_handler) + + @deferable + @convert_dbus_exception + def provide_medium(self, medium, reply_handler=None, error_handler=None): + """Continue a paused transaction which waits for a medium to install + from. + + :param medium: The name of the provided medium. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.ForeignTransaction, dbus.DBusException + """ + self._iface.ProvideMedium(medium, reply_handler=reply_handler, + error_handler=error_handler) + + @deferable + @convert_dbus_exception + def resolve_config_file_conflict(self, config, answer, reply_handler=None, + error_handler=None): + """Continue a paused transaction which waits for the resolution of a + configuration file conflict. + + :param config: The path to the current version of the configuration + file. + :param answer: Can be either "keep" or "replace". + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: aptdaemon.errors.ForeignTransaction, dbus.DBusException + """ + self._iface.ResolveConfigFileConflict(config, answer, + reply_handler=reply_handler, + error_handler=error_handler) + + +class AptClient(object): + + """Provides a complete client for aptdaemon.""" + + def __init__(self, bus=None): + """Return a new AptClient instance.""" + if bus: + self.bus = bus + else: + self.bus = dbus.SystemBus() + # Catch an invalid locale + try: + self._locale = "%s.%s" % locale.getdefaultlocale() + except ValueError: + self._locale = None + self.terminal = None + + @convert_dbus_exception + def get_trusted_vendor_keys(self, reply_handler=None, error_handler=None): + """Get the list of the installed vendor keys which are used to + authenticate packages. + + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: Fingerprints of all installed vendor keys. + """ + daemon = get_aptdaemon(self.bus) + keys = daemon.GetTrustedVendorKeys(reply_handler=reply_handler, + error_handler=error_handler) + return keys + + @deferable + @convert_dbus_exception + def upgrade_system(self, safe_mode=True, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to apply all avaibale upgrades. + + :param safe_mode: If True only already installed packages will be + updated. Updates which require to remove installed packages or to + install additional packages will be skipped. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a + defer.Deferred. This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("UpgradeSystem", [safe_mode], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def install_packages(self, package_names, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to install the given packages from the + reporitories. + + The version number and target release of the packages can be specified + using the traditional apt-get syntax, e.g. "xterm=281.1" to force + installing the version 281.1 of xterm or "xterm/experimental" to + force installing xterm from the experimental release. + + :param package_names: List of names of the packages which should be + installed. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a + defer.Deferred. This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("InstallPackages", [package_names], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def add_repository(self, src_type, uri, dist, comps=None, comment="", + sourcesfile="", wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to enable a repository. + + :param src_type: The type of the repository (deb, deb-src). + :param uri: The main repository URI + (e.g. http://archive.ubuntu.com/ubuntu) + :param dist: The distribution to use (e.g. stable or lenny-backports). + :param comps: List of components (e.g. main, restricted). + :param comment: A comment which should be added to the sources.list. + :param sourcesfile: (Optoinal) filename in sources.list.d. + + :param wait: if True run the transaction immediately and return + its exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + # dbus can not deal with empty lists and will error + if not comps: + comps = [""] + return self._run_transaction("AddRepository", + [src_type, uri, dist, comps, comment, + sourcesfile], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def add_vendor_key_from_keyserver(self, keyid, keyserver, wait=False, + reply_handler=None, error_handler=None): + """Create a new transaction to download and install the key of a + software vendor. The key is used to authenticate packages of the + vendor. + + :param keyid: The id of the GnuPG key (e.g. 0x0EB12F05) + :param keyserver: The server to get the key from (e.g. + keyserver.ubuntu.com) + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("AddVendorKeyFromKeyserver", + [keyid, keyserver], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def add_vendor_key_from_file(self, path, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to install the key file of a software + vendor. The key is used to authenticate packages of the vendor. + + :param path: The absolute path to the key file. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("AddVendorKeyFromFile", [path], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def remove_vendor_key(self, fingerprint, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to remove the key of a software vendor + from the list of trusted ones. + + The key is used to authenticate the origin of packages. + + :param fingerprint: The fingerprint of the key. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("RemoveVendorKey", [fingerprint], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def install_file(self, path, force=False, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to install a local package file. + + :param path: The absolute path to the .deb-file. + :param force: Force the installation of a .deb-file even if it + violates the quality standard defined in the packaging policy. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + # Root is not allowed to access FUSE file systems. So copy files + # to the local system. + # FIXME: the locally cached one should be removed afterwards + home = os.getenv("HOME", None) + if home and path.startswith(os.path.join(home, ".gvfs")): + shutil.copy(path, "/tmp") + path = os.path.join("/tmp", os.path.basename(path)) + return self._run_transaction("InstallFile", [path, force], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def upgrade_packages(self, package_names, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to upgrade installed packages. + + The version number and target release of the packages can be specified + using the traditional apt-get syntax, e.g. "xterm=281.1" to force + installing the version 281.1 of xterm or "xterm/experimental" to + force installing xterm from the experimental release. + + :param package_names: The list of package which should be upgraded. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("UpgradePackages", [package_names], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def remove_packages(self, package_names, wait=False, + reply_handler=None, error_handler=None): + """Create a new transaction to remove installed packages. + + :param package_names: The list of packages which should be removed. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("RemovePackages", [package_names], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def commit_packages(self, install, reinstall, remove, purge, upgrade, + downgrade, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to perform a complex package management + task which allows to install, remove, upgrade or downgrade several + packages at the same time. + + The version number and target release of the packages can be specified + using the traditional apt-get syntax, e.g. "xterm=281.1" to force + installing the version 281.1 of xterm or "xterm/experimental" to + force installing xterm from the experimental release. + + :param install: List of packages to install. + :param reinstall: List of packages to re-install. + :param remove: List of packages to remove. + :param purge: List of packages to purge. + :param upgrade: List of packages to upgrade. + :param downgrade: List of packages to downgrade. The version of the + package has to be appended to the name separated by a "=", e.g. + "xterm=272-1". + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + def check_empty_list(lst): + if not lst: + return [""] + else: + return lst + pkgs = [check_empty_list(lst) for lst in [install, reinstall, remove, + purge, upgrade, downgrade]] + return self._run_transaction("CommitPackages", pkgs, + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def fix_broken_depends(self, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to fix unsatisfied dependencies of + already installed packages. + + Corresponds to the ``apt-get -f install`` call. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("FixBrokenDepends", [], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def reconfigure(self, packages, priority="default", + wait=False, reply_handler=None, error_handler=None): + """Create a new transaction to reconfigure already installed packages. + + Corresponds to the ``dpkg-reconfigure`` call. + + :param packages: List of package names which should be reconfigured. + :param priority: The minimum priority of question that will be + displayed. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("Reconfigure", [packages, priority], wait, + reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def fix_incomplete_install(self, wait=False, reply_handler=None, + error_handler=None): + """Create a new transaction to complete a previous interrupted + installation. + + Corresponds to the ``dpkg --confgiure -a`` call. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("FixIncompleteInstall", [], wait, + reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def update_cache(self, sources_list=None, wait=False, + reply_handler=None, error_handler=None): + """Create a new transaction to update the package cache. + + The repositories will be queried for installable packages. + + :param sources_list: Path to a sources.list which contains repositories + that should be updated only. The other repositories will + be ignored in this case. Can be either the file name of a snippet + in /etc/apt/sources.list.d or an absolute path. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + if sources_list: + return self._run_transaction("UpdateCachePartially", + [sources_list], wait, + reply_handler, error_handler) + else: + return self._run_transaction("UpdateCache", [], wait, + reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def enable_distro_component(self, component, wait=False, + reply_handler=None, error_handler=None): + """Create a new transaction to enable the component of the + distribution repository. + + :param component: The name of the component, e.g. main or universe. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("EnableDistroComponent", [component], + wait, reply_handler, error_handler) + + @deferable + @convert_dbus_exception + def clean(self, wait=False, reply_handler=None, error_handler=None): + """Remove all downloaded files. + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("Clean", [], wait, reply_handler, + error_handler) + + @deferable + @convert_dbus_exception + def add_license_key(self, pkg_name, json_token, server_name, wait=False, + reply_handler=None, error_handler=None): + """Install a license key to use a piece of proprietary software. + + :param pkg_name: The package which requires the license + :param json_token: The oauth token in json format + :param server_name: The server name (ubuntu-procduction, + ubuntu-staging) + + :param wait: if True run the transaction immediately and return its + exit state instead of the transaction itself. + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + return self._run_transaction("AddLicenseKey", + [pkg_name, json_token, server_name], + wait, reply_handler, + error_handler) + + def _run_transaction(self, method_name, args, wait, reply_handler, + error_handler): + async_ = reply_handler and error_handler + try: + deferred = self._run_transaction_helper(method_name, args, wait, + async_) + except Exception as error: + if async_: + error_handler(error) + return + else: + raise + if async_: + def on_error(error): + """Convert the DeferredException to a normal exception.""" + try: + error.raise_exception() + except Exception as error: + error_handler(error) + deferred.add_callbacks(reply_handler) + deferred.add_errback(on_error) + return deferred + else: + # Iterate on the main loop - we cannot use a sub loop here, + # since the D-Bus python bindings only work on the main loop + context = GLib.main_context_default() + while not hasattr(deferred, "result"): + context.iteration(True) + # If there has been an error in the helper raise it + if isinstance(deferred.result, defer.DeferredException): + deferred.result.raise_exception() + trans = deferred.result + if trans.error: + raise trans.error + if wait: + # Wait until the transaction is complete and the properties + # of the transaction have been updated + while trans.exit == enums.EXIT_UNFINISHED: + context.iteration(True) + return trans.exit + else: + return trans + + @defer.inline_callbacks + def _run_transaction_helper(self, method_name, args, wait, async_): + daemon = get_aptdaemon(self.bus) + dbus_method = daemon.get_dbus_method(method_name) + if async_: + deferred = defer.Deferred() + dbus_method(reply_handler=deferred.callback, + error_handler=deferred.errback, *args, + timeout=_APTDAEMON_DBUS_TIMEOUT) + tid = yield deferred + else: + tid = dbus_method(*args, timeout=_APTDAEMON_DBUS_TIMEOUT) + trans = AptTransaction(tid, self.bus) + if self._locale: + yield trans.set_locale(self._locale) + if self.terminal: + yield trans.set_terminal(self.terminal) + yield trans.sync() + if wait and async_: + deferred_wait = defer.Deferred() + sig = trans.connect("finished", + lambda trans, exit: + (exit != enums.EXIT_UNFINISHED and + deferred_wait.callback(exit))) + yield trans.run() + yield deferred_wait + GLib.source_remove(sig) + defer.return_value(trans.exit) + elif wait: + yield trans.run() + defer.return_value(trans) + + +@deferable +@convert_dbus_exception +def get_transaction(tid, bus=None, reply_handler=None, error_handler=None): + """Get an existing transaction by its identifier. + + :param tid: The identifer and D-Bus path of the transaction + e.g. /org/debian/apt/transaction/78904e5f9fa34098879e768032789109 + :param bus: Optionally the D-Bus on which aptdaemon listens. Defaults + to the system bus. + + :param reply_handler: Callback function. If specified in combination + with error_handler the method will be called asynchrounsouly. + :param error_handler: Errback function. In case of an error the given + callback gets the corresponding exception instance. + :param defer: Run the method asynchrounsly and return a defer.Deferred. + This options is only available as a keyword. + + :raises: dbus.DBusException + + :returns: An AptTransaction instance. + """ + if not bus: + bus = dbus.SystemBus() + trans = AptTransaction(tid, bus) + if error_handler and reply_handler: + trans.sync(reply_handler=reply_handler, error_handler=error_handler) + else: + trans.sync() + return trans + + +def get_size_string(bytes): + """Returns a human friendly string for a given byte size. + + Note: The bytes are skipped from the returned unit: 1024 returns 1K + """ + for unit in ["", "K", "M", "G"]: + if bytes < 1024.0: + return "%3.1f%s" % (bytes, unit) + bytes /= 1024.0 + return "%3.1f%s" % (bytes, "T") + + +def get_aptdaemon(bus=None): + """Get the daemon D-Bus interface. + + :param bus: Optionally the D-Bus on which aptdaemon listens. Defaults + to the system bus. + + :raises: dbus.DBusException + + :returns: An dbus.Interface instance. + """ + if not bus: + bus = dbus.SystemBus() + return dbus.Interface(bus.get_object("org.debian.apt", + "/org/debian/apt", + False), + "org.debian.apt") + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/config.py b/aptdaemon/config.py new file mode 100644 index 0000000..5e1219a --- /dev/null +++ b/aptdaemon/config.py @@ -0,0 +1,243 @@ +"""Handling configuration files.""" +# Copyright (C) 2010 Sebastian Heinlein <sevel@glatzor.de> +# +# Licensed under the GNU General Public License Version 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("ConfigWriter",) + +import logging +import os + +import apt_pkg + +log = logging.getLogger("AptDaemon.ConfigWriter") + + +class Value(object): + + """Represents a value with position information. + + .. attribute:: string + The value string. + + .. attribute:: line + The line number of the configuration file in which the value is set. + + .. attribute:: start + The position in the line at which the value starts. + + .. attribute:: end + The position in the line at which the value ends. + + .. attribute:: quotes + The outer qoutes of the value: ' or " + """ + + def __init__(self, line, start, quotes): + self.string = "" + self.start = start + self.end = None + self.line = line + self.quotes = quotes + + def __cmp__(self, other): + return self.string == other + + def __repr__(self): + return "Value: '%s' (line %s: %s to %s)" % (self.string, self.line, + self.start, self.end) + + +class ConfigWriter(object): + + """Modifies apt configuration files.""" + + def parse(self, lines): + """Parse an ISC based apt configuration. + + :param lines: The list of lines of a configuration file. + + :returns: Dictionary of key, values found in the parsed configuration. + """ + options = {} + in_comment = False + in_value = False + prev_char = None + option = [] + value = None + option_name = "" + value_list = [] + in_brackets = True + level = 0 + for line_no, line in enumerate(lines): + for char_no, char in enumerate(line): + if not in_comment and char == "*" and prev_char == "/": + in_comment = True + prev_char = "" + continue + elif in_comment and char == "/" and prev_char == "*": + # A multiline comment was closed + in_comment = False + prev_char = "" + option_name = option_name[:-1] + continue + elif in_comment: + # We ignore the content of multiline comments + pass + elif not in_value and ((char == "/" and prev_char == "/") or + char == "#"): + # In the case of a line comment continue processing + # the next line + prev_char = "" + option_name = option_name[:-1] + break + elif char in "'\"": + if in_value and value.quotes == char: + value.end = char_no + in_value = not in_value + elif not value: + value = Value(line_no, char_no, char) + in_value = not in_value + else: + value.string += char + elif in_value: + value.string += char + elif option_name and char == ":" and prev_char == ":": + option.append(option_name[:-1]) + option_name = "" + elif char.isalpha() or char in "/-:._+": + option_name += char.lower() + elif char == ";": + if in_brackets: + value_list.append(value) + value = None + continue + if value_list: + log.debug("Found %s \"%s\"", "::".join(option), + value_list) + options["::".join(option)] = value_list + value_list = [] + elif value: + log.debug("Found %s \"%s\"", "::".join(option), value) + options["::".join(option)] = value + else: + log.debug("Skipping empty key %s", "::".join(option)) + value = None + if level > 0: + option.pop() + else: + option = [] + elif char == "}": + level -= 1 + in_brackets = False + elif char == "{": + level += 1 + if option_name: + option.append(option_name) + option_name = "" + in_brackets = True + elif char in "\t\n ": + if option_name: + option.append(option_name) + option_name = "" + in_brackets = False + else: + raise ValueError("Unknown char '%s' in line: '%s'" % + (char, line)) + prev_char = char + return options + + def set_value(self, option, value, defaultfile): + """Change the value of an option in the configuration. + + :param option: The name of the option, e.g. + 'apt::periodic::AutoCleanInterval'. + :param value: The value of the option. Will be converted to string. + :param defaultfile: The filename of the ``/etc/apt/apt.conf.d`` + configuration snippet in which the option should be set. + If the value is overriden by a later configuration file snippet + it will be disabled in the corresponding configuration file. + """ + # FIXME: Support value lists + # Convert the value to string + if value is True: + value = "true" + elif value is False: + value = "false" + else: + value = str(value) + # Check all configuration file snippets + etc_parts = os.path.join(apt_pkg.config.find_dir("Dir::Etc"), + apt_pkg.config.find_dir("Dir::Etc::Parts")) + for filename in os.listdir(etc_parts): + if filename < defaultfile: + continue + with open(os.path.join(etc_parts, filename)) as fd: + lines = fd.readlines() + config = self.parse(lines) + try: + val = config[option.lower()] + except KeyError: + if filename == defaultfile: + lines.append("%s '%s';\n" % (option, value)) + else: + continue + else: + # Check if the value needs to be changed at all + if ((value == "true" and + val.string.lower() in ["yes", "with", "on", + "enable"]) or + (value == "false" and + val.string.lower() in ["no", "without", "off", + "disable"]) or + (str(value) == val.string)): + continue + if filename == defaultfile: + line = lines[val.line] + new_line = line[:val.start + 1] + new_line += value + new_line += line[val.end:] + lines[val.line] = new_line + else: + # Comment out existing values instead in non default + # configuration files + # FIXME Quite dangerous for brackets + lines[val.line] = "// %s" % lines[val.line] + with open(os.path.join(etc_parts, filename), "w") as fd: + log.debug("Writting %s", filename) + fd.writelines(lines) + if not os.path.exists(os.path.join(etc_parts, defaultfile)): + with open(os.path.join(etc_parts, defaultfile), "w") as fd: + log.debug("Writting %s", filename) + line = "%s '%s';\n" % (option, value) + fd.write(line) + + +def main(): + apt_pkg.init_config() + cw = ConfigWriter() + for filename in sorted(os.listdir("/etc/apt/apt.conf.d/")): + lines = open("/etc/apt/apt.conf.d/%s" % filename).readlines() + cw.parse(lines) + print((cw.set_value("huhu::abc", "lumpi", "10glatzor"))) + +if __name__ == "__main__": + main() + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/console.py b/aptdaemon/console.py new file mode 100644 index 0000000..e4a9e71 --- /dev/null +++ b/aptdaemon/console.py @@ -0,0 +1,691 @@ +""" +This module provides a command line client for the aptdaemon +""" +# Copyright (C) 2008-2009 Sebastian Heinlein <sevel@glatzor.de> +# +# Licensed under the GNU General Public License Version 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("ConsoleClient", "main") + +import array +import fcntl +from gettext import gettext as _ +from gettext import ngettext +import locale +from optparse import OptionParser +import os +import pty +import re +import termios +import time +import tty +import signal +import sys + +from aptsources.sourceslist import SourceEntry +from gi.repository import GLib +import dbus.mainloop.glib +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + +import aptdaemon +from . import client +from . import enums +from . import errors + +ANSI_BOLD = chr(27) + "[1m" +ANSI_RESET = chr(27) + "[0m" + +PY3K = sys.version_info.major > 2 + + +class ConsoleClient: + """ + Command line interface client to aptdaemon + """ + def __init__(self, show_terminal=True, allow_unauthenticated=False, + details=False): + self._client = client.AptClient() + self.master_fd, self.slave_fd = pty.openpty() + self._signals = [] + signal.signal(signal.SIGINT, self._on_cancel_signal) + signal.signal(signal.SIGQUIT, self._on_cancel_signal) + signal.signal(signal.SIGWINCH, self._on_terminal_resize) + self._terminal_width = self._get_terminal_width() + self._watchers = [] + self._old_tty_mode = None + self._show_status = True + self._status = "" + self._percent = 0 + self._show_terminal = show_terminal + self._details = details + self._allow_unauthenticated = allow_unauthenticated + self._show_progress = True + self._status_details = "" + self._progress_details = "" + # Used for a spinning line to indicate a still working transaction + self._spin_elements = "|/-\\" + self._spin_cur = -1 + self._spin_stamp = time.time() + self._transaction = None + self._loop = GLib.MainLoop() + + def add_repository(self, line="", sourcesfile=""): + """Add repository to the sources list.""" + entry = SourceEntry(line) + self._client.add_repository(entry.type, entry.uri, entry.dist, + entry.comps, entry.comment, + sourcesfile, + reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def add_vendor_key_from_file(self, path): + """Install repository key file.""" + self._client.add_vendor_key_from_file( + path, + reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def add_vendor_key_from_keyserver(self, keyid, keyserver): + """Install repository key file.""" + self._client.add_vendor_key_from_keyserver( + keyid, keyserver, + reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def remove_vendor_key(self, fingerprint): + """Remove repository key.""" + self._client.remove_vendor_key(fingerprint, + reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def install_file(self, path): + """Install package file.""" + self._client.install_file(path, reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def list_trusted_vendor_keys(self): + """List the keys of the trusted vendors.""" + def on_done(keys): + for key in keys: + print(key) + self._loop.quit() + self._client.get_trusted_vendor_keys(reply_handler=on_done, + error_handler=self._on_exception) + + def commit_packages(self, install, reinstall, remove, purge, upgrade, + downgrade): + """Commit changes""" + self._client.commit_packages(install, reinstall, remove, purge, + upgrade, downgrade, + reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def fix_incomplete_install(self): + """Fix incomplete installs""" + self._client.fix_incomplete_install( + reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def fix_broken_depends(self): + """Repair broken dependencies.""" + self._client.fix_broken_depends(reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def update_cache(self): + """Update cache""" + self._client.update_cache(reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def upgrade_system(self, safe_mode): + """Upgrade system""" + self._client.upgrade_system(safe_mode, + reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def reconfigure(self, packages, priority): + """Reconfigure packages.""" + self._client.reconfigure(packages, priority, + reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def clean(self): + """Clean archives.""" + self._client.clean(reply_handler=self._run_transaction, + error_handler=self._on_exception) + + def run(self): + """Start the console client application.""" + try: + self._loop.run() + except KeyboardInterrupt: + pass + + def _set_transaction(self, transaction): + """Monitor the given transaction""" + for handler in self._signals: + GLib.source_remove(handler) + self._transaction = transaction + self._signals = [] + self._signals.append(transaction.connect("terminal-attached-changed", + self._on_terminal_attached)) + self._signals.append(transaction.connect("status-changed", + self._on_status)) + self._signals.append(transaction.connect("status-details-changed", + self._on_status_details)) + self._signals.append(transaction.connect("progress-changed", + self._on_progress)) + self._signals.append(transaction.connect("progress-details-changed", + self._on_progress_details)) + self._signals.append(transaction.connect("finished", self._on_exit)) + if self._show_terminal: + transaction.set_terminal(os.ttyname(self.slave_fd)) + transaction.set_allow_unauthenticated(self._allow_unauthenticated) + + def _on_exit(self, trans, enum): + """Callback for the exit state of the transaction""" + # Make sure to dettach the terminal + self._detach() + if self._show_progress: + output = "[+] 100%% %s %-*.*s%s\n" % ( + ANSI_BOLD, + self._terminal_width - 9, + self._terminal_width - 9, + enums.get_exit_string_from_enum(enum), + ANSI_RESET) + sys.stderr.write(output) + + if enum == enums.EXIT_FAILED: + msg = "%s: %s\n%s\n\n%s" % ( + _("ERROR"), + enums.get_error_string_from_enum(trans.error_code), + enums.get_error_description_from_enum(trans.error_code), + trans.error_details) + print(msg) + self._loop.quit() + + def _on_terminal_attached(self, transaction, attached): + """Callback for the terminal-attachabed-changed signal of the + transaction. + """ + if self._show_terminal and attached and not self._watchers: + self._clear_progress() + self._show_progress = False + self._attach() + elif not attached: + self._show_progress = True + self._detach() + + def _on_status(self, transaction, status): + """Callback for the Status signal of the transaction""" + self._status = enums.get_status_string_from_enum(status) + self._update_progress() + + def _on_status_details(self, transaction, text): + """Callback for the StatusDetails signal of the transaction.""" + self._status_details = text + self._update_progress() + + def _on_progress_details(self, transaction, items_done, items_total, + bytes_done, bytes_total, speed, eta): + """Callback for the ProgressDetails signal of the transaction.""" + if bytes_total and speed: + self._progress_details = ( + _("Downloaded %(cur)sB of %(total)sB at %(rate)sB/s") % + {'cur': client.get_size_string(bytes_done), + 'total': client.get_size_string(bytes_total), + 'rate': client.get_size_string(speed)}) + elif bytes_total: + self._progress_details = ( + _("Downloaded %(cur)sB of %(total)sB") % + {'cur': client.get_size_string(bytes_done), + 'total': client.get_size_string(bytes_total)}) + else: + self._progress_details = "" + self._update_progress() + + def _on_progress(self, transaction, percent): + """Callback for the Progress signal of the transaction""" + self._percent = percent + self._update_progress() + + def _update_progress(self): + """Update the progress bar.""" + if not self._show_progress: + return + text = ANSI_BOLD + self._status + ANSI_RESET + if self._status_details: + text += " " + self._status_details + if self._progress_details: + text += " (%s)" % self._progress_details + text_width = self._terminal_width - 9 + # Spin the progress line (maximum 5 times a second) + if self._spin_stamp + 0.2 < time.time(): + self._spin_cur = (self._spin_cur + 1) % len(self._spin_elements) + self._spin_stamp = time.time() + spinner = self._spin_elements[self._spin_cur] + # Show progress information if available + if self._percent > 100: + percent = "---" + else: + percent = self._percent + sys.stderr.write("[%s] " % spinner + + "%3.3s%% " % percent + + "%-*.*s" % (text_width, text_width, text) + "\r") + + def _update_custom_progress(self, msg, percent=None, spin=True): + """Update the progress bar with a custom status message.""" + text = ANSI_BOLD + msg + ANSI_RESET + text_width = self._terminal_width - 9 + # Spin the progress line (maximum 5 times a second) + if spin: + self._spin_cur = (self._spin_cur + 1) % len(self._spin_elements) + self._spin_stamp = time.time() + spinner = self._spin_elements[self._spin_cur] + else: + spinner = "+" + # Show progress information if available + if percent is None: + percent = "---" + sys.stderr.write("[%s] " % spinner + + "%3.3s%% " % percent + + "%-*.*s" % (text_width, text_width, text) + "\r") + return True + + def _stop_custom_progress(self): + """Stop the spinner which shows non trans status messages.""" + if self._progress_id is not None: + GLib.source_remove(self._progress_id) + + def _clear_progress(self): + """Clear progress information on stderr.""" + sys.stderr.write("%-*.*s\r" % (self._terminal_width, + self._terminal_width, + " ")) + + def _on_cancel_signal(self, signum, frame): + """Callback for a cancel signal.""" + if (self._transaction and + self._transaction.status != enums.STATUS_SETTING_UP): + self._transaction.cancel() + else: + self._loop.quit() + + def _on_terminal_resize(self, signum, frame): + """Callback for a changed terminal size.""" + self._terminal_width = self._get_terminal_width() + self._update_progress() + + def _detach(self): + """Dettach the controlling terminal to aptdaemon.""" + for wid in self._watchers: + GLib.source_remove(wid) + if self._old_tty_mode: + tty.tcsetattr(pty.STDIN_FILENO, tty.TCSAFLUSH, + self._old_tty_mode) + + def _attach(self): + """Attach the controlling terminal to aptdaemon. + Based on pty.spwan() + """ + try: + self._old_tty_mode = tty.tcgetattr(pty.STDIN_FILENO) + tty.setraw(pty.STDIN_FILENO) + except tty.error: # This is the same as termios.error + self._old_tty_mode = None + flags = GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP + self._watchers.append( + GLib.io_add_watch(pty.STDIN_FILENO, + GLib.PRIORITY_HIGH_IDLE, flags, + self._copy_io, self.master_fd)) + self._watchers.append( + GLib.io_add_watch(self.master_fd, GLib.PRIORITY_HIGH_IDLE, + flags, self._copy_io, pty.STDOUT_FILENO)) + + def _copy_io(self, source, condition, target): + """Callback to copy data between terminals.""" + if condition == GLib.IO_IN: + data = os.read(source, 1024) + if target: + os.write(target, data) + return True + os.close(source) + return False + + def _get_terminal_width(self): + """Return the witdh in characters of the current terminal.""" + try: + return array.array("h", fcntl.ioctl(sys.stderr, termios.TIOCGWINSZ, + "\0" * 8))[1] + except IOError: + # Fallback to the "default" size + return 80 + + def _on_exception(self, error): + """Error callback.""" + self._detach() + try: + raise error + except errors.PolicyKitError: + msg = "%s %s\n\n%s" % (_("ERROR:"), + _("You are not allowed to perform " + "this action."), + error.get_dbus_message()) + except dbus.DBusException: + msg = "%s %s - %s" % (_("ERROR:"), error.get_dbus_name(), + error.get_dbus_message()) + except: + msg = str(error) + self._loop.quit() + sys.exit(msg) + + def _run_transaction(self, trans): + """Callback which runs a requested transaction.""" + self._set_transaction(trans) + self._stop_custom_progress() + if self._transaction.role in [enums.ROLE_UPDATE_CACHE, + enums.ROLE_ADD_VENDOR_KEY_FILE, + enums.ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER, + enums.ROLE_REMOVE_VENDOR_KEY, + enums.ROLE_FIX_INCOMPLETE_INSTALL]: + # TRANSLATORS: status message + self._progress_id = GLib.timeout_add(250, + self._update_custom_progress, + _("Queuing")) + self._transaction.run( + error_handler=self._on_exception, + reply_handler=lambda: self._stop_custom_progress()) + else: + # TRANSLATORS: status message + self._progress_id = GLib.timeout_add(250, + self._update_custom_progress, + _("Resolving dependencies")) + self._transaction.simulate(reply_handler=self._show_changes, + error_handler=self._on_exception) + + def _show_changes(self): + def show_packages(pkgs): + """Format the pkgs in a nice way.""" + line = " " + pkgs.sort() + for pkg in pkgs: + try: + name, version = pkg.split("=", 1)[0:2] + except ValueError: + name = pkg + version = None + if self._details and version: + output = "%s=%s" % (name, version) + else: + output = name + if (len(line) + 1 + len(output) > self._terminal_width and + line != " "): + print(line) + line = " " + line += " %s" % output + if line != " ": + print(line) + self._stop_custom_progress() + self._clear_progress() + (installs, reinstalls, removals, purges, upgrades, + downgrades) = self._transaction.packages + (dep_installs, dep_reinstalls, dep_removals, dep_purges, dep_upgrades, + dep_downgrades, dep_kepts) = self._transaction.dependencies + installs.extend(dep_installs) + upgrades.extend(dep_upgrades) + removals.extend(purges) + removals.extend(dep_removals) + removals.extend(dep_purges) + reinstalls.extend(dep_reinstalls) + downgrades.extend(dep_downgrades) + kepts = dep_kepts + if installs: + # TRANSLATORS: %s is the number of packages + print((ngettext("The following NEW package will be installed " + "(%(count)s):", + "The following NEW packages will be installed " + "(%(count)s):", + len(installs)) % {"count": len(installs)})) + show_packages(installs) + if upgrades: + # TRANSLATORS: %s is the number of packages + print((ngettext("The following package will be upgraded " + "(%(count)s):", + "The following packages will be upgraded " + "(%(count)s):", + len(upgrades)) % {"count": len(upgrades)})) + show_packages(upgrades) + if removals: + # TRANSLATORS: %s is the number of packages + print((ngettext("The following package will be REMOVED " + "(%(count)s):", + "The following packages will be REMOVED " + "(%(count)s):", + len(removals)) % {"count": len(removals)})) + # FIXME: mark purges + show_packages(removals) + if downgrades: + # TRANSLATORS: %s is the number of packages + print((ngettext("The following package will be DOWNGRADED " + "(%(count)s):", + "The following packages will be DOWNGRADED " + "(%(count)s):", + len(downgrades)) % {"count": len(downgrades)})) + show_packages(downgrades) + if reinstalls: + # TRANSLATORS: %s is the number of packages + print((ngettext("The following package will be reinstalled " + "(%(count)s):", + "The following packages will be reinstalled " + "(%(count)s):", + len(reinstalls)) % {"count": len(reinstalls)})) + show_packages(reinstalls) + if kepts: + print((ngettext("The following package has been kept back " + "(%(count)s):", + "The following packages have been kept back " + "(%(count)s):", + len(kepts)) % {"count": len(kepts)})) + show_packages(kepts) + + if self._transaction.download: + print(_("Need to get %sB of archives.") % + client.get_size_string(self._transaction.download)) + if self._transaction.space > 0: + print(_("After this operation, %sB of additional disk space " + "will be used.") % + client.get_size_string(self._transaction.space)) + elif self._transaction.space < 0: + print(_("After this operation, %sB of additional disk space " + "will be freed.") % + client.get_size_string(self._transaction.space)) + if (self._transaction.space or self._transaction.download or + installs or upgrades or downgrades or removals or kepts or + reinstalls): + try: + if PY3K: + cont = input(_("Do you want to continue [Y/n]?")) + else: + cont = raw_input(_("Do you want to continue [Y/n]?")) + except EOFError: + cont = "n" + # FIXME: Listen to changed dependencies! + if (not re.match(locale.nl_langinfo(locale.YESEXPR), cont) and + cont != ""): + msg = enums.get_exit_string_from_enum(enums.EXIT_CANCELLED) + self._update_custom_progress(msg, None, False) + self._loop.quit() + sys.exit(1) + # TRANSLATORS: status message + self._progress_id = GLib.timeout_add(250, + self._update_custom_progress, + _("Queuing")) + self._transaction.run( + error_handler=self._on_exception, + reply_handler=lambda: self._stop_custom_progress()) + + +def main(): + """Run a command line client for aptdaemon""" + epilog = _("To operate on more than one package put the package " + "names in quotation marks:\naptdcon --install " + "\"foo bar\"") + parser = OptionParser(version=aptdaemon.__version__, epilog=epilog) + parser.add_option("-c", "--refresh", default="", + action="store_true", dest="refresh", + help=_("Refresh the cache")) + parser.add_option("", "--fix-depends", default="", + action="store_true", dest="fix_depends", + help=_("Try to resolve broken dependencies. " + "Potentially dangerous operation since it could " + "try to remove many packages.")) + parser.add_option("", "--fix-install", default="", + action="store_true", dest="fix_install", + help=_("Try to finish a previous incompleted " + "installation")) + parser.add_option("-i", "--install", default="", + action="store", type="string", dest="install", + help=_("Install the given packages")) + parser.add_option("", "--reinstall", default="", + action="store", type="string", dest="reinstall", + help=_("Reinstall the given packages")) + parser.add_option("-r", "--remove", default="", + action="store", type="string", dest="remove", + help=_("Remove the given packages")) + parser.add_option("-p", "--purge", default="", + action="store", type="string", dest="purge", + help=_("Remove the given packages including " + "configuration files")) + parser.add_option("-u", "--upgrade", default="", + action="store", type="string", dest="upgrade", + help=_("Install the given packages")) + parser.add_option("", "--downgrade", default="", + action="store", type="string", dest="downgrade", + help=_("Downgrade the given packages")) + parser.add_option("", "--upgrade-system", + action="store_true", dest="safe_upgrade", + help=_("Deprecated: Please use " + "--safe-upgrade")) + parser.add_option("", "--safe-upgrade", + action="store_true", dest="safe_upgrade", + help=_("Upgrade the system in a safe way")) + parser.add_option("", "--full-upgrade", + action="store_true", dest="full_upgrade", + help=_("Upgrade the system, possibly installing and " + "removing packages")) + parser.add_option("", "--add-vendor-key", default="", + action="store", type="string", dest="add_vendor_key", + help=_("Add the vendor to the trusted ones")) + parser.add_option("", "--add-vendor-key-from-keyserver", default="", + action="store", type="string", + help=_("Add the vendor keyid (also needs " + "--keyserver)")) + parser.add_option("", "--keyserver", default="", + action="store", type="string", + help=_("Use the given keyserver for looking up " + "keys")) + parser.add_option("", "--add-repository", default="", + action="store", type="string", dest="add_repository", + help=_("Add new repository from the given " + "deb-line")) + parser.add_option("", "--sources-file", action="store", default="", + type="string", dest="sources_file", + help=_("Specify an alternative sources.list.d file to " + "which repositories should be added.")) + parser.add_option("", "--list-trusted-vendors", default="", + action="store_true", dest="list_trusted_vendor_keys", + help=_("List trusted vendor keys")) + parser.add_option("", "--remove-vendor-key", default="", + action="store", type="string", dest="remove_vendor_key", + help=_("Remove the trusted key of the given " + "fingerprint")) + parser.add_option("", "--clean", + action="store_true", dest="clean", + help=_("Remove downloaded package files")) + parser.add_option("", "--reconfigure", default="", + action="store", type="string", dest="reconfigure", + help=_("Reconfigure installed packages. Optionally the " + "minimum priority of questions can be " + "specified")) + parser.add_option("", "--priority", default="default", + action="store", type="string", dest="priority", + help=_("The minimum debconf priority of question to " + "be displayed")) + parser.add_option("", "--hide-terminal", + action="store_true", dest="hide_terminal", + help=_("Do not attach to the apt terminal")) + parser.add_option("", "--allow-unauthenticated", + action="store_true", dest="allow_unauthenticated", + default=False, + help=_("Allow packages from unauthenticated " + "sources")) + parser.add_option("-d", "--show-details", + action="store_true", dest="details", + help=_("Show additional information about the packages. " + "Currently only the version number")) + (options, args) = parser.parse_args() + con = ConsoleClient(show_terminal=not options.hide_terminal, + allow_unauthenticated=options.allow_unauthenticated, + details=options.details) + # TRANSLATORS: status message + con._progress_id = GLib.timeout_add(250, con._update_custom_progress, + _("Waiting for authentication")) + if options.safe_upgrade: + con.upgrade_system(True) + elif options.full_upgrade: + con.upgrade_system(False) + elif options.refresh: + con.update_cache() + elif options.reconfigure: + con.reconfigure(options.reconfigure.split(), options.priority) + elif options.clean: + con.clean() + elif options.fix_install: + con.fix_incomplete_install() + elif options.fix_depends: + con.fix_broken_depends() + elif options.install and options.install.endswith(".deb"): + con.install_file(options.install) + elif (options.install or options.reinstall or options.remove or + options.purge or options.upgrade or options.downgrade): + con.commit_packages(options.install.split(), + options.reinstall.split(), + options.remove.split(), + options.purge.split(), + options.upgrade.split(), + options.downgrade.split()) + elif options.add_repository: + con.add_repository(options.add_repository, options.sources_file) + elif options.add_vendor_key: + # FIXME: Should detect if from stdin or file + con.add_vendor_key_from_file(options.add_vendor_key) + elif options.add_vendor_key_from_keyserver and options.keyserver: + con.add_vendor_key_from_keyserver( + options.add_vendor_key_from_keyserver, + options.keyserver) + elif options.remove_vendor_key: + con.remove_vendor_key(options.remove_vendor_key) + elif options.list_trusted_vendor_keys: + con.list_trusted_vendor_keys() + else: + parser.print_help() + sys.exit(1) + con.run() + +if __name__ == "__main__": + main() diff --git a/aptdaemon/core.py b/aptdaemon/core.py new file mode 100644 index 0000000..ac05247 --- /dev/null +++ b/aptdaemon/core.py @@ -0,0 +1,2204 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Core components of aptdaemon. + +This module provides the following core classes of the aptdaemon: +AptDaemon - complete daemon for managing software via DBus interface +Transaction - represents a software management operation +TransactionQueue - queue for aptdaemon transactions + +The main function allows to run the daemon as a command. +""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("Transaction", "TransactionQueue", "AptDaemon", + "APTDAEMON_TRANSACTION_DBUS_INTERFACE", "APTDAEMON_DBUS_INTERFACE" + "APTDAEMON_DBUS_PATH", "APTDAEMON_DBUS_SERVICE", + "APTDAEMON_IDLE_CHECK_INTERVAL", "APTDAEMON_IDLE_TIMEOUT", + "TRANSACTION_IDLE_TIMEOUT", "TRANSACTION_DEL_TIMEOUT") + +import collections +from xml.etree import ElementTree +import gettext +from hashlib import md5 +import locale +import logging +import logging.handlers +from optparse import OptionParser +import os +import re +import signal +import sys +import time +import uuid + +from gi.repository import GObject, GLib +import dbus.exceptions +import dbus.service +import dbus.mainloop.glib + +from .config import ConfigWriter +from . import errors +from . import enums +from defer import inline_callbacks, return_value, Deferred +from defer.utils import dbus_deferred_method +from . import policykit1 +from .utils import split_package_id, set_euid_egid +from .worker import DummyWorker +from .worker.aptworker import (AptWorker, + trans_only_installs_pkgs_from_high_trust_repos) +from .loop import mainloop +from .logger import ColoredFormatter + +# Setup i18n +_ = lambda msg: gettext.dgettext("aptdaemon", msg) +if sys.version >= '3': + _gettext_method = "gettext" + _ngettext_method = "ngettext" +else: + _gettext_method = "ugettext" + _ngettext_method = "ungettext" + +APTDAEMON_DBUS_INTERFACE = 'org.debian.apt' +APTDAEMON_DBUS_PATH = '/org/debian/apt' +APTDAEMON_DBUS_SERVICE = 'org.debian.apt' + +APTDAEMON_TRANSACTION_DBUS_INTERFACE = 'org.debian.apt.transaction' + +APTDAEMON_IDLE_CHECK_INTERVAL = 60 +APTDAEMON_IDLE_TIMEOUT = 10 * 60 + +# Maximum allowed time between the creation of a transaction and its queuing +TRANSACTION_IDLE_TIMEOUT = 300 +# Keep the transaction for the given time alive on the bus after it has +# finished +TRANSACTION_DEL_TIMEOUT = 30 + +# regexp for the pkgname and optional arch, for details see +# http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Source +REGEX_VALID_PACKAGENAME = "^[a-z0-9][a-z0-9\-+.]+(:[a-z0-9]+)?$" +# regexp for the version number, for details see: +# http://www.debian.org/doc/debian-policy/ch-controlfields.html#s-f-Version +REGEX_VALID_VERSION = "^[0-9][0-9.+\-A-Za-z:~]*$" +# regexp for the archive (Suite) as found in the Release file +REGEX_VALID_RELEASE = "^[a-zA-Z0-9_\-\.]+$" + +# Setup the DBus main loop +dbus.mainloop.glib.DBusGMainLoop(set_as_default=True) + +# Required for daemon mode +os.putenv("PATH", + "/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin") + +# Setup logging to syslog and the console +log = logging.getLogger("AptDaemon") +try: + _syslog_handler = logging.handlers.SysLogHandler( + address="/dev/log", + facility=logging.handlers.SysLogHandler.LOG_DAEMON) + _syslog_handler.setLevel(logging.INFO) + _syslog_formatter = logging.Formatter("%(name)s: %(levelname)s: " + "%(message)s") + _syslog_handler.setFormatter(_syslog_formatter) +except: + pass +else: + log.addHandler(_syslog_handler) +_console_handler = logging.StreamHandler() +_console_formatter = ColoredFormatter("%(asctime)s %(name)s [%(levelname)s]: " + "%(message)s", + "%T") +_console_handler.setFormatter(_console_formatter) +log.addHandler(_console_handler) +# FIXME: Use LoggerAdapter (requires Python 2.6) +log_trans = logging.getLogger("AptDaemon.Trans") + +# Required for translations from APT +try: + locale.setlocale(locale.LC_ALL, "") +except locale.Error: + log.warning("Failed to unset LC_ALL. Translations are not available.") + + +def _excepthook(exc_type, exc_obj, exc_tb, apport_excepthook): + """Handle exceptions of aptdaemon and avoid tiggering apport crash + reports for valid DBusExceptions that are sent to the client. + """ + # apport registers it's own excepthook as sys.excepthook. So we have to + # send exceptions that we don't want to be tracked to Python's + # internal excepthook directly + if issubclass(exc_type, errors.AptDaemonError): + sys.__excepthook__(exc_type, exc_obj, exc_tb) + else: + apport_excepthook(exc_type, exc_obj, exc_tb) + +if sys.excepthook.__name__ == "apport_excepthook": + apport_excepthook = sys.excepthook + sys.excepthook = lambda etype, eobj, etb: _excepthook(etype, eobj, etb, + apport_excepthook) + + +class DBusObject(dbus.service.Object): + + """Enhanced D-Bus object class which supports properties.""" + + WRITABLE_PROPERTIES = () + + # pylint: disable-msg=C0103,C0322 + @dbus.service.signal(dbus_interface=dbus.PROPERTIES_IFACE, + signature="sa{sv}as") + def PropertiesChanged(self, interface, changed_properties, + invalidated_properties): + """The signal gets emitted if a property of the object's + interfaces changed. + + :param property: The name of the interface. + :param changed_properties: A dictrionary of changed + property/value pairs + :param invalidated_properties: An array of property names which + changed but the value isn't conveyed. + + :type interface: s + :type changed_properties: a{sv} + :type invalidated_properties: as + """ + log.debug("Emitting PropertiesChanged: %s, %s, %s" % + (interface, changed_properties, invalidated_properties)) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.method(dbus.INTROSPECTABLE_IFACE, + in_signature='', out_signature='s', + path_keyword='object_path', + connection_keyword='connection') + def Introspect(self, object_path, connection): + # Inject the properties into the introspection xml data + data = dbus.service.Object.Introspect(self, object_path, connection) + xml = ElementTree.fromstring(data) + for iface in xml.findall("interface"): + props = self._get_properties(iface.attrib["name"]) + for key, value in props.items(): + attrib = {"name": key} + if key in self.WRITABLE_PROPERTIES: + attrib["access"] = "readwrite" + else: + attrib["access"] = "read" + if isinstance(value, dbus.String): + attrib["type"] = "s" + elif isinstance(value, dbus.UInt32): + attrib["type"] = "u" + elif isinstance(value, dbus.Int32): + attrib["type"] = "i" + elif isinstance(value, dbus.UInt64): + attrib["type"] = "t" + elif isinstance(value, dbus.Int64): + attrib["type"] = "x" + elif isinstance(value, dbus.Boolean): + attrib["type"] = "b" + elif isinstance(value, dbus.Struct): + attrib["type"] = "(%s)" % value.signature + elif isinstance(value, dbus.Dictionary): + attrib["type"] = "a{%s}" % value.signature + elif isinstance(value, dbus.Array): + attrib["type"] = "a%s" % value.signature + else: + raise Exception("Type %s of property %s isn't " + "convertable" % (type(value), key)) + iface.append(ElementTree.Element("property", attrib)) + new_data = ElementTree.tostring(xml, encoding="UTF-8") + return new_data + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(dbus.PROPERTIES_IFACE, + in_signature="ssv", out_signature="", + sender_keyword="sender") + def Set(self, iface, name, value, sender): + """Set a property. + + Only the user who intiaited the transaction is + allowed to modify it. + + :param iface: The interface which provides the property. + :param name: The name of the property which should be modified. + :param value: The new value of the property. + + :type iface: s + :type name: s + :type value: v + """ + log.debug("Set() was called: %s, %s" % (name, value)) + return self._set_property(iface, name, value, sender) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature="s", out_signature="a{sv}") + def GetAll(self, iface): + """Get all available properties of the given interface.""" + log.debug("GetAll() was called: %s" % iface) + return self._get_properties(iface) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.method(dbus.PROPERTIES_IFACE, + in_signature="ss", out_signature="v") + def Get(self, iface, property): + """Return the value of the given property provided by the given + interface. + """ + log.debug("Get() was called: %s, %s" % (iface, property)) + return self._get_properties(iface)[property] + + def _set_property(self, iface, name, value, sender): + """Helper to set a property on the properties D-Bus interface.""" + raise dbus.exceptions.DBusException("Unknown or read only " + "property: %s" % name) + + def _get_properties(self, iface): + """Helper to get the properties of a D-Bus interface.""" + return {} + + +class Transaction(DBusObject): + + """Represents a transaction on the D-Bus. + + A transaction represents a single package management task, e.g. + installation or removal of packages. This class allows to expose + information and to controll the transaction via DBus using PolicyKit + for managing privileges. + """ + + ROLE_ACTION_MAP = { + enums.ROLE_PK_QUERY: None, + enums.ROLE_INSTALL_PACKAGES: ( + policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES), + enums.ROLE_REMOVE_PACKAGES: ( + policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES), + enums.ROLE_INSTALL_FILE: ( + policykit1.PK_ACTION_INSTALL_FILE), + enums.ROLE_UPGRADE_PACKAGES: ( + policykit1.PK_ACTION_UPGRADE_PACKAGES), + enums.ROLE_UPGRADE_SYSTEM: ( + policykit1.PK_ACTION_UPGRADE_PACKAGES), + enums.ROLE_UPDATE_CACHE: ( + policykit1.PK_ACTION_UPDATE_CACHE), + enums.ROLE_COMMIT_PACKAGES: ( + policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES), + enums.ROLE_ADD_VENDOR_KEY_FILE: ( + policykit1.PK_ACTION_CHANGE_REPOSITORY), + enums.ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER: ( + policykit1.PK_ACTION_CHANGE_REPOSITORY), + enums.ROLE_REMOVE_VENDOR_KEY: ( + policykit1.PK_ACTION_CHANGE_REPOSITORY), + enums.ROLE_FIX_INCOMPLETE_INSTALL: ( + policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES), + enums.ROLE_FIX_BROKEN_DEPENDS: ( + policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES), + enums.ROLE_ADD_REPOSITORY: ( + policykit1.PK_ACTION_CHANGE_REPOSITORY), + enums.ROLE_RECONFIGURE: ( + policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES), + enums.ROLE_CLEAN: ( + policykit1.PK_ACTION_CLEAN), + enums.ROLE_ENABLE_DISTRO_COMP: ( + policykit1.PK_ACTION_CHANGE_REPOSITORY), + enums.ROLE_ADD_LICENSE_KEY: ( + policykit1.PK_ACTION_INSTALL_OR_REMOVE_PACKAGES)} + + WRITABLE_PROPERTIES = ("HttpProxy", "Terminal", "AllowUnauthenticated", + "DebconfSocket", "MetaData", "Locale", + "RemoveObsoleteDepends") + + def __init__(self, tid, role, queue, pid, uid, gid, cmdline, sender, + connect=True, bus=None, packages=None, kwargs=None): + """Initialize a new Transaction instance. + + Keyword arguments: + tid -- The unique identifier + role -- The role enum of the transaction + queue -- TransactionQueue instance of the daemon + pid -- the id of the process which created the transaction + uid -- the uid of the user who created the transaction + cmdline -- the cmdline of the calling process + sender -- the DBus name of the sender who created the transaction + connect -- if the Transaction should connect to DBus (default is True) + bus -- the DBus connection which should be used + (defaults to system bus) + """ + if tid is None: + tid = uuid.uuid4().hex + self.tid = "/org/debian/apt/transaction/%s" % tid + if connect is True: + self.bus = bus + if bus is None: + self.bus = dbus.SystemBus() + bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE, self.bus) + dbus_path = self.tid + else: + bus = None + bus_name = None + dbus_path = None + DBusObject.__init__(self, bus_name, dbus_path) + if not packages: + packages = ([], [], [], [], [], []) + if not kwargs: + kwargs = {} + self.queue = queue + self.uid = uid + self.gid = gid + self.locale = dbus.String("") + self.allow_unauthenticated = dbus.Boolean(False) + self.remove_obsoleted_depends = dbus.Boolean(False) + self.cmdline = cmdline + self.pid = pid + self.http_proxy = dbus.String("") + self.terminal = dbus.String("") + pk_socket = "/run/user/%d/pk-debconf-socket" % self.uid + if os.path.exists(pk_socket): + self.debconf = dbus.String(pk_socket) + else: + self.debconf = dbus.String("") + self.kwargs = kwargs + self._translation = None + # The transaction which should be executed after this one + self.after = None + self._role = dbus.String(role) + self._progress = dbus.Int32(0) + # items_done, total_items, bytes_done, total_bytes, speed, time + self._progress_details = dbus.Struct((0, 0, 0, 0, 0.0, 0), + signature="iixxdx") + self._progress_download = dbus.Struct(("", "", "", 0, 0, ""), + signature="sssxxs") + self._progress_package = dbus.Struct(("", ""), signature="ss") + self._exit = dbus.String(enums.EXIT_UNFINISHED) + self._status = dbus.String(enums.STATUS_SETTING_UP) + self._status_details = dbus.String("") + self._error = None + self._error_property = dbus.Struct(("", ""), signature="ss") + self._cancellable = dbus.Boolean(True) + self._term_attached = dbus.Boolean(False) + self._required_medium = dbus.Struct(("", ""), signature="ss") + self._config_file_conflict = dbus.Struct(("", ""), signature="ss") + self._config_file_conflict_resolution = "" + self.cancelled = dbus.Boolean(False) + self.paused = dbus.Boolean(False) + self._meta_data = dbus.Dictionary(signature="sv") + self._download = dbus.Int64(0) + self._space = dbus.Int64(0) + self._depends = dbus.Struct([dbus.Array([], signature='s') + for i in range(7)], + signature="asasasasasasas") + self._packages = dbus.Struct([dbus.Array(pkgs, signature="s") + for pkgs in packages], + signature="asasasasasas") + self._unauthenticated = dbus.Array([], signature=dbus.Signature('s')) + self._high_trust_packages = dbus.Array([], + signature=dbus.Signature('s')) + # Add a timeout which removes the transaction from the bus if it + # hasn't been setup and run for the TRANSACTION_IDLE_TIMEOUT period + self._idle_watch = GLib.timeout_add_seconds( + TRANSACTION_IDLE_TIMEOUT, self._remove_from_connection_no_raise) + # Handle a disconnect of the client application + self.sender_alive = True + if bus: + self._sender_watch = bus.watch_name_owner( + sender, self._sender_owner_changed) + else: + self._sender_watch = None + self.sender = sender + self.output = "" + self.simulated = None + self._simulated_cb = None + + def _sender_owner_changed(self, connection): + """Callback if the owner of the original sender changed, e.g. + disconnected.""" + if not connection: + self.sender_alive = False + + def _remove_from_connection_no_raise(self): + """Version of remove_from_connection that does not raise if the + object isn't exported. + """ + log_trans.debug("Removing transaction") + try: + self.remove_from_connection() + except LookupError as error: + log_trans.debug("remove_from_connection() raised LookupError: " + "'%s'" % error) + # Forget a not yet queued transaction + try: + self.queue.limbo.pop(self.tid) + except KeyError: + pass + return False + + def _convert_struct(self, lst, signature): + """Convert a list to a DBus struct with the given signature. Currently + integer, long, unsigned long, double, string and boolean are + supported (ixtdsb). + """ + struct = [] + for num, item in enumerate(lst): + try: + if signature[num] == "i": + struct.append(dbus.Int32(item)) + elif signature[num] == "x": + struct.append(dbus.Int64(item)) + elif signature[num] == "t": + struct.append(dbus.UInt64(item)) + elif signature[num] == "d": + struct.append(dbus.Double(item)) + elif signature[num] == "b": + struct.append(dbus.Boolean(item)) + elif signature[num] == "s": + struct.append(get_dbus_string(item)) + else: + raise Exception("Value %s with unknown signature %s" % + (item, signature[num])) + except Exception as error: + raise error.__class__("Failed to convert item %s of %s with " + "signature %s: %s" % (num, lst, + signature, + str(error))) + return dbus.Struct(struct, signature=dbus.Signature(signature)) + + def _set_meta_data(self, data): + # Perform some checks + if self.status != enums.STATUS_SETTING_UP: + raise errors.TransactionAlreadyRunning() + if not isinstance(data, dbus.Dictionary): + raise errors.InvalidMetaDataError("The data value has to be a " + "dictionary: %s" % data) + if not data.signature.startswith("s"): + raise errors.InvalidMetaDataError("Only strings are accepted " + "as keys.") + for key, value in data.items(): + if key in self._meta_data: + raise errors.InvalidMetaDataError("The key %s already " + "exists. It is not allowed " + "to overwrite existing " + "data." % key) + if not len(key.split("_")) > 1: + raise errors.InvalidMetaDataError("The key %s has to be of " + "the format " + "IDENTIFIER-KEYNAME") + if not isinstance(value, dbus.String): + raise errors.InvalidMetaDataError("The value has to be a " + "string: %s" % value) + # Merge new data into existing one: + self._meta_data.update(data) + self.PropertyChanged("MetaData", self._meta_data) + + def _get_meta_data(self): + return self._meta_data + + meta_data = property(_get_meta_data, _set_meta_data, + doc="Allows client applications to store meta data " + "for the transaction in a dictionary.") + + def _set_role(self, enum): + if self._role != enums.ROLE_UNSET: + raise errors.TransactionRoleAlreadySet() + self._role = dbus.String(enum) + self.PropertyChanged("Role", self._role) + + def _get_role(self): + return self._role + + role = property(_get_role, _set_role, doc="Operation type of transaction.") + + def _set_progress_details(self, details): + # items_done, total_items, bytes_done, total_bytes, speed, time + self._progress_details = self._convert_struct(details, "iixxdx") + self.PropertyChanged("ProgressDetails", self._progress_details) + + def _get_progress_details(self): + return self._progress_details + + progress_details = property(_get_progress_details, _set_progress_details, + doc="Tuple containing detailed progress " + "information: items done, total items, " + "bytes done, total bytes, speed and " + "remaining time") + + def _set_error(self, excep): + self._error = excep + msg = self.gettext(excep.details) % excep.details_args + self._error_property = self._convert_struct((excep.code, msg), "ss") + self.PropertyChanged("Error", self._error_property) + + def _get_error(self): + return self._error + + error = property(_get_error, _set_error, doc="Raised exception.") + + def _set_exit(self, enum): + self.status = enums.STATUS_FINISHED + self._exit = dbus.String(enum) + self.PropertyChanged("ExitState", self._exit) + self.Finished(self._exit) + if self._sender_watch: + self._sender_watch.cancel() + # Remove the transaction from the Bus after it is complete. A short + # timeout helps lazy clients + GLib.timeout_add_seconds(TRANSACTION_DEL_TIMEOUT, + self._remove_from_connection_no_raise) + + def _get_exit(self): + return self._exit + + exit = property(_get_exit, _set_exit, + doc="The exit state of the transaction.") + + def _get_download(self): + return self._download + + def _set_download(self, size): + self._download = dbus.Int64(size) + self.PropertyChanged("Download", self._download) + + download = property(_get_download, _set_download, + doc="The download size of the transaction.") + + def _get_space(self): + return self._space + + def _set_space(self, size): + self._space = dbus.Int64(size) + self.PropertyChanged("Space", self._space) + + space = property(_get_space, _set_space, + doc="The required disk space of the transaction.") + + def _set_packages(self, packages): + self._packages = dbus.Struct([dbus.Array(pkgs, signature="s") + for pkgs in packages], + signature="as") + self.PropertyChanged("Packages", self._packages) + + def _get_packages(self): + return self._packages + + packages = property(_get_packages, _set_packages, + doc="Packages which will be explictly installed, " + "reinstalled, removed, purged, upgraded or " + "downgraded.") + + def _get_unauthenticated(self): + return self._unauthenticated + + def _set_unauthenticated(self, unauthenticated): + self._unauthenticated = dbus.Array(unauthenticated, signature="s") + self.PropertyChanged("Unauthenticated", self._unauthenticated) + + unauthenticated = property(_get_unauthenticated, _set_unauthenticated, + doc="Unauthenticated packages in this " + "transaction") + + # package that can have a different auth schema, useful for e.g. + # lightweight packages like unity-webapps or packages comming from + # a high trust repository (e.g. a internal company repo) + def _get_high_trust_packages(self): + return self._high_trust_packages + + def _set_high_trust_packages(self, whitelisted_packages): + self._high_trust_packages = dbus.Array(whitelisted_packages, + signature="s") + self.PropertyChanged("HighTrustWhitelistedPackages", + self._high_trust_packages) + + high_trust_packages = property(_get_high_trust_packages, + _set_high_trust_packages, + doc="High trust packages in this " + "transaction") + + def _get_depends(self): + return self._depends + + def _set_depends(self, depends): + self._depends = dbus.Struct([dbus.Array(deps, signature="s") + for deps in depends], + signature="as") + self.PropertyChanged("Dependencies", self._depends) + + depends = property(_get_depends, _set_depends, + doc="The additional dependencies: installs, removals, " + "upgrades and downgrades.") + + def _get_status(self): + return self._status + + def _set_status(self, enum): + self._status = dbus.String(enum) + self.PropertyChanged("Status", self._status) + + status = property(_get_status, _set_status, + doc="The status of the transaction.") + + def _get_status_details(self): + return self._status_details + + def _set_status_details(self, text): + self._status_details = get_dbus_string(text) + self.PropertyChanged("StatusDetails", self._status_details) + + status_details = property(_get_status_details, _set_status_details, + doc="The status message from apt.") + + def _get_progress(self): + return self._progress + + def _set_progress(self, percent): + self._progress = dbus.Int32(percent) + self.PropertyChanged("Progress", self._progress) + + progress = property(_get_progress, _set_progress, + "The progress of the transaction in percent.") + + def _get_progress_package(self): + return self._progress_package + + def _set_progress_package(self, progress_package): + self._progress_package = self._convert_struct(progress_package, "ss") + + progress_package = property(_get_progress_package, + _set_progress_package, + doc="The last progress update of a currently" + "processed package. A tuple of package " + "name and status enum.") + + def _get_progress_download(self): + return self._progress_download + + def _set_progress_download(self, progress_download): + self._progress_download = self._convert_struct(progress_download, + "sssxxs") + self.PropertyChanged("ProgressDownload", self._progress_download) + + progress_download = property(_get_progress_download, + _set_progress_download, + doc="The last progress update of a currently" + "running download. A tuple of URI, " + "status, short description, full size, " + "partially downloaded size and a status " + "message.") + + def _get_cancellable(self): + return self._cancellable + + def _set_cancellable(self, cancellable): + self._cancellable = dbus.Boolean(cancellable) + self.PropertyChanged("Cancellable", self._cancellable) + + cancellable = property(_get_cancellable, _set_cancellable, + doc="If it's currently allowed to cancel the " + "transaction.") + + def _get_term_attached(self): + return self._term_attached + + def _set_term_attached(self, attached): + self._term_attached = dbus.Boolean(attached) + self.PropertyChanged("TerminalAttached", self._term_attached) + + term_attached = property(_get_term_attached, _set_term_attached, + doc="If the controlling terminal is currently " + "attached to the dpkg call of the " + "transaction.") + + def _get_required_medium(self): + return self._required_medium + + def _set_required_medium(self, medium): + self._required_medium = self._convert_struct(medium, "ss") + self.PropertyChanged("RequiredMedium", self._required_medium) + self.MediumRequired(*self._required_medium) + + required_medium = property(_get_required_medium, _set_required_medium, + doc="Tuple containing the label and the drive " + "of a required CD/DVD to install packages " + "from.") + + def _get_config_file_conflict(self): + return self._config_file_conflict + + def _set_config_file_conflict(self, prompt): + if prompt is None: + self._config_file_conflict = dbus.Struct(("", ""), signature="ss") + return + self._config_file_conflict = self._convert_struct(prompt, "ss") + self.PropertyChanged("ConfigFileConflict", self._config_file_conflict) + self.ConfigFileConflict(*self._config_file_conflict) + + config_file_conflict = property(_get_config_file_conflict, + _set_config_file_conflict, + doc="Tuple containing the old and the new " + "path of the configuration file") + + # Signals + + # pylint: disable-msg=C0103,C0322 + @dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE, + signature="sv") + def PropertyChanged(self, property, value): + """The signal gets emitted if a property of the transaction changed. + + :param property: The name of the property. + :param value: The new value of the property. + + :type property: s + :type value: v + """ + log_trans.debug("Emitting PropertyChanged: %s, %s" % (property, value)) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE, + signature="s") + def Finished(self, exit_state): + """The signal gets emitted if the transaction has been finished. + + :param exit_state: The exit state of the transaction, e.g. + ``exit-failed``. + :type exit_state: s + """ + log_trans.debug("Emitting Finished: %s" % + enums.get_exit_string_from_enum(exit_state)) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE, + signature="ss") + def MediumRequired(self, medium, drive): + """Set and emit the required medium change. + + This method/signal should be used to inform the user to + insert the installation CD/DVD: + + Keyword arguments: + medium -- the CD/DVD label + drive -- mount point of the drive + """ + log_trans.debug("Emitting MediumRequired: %s, %s" % (medium, drive)) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.signal(dbus_interface=APTDAEMON_TRANSACTION_DBUS_INTERFACE, + signature="ss") + def ConfigFileConflict(self, old, new): + """Set and emit the ConfigFileConflict signal. + + This method/signal should be used to inform the user to + answer a config file prompt. + + Keyword arguments: + old -- current version of the configuration prompt + new -- new version of the configuration prompt + """ + log_trans.debug("Emitting ConfigFileConflict: %s, %s" % (old, new)) + + # Methods + + def _set_locale(self, locale_str): + """Set the language and encoding. + + Keyword arguments: + locale -- specifies language, territory and encoding according + to RFC 1766, e.g. "de_DE.UTF-8" + """ + if self.status != enums.STATUS_SETTING_UP: + raise errors.TransactionAlreadyRunning() + if "/" in str(locale_str): + raise ValueError("Security exception: Absolute path for locale") + try: + # ensure locale string is str() and not dbus.String() + (lang, encoding) = locale._parse_localename(str(locale_str)) + except ValueError: + raise + else: + if lang is None: + lang = "C" + self.locale = dbus.String(lang) + else: + self.locale = dbus.String("%s.%s" % (lang, encoding)) + self._translation = gettext.translation("aptdaemon", + fallback=True, + languages=[lang]) + self.PropertyChanged("locale", self.locale) + + @inline_callbacks + def _set_http_proxy(self, url, sender): + """Set an http network proxy. + + Keyword arguments: + url -- the URL of the proxy server, e.g. http://proxy:8080 + """ + if url != "" and (not url.startswith("http://") or ":" not in url): + raise errors.InvalidProxyError(url) + action = policykit1.PK_ACTION_SET_PROXY + yield policykit1.check_authorization_by_name(sender, action, + bus=self.bus) + self.http_proxy = dbus.String(url) + self.PropertyChanged("HttpProxy", self.http_proxy) + + def _set_remove_obsoleted_depends(self, remove_obsoleted_depends): + """Set the handling of the removal of automatically installed + dependencies which are now obsoleted. + + Keyword arguments: + remove_obsoleted_depends -- If True also remove automatically installed + dependencies of to removed packages + """ + self.remove_obsoleted_depends = dbus.Boolean(remove_obsoleted_depends) + self.PropertyChanged("RemoveObsoletedDepends", + self.remove_obsoleted_depends) + + def _set_allow_unauthenticated(self, allow_unauthenticated): + """Set the handling of unauthenticated packages + + Keyword arguments: + allow_unauthenticated -- True to allow packages that come from a + repository without a valid authentication signature + """ + self.allow_unauthenticated = dbus.Boolean(allow_unauthenticated) + self.PropertyChanged("AllowUnauthenticated", + self.allow_unauthenticated) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.method(APTDAEMON_TRANSACTION_DBUS_INTERFACE, + in_signature="s", out_signature="", + sender_keyword="sender") + def RunAfter(self, tid, sender): + """Queue the transaction for processing after the given transaction. + + The transaction will also fail if the previous one failed. Several + transactions can be chained up. + + :param tid: The id of the transaction which should be executed + before. + + :type tid: s + """ + log_trans.info("Queuing transaction %s", self.tid) + try: + trans_before = self.queue.limbo[tid] + except KeyError: + raise Exception("The given transaction doesn't exist or is " + "already queued!") + if trans_before.after: + raise Exception("There is already an after transaction!") + trans_before.after = self + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE, + in_signature="", out_signature="", + sender_keyword="sender") + def Run(self, sender): + """Check the authentication, simulate and queue the transaction for + processing.""" + log_trans.info("Queuing transaction %s", self.tid) + return self._run(sender) + + @inline_callbacks + def _run(self, sender): + yield self._check_foreign_user(sender) + yield self._check_simulated() + yield self._check_auth() + self.queue.put(self.tid) + self.status = enums.STATUS_WAITING + next_trans = self.after + while next_trans: + yield self._check_simulated() + yield next_trans._check_auth() + self.queue.put(next_trans.tid) + next_trans.status = enums.STATUS_WAITING + next_trans = next_trans.after + + @inline_callbacks + def _check_simulated(self): + # Simulate the new transaction if this has not been done before: + # FIXME: Compare the simulated timestamp with the time stamp of + # the status and re-simulate the transaction + if self.simulated is None: + # If there isn't any transaction on the queue we send an early + # progress information. Otherwise it juse seems that aptdaemon + # hangs since it doesn't send any progress information after the + # the transaction has been started + if not self.queue.worker.trans: + self.progress = 9 + yield self._simulate_real() + else: + return + + @inline_callbacks + def _check_auth(self): + """Check silently if one of the high level privileges has been granted + before to reduce clicks to install packages from third party + epositories: AddRepository -> UpdateCache -> InstallPackages + """ + self.status = enums.STATUS_AUTHENTICATING + action = self.ROLE_ACTION_MAP[self.role] + if action is None: + return + # Special case if InstallPackages only touches stuff from the + # high trust whitelist + if (self.role in (enums.ROLE_INSTALL_PACKAGES, + enums.ROLE_COMMIT_PACKAGES) and + trans_only_installs_pkgs_from_high_trust_repos(self)): + action = policykit1.PK_ACTION_INSTALL_PACKAGES_FROM_HIGH_TRUST_REPO + # Special case if CommitPackages only upgrades + if (self.role == enums.ROLE_COMMIT_PACKAGES and + not self.packages[enums.PKGS_INSTALL] and + not self.packages[enums.PKGS_REINSTALL] and + not self.packages[enums.PKGS_REMOVE] and + not self.packages[enums.PKGS_PURGE] and + not self.packages[enums.PKGS_DOWNGRADE]): + action = policykit1.PK_ACTION_UPGRADE_PACKAGES + try: + authorized = yield self._check_alternative_auth() + if not authorized: + yield policykit1.check_authorization_by_name(self.sender, + action, + bus=self.bus) + except errors.NotAuthorizedError as error: + self.error = errors.TransactionFailed(enums.ERROR_NOT_AUTHORIZED, + str(error)) + self.exit = enums.EXIT_FAILED + raise(error) + except errors.AuthorizationFailed as error: + self.error = errors.TransactionFailed(enums.ERROR_AUTH_FAILED, + str(error)) + self.exit = enums.EXIT_FAILED + raise(error) + + @inline_callbacks + def _check_alternative_auth(self): + """Check non-interactively if one of the high level privileges + has been granted. + """ + if self.role not in [enums.ROLE_ADD_REPOSITORY, + enums.ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER, + enums.ROLE_UPDATE_CACHE, + enums.ROLE_INSTALL_PACKAGES, + enums.ROLE_ADD_LICENSE_KEY]: + return_value(False) + flags = policykit1.CHECK_AUTH_NONE + for action in [policykit1.PK_ACTION_INSTALL_PACKAGES_FROM_NEW_REPO, + policykit1.PK_ACTION_INSTALL_PURCHASED_PACKAGES]: + try: + yield policykit1.check_authorization_by_name(self.sender, + action, + bus=self.bus, + flags=flags) + except errors.NotAuthorizedError: + continue + else: + return_value(True) + return_value(False) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE, + in_signature="", out_signature="", + sender_keyword="sender") + def Cancel(self, sender): + """Cancel the transaction.""" + log_trans.info("Cancelling transaction %s", self.tid) + return self._cancel(sender) + + @inline_callbacks + def _cancel(self, sender): + try: + yield self._check_foreign_user(sender) + except errors.ForeignTransaction: + action = policykit1.PK_ACTION_CANCEL_FOREIGN + yield policykit1.check_authorization_by_name(sender, action, + bus=self.bus) + try: + self.queue.remove(self) + log_trans.debug("Removed transaction from queue") + except ValueError: + pass + else: + self.status = enums.STATUS_CANCELLING + self.exit = enums.EXIT_CANCELLED + return + if self.tid in self.queue.limbo: + self.exit = enums.EXIT_CANCELLED + return + elif self.cancellable: + log_trans.debug("Setting cancel event") + self.cancelled = True + self.status = enums.STATUS_CANCELLING + self.paused = False + return + raise errors.AptDaemonError("Could not cancel transaction") + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE, + in_signature="", out_signature="", + sender_keyword="sender") + def Simulate(self, sender): + """Simulate a transaction to update its dependencies, download size + and required disk space. + + Call this method if you want to show changes before queuing the + transaction. + """ + log_trans.info("Simulate was called") + return self._simulate(sender) + + @inline_callbacks + def _simulate(self, sender): + if self._simulated_cb: + raise errors.TransactionAlreadySimulating() + if self.status != enums.STATUS_SETTING_UP: + raise errors.TransactionAlreadyRunning() + yield self._check_foreign_user(sender) + yield self._simulate_real() + + @inline_callbacks + def _simulate_real(self): + if self._simulated_cb: + raise errors.TransactionAlreadySimulating() + if self.role == enums.ROLE_INSTALL_FILE: + yield self._check_auth() + self.queue.worker.simulate(self) + deferred = Deferred() + if self._idle_watch is not None: + GLib.source_remove(self._idle_watch) + self._idle_watch = None + self._simulated_cb = self.queue.worker.connect( + "transaction-simulated", + self._on_transaction_simulated, + deferred) + yield deferred + + def _on_transaction_simulated(self, worker, trans, deferred): + if trans is not self: + return + self.queue.worker.disconnect(self._simulated_cb) + self._simualted_cb = None + if trans.error: + deferred.errback(trans.error) + else: + deferred.callback() + + def _set_terminal(self, ttyname): + """Set the controlling terminal. + + The worker will be attached to the specified slave end of a pty + master/slave pair. This allows to interact with the + + Can only be changed before the transaction is started. + + Keyword arguments: + ttyname -- file path to the slave file descriptor + """ + if self.status != enums.STATUS_SETTING_UP: + raise errors.TransactionAlreadyRunning() + with set_euid_egid(self.uid, self.gid): + if os.path.dirname(ttyname) != "/dev/pts": + raise errors.AptDaemonError("%s isn't a tty" % ttyname) + + slave_fd = None + try: + slave_fd = os.open(ttyname, os.O_RDWR | os.O_NOCTTY) + except Exception: + raise errors.AptDaemonError("Could not open %s" % ttyname) + else: + if os.fstat(slave_fd).st_uid != self.uid: + raise errors.AptDaemonError("Pty device '%s' has to be owned by" + "the owner of the transaction " + "(uid %s) " % (ttyname, self.uid)) + if os.isatty(slave_fd): + self.terminal = dbus.String(ttyname) + self.PropertyChanged("Terminal", self.terminal) + else: + raise errors.AptDaemonError("%s isn't a tty" % ttyname) + finally: + if slave_fd is not None: + os.close(slave_fd) + + def _set_debconf(self, debconf_socket): + """Set the socket of the debconf proxy. + + The worker process forwards all debconf commands through this + socket by using the passthrough frontend. On the client side + debconf-communicate should be connected to the socket. + + Can only be changed before the transaction is started. + + Keyword arguments: + debconf_socket: absolute path to the socket + """ + if self.status != enums.STATUS_SETTING_UP: + raise errors.TransactionAlreadyRunning() + with set_euid_egid(self.uid, self.gid): + try: + stat = os.stat(debconf_socket) + except Exception: + raise errors.AptDaemonError("socket status could not be read: " + "%s" % debconf_socket) + else: + if stat.st_uid != self.uid: + raise errors.AptDaemonError("socket '%s' has to be owned by the " + "owner of the " + "transaction" % debconf_socket) + self.debconf = dbus.String(debconf_socket) + self.PropertyChanged("DebconfSocket", self.debconf) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE, + in_signature="s", out_signature="", + sender_keyword="sender") + def ProvideMedium(self, medium, sender): + """Continue paused transaction with the inserted medium. + + If a media change is required to install packages from CD/DVD + the transaction will be paused and could be resumed with this + method. + + :param medium: The label of the CD/DVD. + :type medium: s + """ + log_trans.info("Medium %s was provided", medium) + return self._provide_medium(medium, sender) + + @inline_callbacks + def _provide_medium(self, medium, sender): + yield self._check_foreign_user(sender) + if not self.required_medium: + raise errors.AptDaemonError("There isn't any required medium.") + if not self.required_medium[0] == medium: + raise errors.AptDaemonError("The medium '%s' isn't " + "requested." % medium) + self.paused = False + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_TRANSACTION_DBUS_INTERFACE, + in_signature="ss", out_signature="", + sender_keyword="sender") + def ResolveConfigFileConflict(self, config, answer, sender): + """Resolve a configuration file conflict and continue the transaction. + + If a config file prompt is detected the transaction will be + paused and could be resumed with this method. + + :param config: The path to the original config file. + :param answer: The answer to the configuration file question, can be + "keep" or "replace" + + :type config: s + :type answer: s + """ + log_trans.info("Resolved conflict of %s with %s", config, answer) + return self._resolve_config_file_conflict(config, answer, sender) + + @inline_callbacks + def _resolve_config_file_conflict(self, config, answer, sender): + yield self._check_foreign_user(sender) + if not self.config_file_conflict: + raise errors.AptDaemonError("There isn't any config file prompt " + "required") + if answer not in ["keep", "replace"]: + # FIXME: should we re-send the config file prompt + # message or assume the client is buggy and + # just use a safe default (like keep)? + raise errors.AptDaemonError("Invalid value: %s" % answer) + if not self.config_file_conflict[0] == config: + raise errors.AptDaemonError("Invalid config file: %s" % config) + self.config_file_conflict_resolution = answer + self.paused = False + + @inline_callbacks + def _set_property(self, iface, name, value, sender): + """Helper to set a name on the properties D-Bus interface.""" + yield self._check_foreign_user(sender) + if iface == APTDAEMON_TRANSACTION_DBUS_INTERFACE: + if name == "MetaData": + self._set_meta_data(value) + elif name == "Terminal": + self._set_terminal(value) + elif name == "DebconfSocket": + self._set_debconf(value) + elif name == "Locale": + self._set_locale(value) + elif name == "RemoveObsoletedDepends": + self._set_remove_obsoleted_depends(value) + elif name == "AllowUnauthenticated": + self._set_allow_unauthenticated(value) + elif name == "HttpProxy": + self._set_http_proxy(value, sender) + else: + raise dbus.exceptions.DBusException("Unknown or read only " + "property: %s" % name) + else: + raise dbus.exceptions.DBusException("Unknown interface: %s" % + iface) + + def _get_properties(self, iface): + """Helper to get the properties of a D-Bus interface.""" + if iface == APTDAEMON_TRANSACTION_DBUS_INTERFACE: + return {"Role": self.role, + "Progress": self.progress, + "ProgressDetails": self.progress_details, + "ProgressDownload": self.progress_download, + "Status": self.status, + "StatusDetails": self.status_details, + "Cancellable": self.cancellable, + "TerminalAttached": self.term_attached, + "RequiredMedium": self.required_medium, + "ConfigFileConflict": self.config_file_conflict, + "ExitState": self.exit, + "Error": self._error_property, + "Locale": self.locale, + "Terminal": self.terminal, + "DebconfSocket": self.debconf, + "Paused": dbus.Boolean(self.paused), + "AllowUnauthenticated": self.allow_unauthenticated, + "RemoveObsoletedDepends": self.remove_obsoleted_depends, + "HttpProxy": self.http_proxy, + "Packages": self.packages, + "MetaData": self.meta_data, + "Dependencies": self.depends, + "Download": self.download, + "Space": self.space, + "Unauthenticated": self.unauthenticated, + } + else: + return {} + + @inline_callbacks + def _check_foreign_user(self, dbus_name): + """Check if the transaction is owned by the given caller.""" + uid = yield policykit1.get_uid_from_dbus_name(dbus_name, self.bus) + if self.uid != uid: + raise errors.ForeignTransaction() + + def _set_kwargs(self, kwargs): + """Set the kwargs which will be send to the AptWorker.""" + self.kwargs = kwargs + + def _get_translations(self): + """Get a usable translations object, no matter what.""" + if self._translation: + return self._translation + else: + domain = "aptdaemon" + return gettext.translation(domain, gettext.bindtextdomain(domain), + gettext.bind_textdomain_codeset(domain), + fallback=True) + + def gettext(self, msg): + """Translate the given message to the language of the transaction. + Fallback to the system default. + """ + # Avoid showing the header of the mo file for an empty string + if not msg: + return "" + translation = self._get_translations() + return getattr(translation, _gettext_method)(msg) + + def ngettext(self, singular, plural, count): + """Translate the given plural message to the language of the + transaction. Fallback to the system default. + """ + translation = self._get_translations() + return getattr(translation, _ngettext_method)(singular, plural, count) + + +class TransactionQueue(GObject.GObject): + + """Queue for transactions.""" + + __gsignals__ = {"queue-changed": (GObject.SignalFlags.RUN_FIRST, + None, + ())} + + def __init__(self, worker): + """Intialize a new TransactionQueue instance.""" + GObject.GObject.__init__(self) + self._queue = collections.deque() + self._proc_count = 0 + self.worker = worker + # Used to keep track of not yet queued transactions + self.limbo = {} + self.worker.connect("transaction-done", self._on_transaction_done) + + def __len__(self): + return len(self._queue) + + def _emit_queue_changed(self): + """Emit the queued-changed signal.""" + log.debug("emitting queue changed") + self.emit("queue-changed") + + def put(self, tid): + """Add an item to the queue.""" + trans = self.limbo.pop(tid) + if trans._idle_watch is not None: + GLib.source_remove(trans._idle_watch) + if self.worker.trans: + trans.status = enums.STATUS_WAITING + self._queue.append(trans) + else: + self.worker.run(trans) + self._emit_queue_changed() + + def _on_transaction_done(self, worker, trans): + """Mark the last item as done and request a new item.""" + # FIXME: Check if the transaction failed because of a broken system or + # if dpkg journal is dirty. If so allready queued transactions + # except the repair transactions should be removed from the queue + if trans.exit in [enums.EXIT_FAILED, enums.EXIT_CANCELLED]: + if trans.exit == enums.EXIT_FAILED: + exit = enums.EXIT_PREVIOUS_FAILED + else: + exit = enums.EXIT_CANCELLED + _trans = trans.after + while _trans: + self.remove(_trans) + _trans.exit = exit + msg = enums.get_role_error_from_enum(trans.role) + _trans.status_details = msg + _trans = _trans.after + try: + next_trans = self._queue.popleft() + except IndexError: + log.debug("There isn't any queued transaction") + else: + self.worker.run(next_trans) + self._emit_queue_changed() + + def remove(self, transaction): + """Remove the specified item from the queue.""" + self._queue.remove(transaction) + self._emit_queue_changed() + + def clear(self): + """Remove all items from the queue.""" + for transaction in self._queue: + transaction._remove_from_connection_no_raise() + self._queue.clear() + + @property + def items(self): + """Return a list containing all queued items.""" + return list(self._queue) + + +class AptDaemon(DBusObject): + + """Provides a system daemon to process package management tasks. + + The daemon is transaction based. Each package management tasks runs + in a separate transaction. The transactions can be created, + monitored and managed via the D-Bus interface. + """ + + def __init__(self, options, connect=True, bus=None): + """Initialize a new AptDaemon instance. + + Keyword arguments: + options -- command line options of the type optparse.Values + connect -- if the daemon should connect to the D-Bus (default is True) + bus -- the D-Bus to connect to (defaults to the system bus) + """ + log.info("Initializing daemon") + # glib does not support SIGQUIT + # GLib.unix_signal_add_full( + # GLib.PRIORITY_HIGH, signal.SIGQUIT, self._sigquit, None) + GLib.unix_signal_add(GLib.PRIORITY_HIGH, signal.SIGTERM, + self._sigquit, None) + # Decrease the priority of the daemon to avoid blocking UI + os.nice(5) + self.options = options + self.packagekit = None + if connect is True: + if bus is None: + bus = dbus.SystemBus() + self.bus = bus + bus_path = APTDAEMON_DBUS_PATH + # Check if another object has already registered the name on + # the bus. Quit the other daemon if replace would be set + try: + bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE, + bus, + do_not_queue=True) + except dbus.exceptions.NameExistsException: + if self.options.replace is False: + log.critical("Another daemon is already running") + sys.exit(1) + log.warning("Replacing already running daemon") + the_other_guy = bus.get_object(APTDAEMON_DBUS_SERVICE, + APTDAEMON_DBUS_PATH) + the_other_guy.Quit(dbus_interface=APTDAEMON_DBUS_INTERFACE, + timeout=300) + time.sleep(1) + bus_name = dbus.service.BusName(APTDAEMON_DBUS_SERVICE, + bus, + do_not_queue=True) + else: + bus_name = None + bus_path = None + DBusObject.__init__(self, bus_name, bus_path) + if options.dummy: + self.worker = DummyWorker() + else: + load_plugins = not options.disable_plugins + try: + from .worker.pkworker import AptPackageKitWorker + self.worker = AptPackageKitWorker(options.chroot, + load_plugins) + except: + self.worker = AptWorker(options.chroot, load_plugins) + self.queue = TransactionQueue(self.worker) + self.queue.connect("queue-changed", self._on_queue_changed) + # keep state of the last information about reboot required + self._reboot_required = self.worker.is_reboot_required() + log.debug("Daemon was initialized") + + def _on_queue_changed(self, queue): + """Callback for a changed transaction queue.""" + # check for reboot required + if self.worker.is_reboot_required() != self._reboot_required: + self._reboot_required = self.worker.is_reboot_required() + self.PropertyChanged("RebootRequired", self._reboot_required) + # check for the queue + if self.queue.worker.trans: + current = self.queue.worker.trans.tid + else: + current = "" + queued = [trans.tid for trans in self.queue.items] + self.ActiveTransactionsChanged(current, queued) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.signal(dbus_interface=APTDAEMON_DBUS_INTERFACE, + signature="sv") + def PropertyChanged(self, property, value): + """The signal gets emitted if a property of the transaction changed. + + :param property: The name of the property. + :param value: The new value of the property. + + :type property: s + :type value: v + """ + log.debug("Emitting PropertyChanged: %s, %s" % (property, value)) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.signal(dbus_interface=APTDAEMON_DBUS_INTERFACE, + signature="sas") + def ActiveTransactionsChanged(self, current, queued): + """The currently processed or the queued transactions changed. + + :param current: The path of the currently running transaction or + an empty string. + :param queued: List of the ids of the queued transactions. + + :type current: s + :type queued: as + """ + log.debug("Emitting ActiveTransactionsChanged signal: %s, %s", + current, queued) + + def run(self): + """Start the daemon and listen for calls.""" + if self.options.disable_timeout is False: + log.debug("Using inactivity check") + GLib.timeout_add_seconds(APTDAEMON_IDLE_CHECK_INTERVAL, + self._check_for_inactivity) + log.debug("Waiting for calls") + try: + mainloop.run() + except KeyboardInterrupt: + self.Quit(None) + + @inline_callbacks + def _create_trans(self, role, sender, packages=None, kwargs=None): + """Helper method which returns the tid of a new transaction.""" + pid, uid, gid, cmdline = ( + yield policykit1.get_proc_info_from_dbus_name(sender, self.bus)) + tid = uuid.uuid4().hex + trans = Transaction( + tid, role, self.queue, pid, uid, gid, cmdline, sender, + packages=packages, kwargs=kwargs, bus=self.bus) + self.queue.limbo[trans.tid] = trans + return_value(trans.tid) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="", out_signature="s", + sender_keyword="sender") + def FixIncompleteInstall(self, sender): + """Try to complete cancelled installations. This is equivalent to a + call of ``dpkg --configure -a``. + + Requires the ``org.debian.apt.install-or-remove-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("FixIncompleteInstall() called") + return self._create_trans(enums.ROLE_FIX_INCOMPLETE_INSTALL, sender) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="", out_signature="s", + sender_keyword="sender") + def FixBrokenDepends(self, sender): + """Try to resolve unsatisfied dependencies of installed packages. + + Requires the ``org.debian.apt.install-or-remove-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("FixBrokenDepends() called") + return self._create_trans(enums.ROLE_FIX_BROKEN_DEPENDS, sender) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="", out_signature="s", + sender_keyword="sender") + def UpdateCache(self, sender): + """Download the latest information about available packages from the + repositories and rebuild the package cache. + + Requires the ``org.debian.apt.update-cache`` + :ref:`PolicyKit privilege <policykit>`. + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("UpdateCache() was called") + kwargs = {"sources_list": None} + return self._create_trans(enums.ROLE_UPDATE_CACHE, sender, + kwargs=kwargs) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="s", out_signature="s", + sender_keyword="sender") + def UpdateCachePartially(self, sources_list, sender): + """Update the cache from the repositories defined in the given + sources.list only. + + Requires the ``org.debian.apt.update-cache`` + :ref:`PolicyKit privilege <policykit>`. + + :param sources_list: The absolute path to a sources.list, e.g. + :file:`/etc/apt/sources.list.d/ppa-aptdaemon.list` or the name + of the snippet in :file:`/etc/apt/sources.list.d/`, e.g. + :file:`ppa-aptdaemon.list`. + :type sources_list: s + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("UpdateCachePartially() was called") + kwargs = {"sources_list": sources_list} + return self._create_trans(enums.ROLE_UPDATE_CACHE, sender, + kwargs=kwargs) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="as", out_signature="s", + sender_keyword="sender") + def RemovePackages(self, package_names, sender): + """Remove the given packages from the system. The configuration files + will be kept by default. Use :func:`CommitPackages()` to also purge the + configuration files. + + Requires the ``org.debian.apt.install-or-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :param package_names: packages to be removed + :type package_names: as + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("RemovePackages() was called: '%s'", package_names) + self._check_package_names(package_names) + return self._create_trans(enums.ROLE_REMOVE_PACKAGES, sender, + packages=([], [], package_names, [], [], [])) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="b", out_signature="s", + sender_keyword="sender") + def UpgradeSystem(self, safe_mode, sender): + """Apply all available upgrades and try to resolve conflicts. + + Requires the ``org.debian.apt.upgrade-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :param safe_mode: If True only already installed packages will be + updated. Updates which require to remove installed packages or to + install additional packages will be skipped. + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("UpgradeSystem() was called with safe mode: " + "%s" % safe_mode) + return self._create_trans(enums.ROLE_UPGRADE_SYSTEM, sender, + kwargs={"safe_mode": safe_mode}) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="asasasasasas", out_signature="s", + sender_keyword="sender") + def CommitPackages(self, install, reinstall, remove, purge, upgrade, + downgrade, sender): + """Perform several package changes at the same time. + + The version number and target release of the packages can be specified + using the traditional apt-get syntax, e.g. "xterm=281.1" to force + installing the version 281.1 of xterm or "xterm/experimental" to + force installing xterm from the experimental release. + + Requires the ``org.debian.apt.install-or-remove-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :param install: Packages to be installed. + :param reinstall: Packages to be re-installed + :param remove: Packages to be removed + :param purge: Package to be removed including theirs configuration + files. + :param upgrade: Packages to be upgraded. + :param downgrade: Packages to be downgraded. You + have to append the target version to the package name separated + by "=" + + :type install: as + :type reinstall: as + :type remove: as + :type purge: as + :type upgrade: as + :type downgrade: as + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + # FIXME: take sha1 or md5 cash into accout to allow selecting a version + # or an origin different from the candidate + log.info("CommitPackages() was called: %s, %s, %s, %s, %s, %s", + install, reinstall, remove, purge, upgrade, downgrade) + + def check_empty_list(lst): + if lst == [""]: + return [] + else: + return lst + packages_lst = [check_empty_list(lst) for lst in [install, reinstall, + remove, purge, + upgrade, + downgrade]] + for packages in packages_lst: + self._check_package_names(packages) + return self._create_trans(enums.ROLE_COMMIT_PACKAGES, sender, + packages=packages_lst) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="as", out_signature="s", + sender_keyword="sender") + def InstallPackages(self, package_names, sender): + """Fetch and install the given packages from the repositories. + + The version number and target release of the packages can be specified + using the traditional apt-get syntax, e.g. "xterm=281.1" to force + installing the version 281.1 of xterm or "xterm/experimental" to + force installing xterm from the experimental release. + + Requires the ``org.debian.apt.install-or-remove-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :param package_names: Packages to be upgraded + :type package_names: as + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("InstallPackages() was called: %s" % package_names) + self._check_package_names(package_names) + return self._create_trans(enums.ROLE_INSTALL_PACKAGES, sender, + packages=(package_names, [], [], [], [], [])) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="as", out_signature="s", + sender_keyword="sender") + def UpgradePackages(self, package_names, sender): + """Upgrade the given packages to their latest version. + + The version number and target release of the packages can be specified + using the traditional apt-get syntax, e.g. "xterm=281.1" to force + installing the version 281.1 of xterm or "xterm/experimental" to + force installing xterm from the experimental release. + + Requires the ``org.debian.apt.upgrade-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :param package_names: Packages to be upgraded + :type package_names: as + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("UpgradePackages() was called: %s" % package_names) + self._check_package_names(package_names) + return self._create_trans(enums.ROLE_UPGRADE_PACKAGES, sender, + packages=([], [], [], [], package_names, [])) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="ss", out_signature="s", + sender_keyword="sender") + def AddVendorKeyFromKeyserver(self, keyid, keyserver, sender): + """Download and install the key of a software vendor. The key is + used to authenticate packages of the vendor. + + Requires the ``org.debian.apt.change-repositories`` + :ref:`PolicyKit privilege <policykit>`. + + :param keyid: The id of the GnuPG key (e.g. 0x0EB12F05) + :param keyserver: The server to get the key from (e.g. + keyserver.ubuntu.com) + + :type keyid: s + :type keyserver: s + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("InstallVendorKeyFromKeyserver() was called: %s %s", + keyid, keyserver) + return self._create_trans(enums.ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER, + sender, kwargs={"keyid": keyid, + "keyserver": keyserver}) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="s", out_signature="s", + sender_keyword="sender") + def AddVendorKeyFromFile(self, path, sender): + """Install the key file of a software vendor. The key is + used to authenticate packages of the vendor. + + Requires the ``org.debian.apt.change-repositories`` + :ref:`PolicyKit privilege <policykit>`. + + :param path: The absolute path to the key file. + :type path: s + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("InstallVendorKeyFile() was called: %s" % path) + return self._create_trans(enums.ROLE_ADD_VENDOR_KEY_FILE, + sender, kwargs={"path": path}) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="s", out_signature="s", + sender_keyword="sender") + def RemoveVendorKey(self, fingerprint, sender): + """Remove the given key of a software vendor. The key is used to + authenticate packages of the vendor. + + Requires the ``org.debian.apt.change-repositories`` + :ref:`PolicyKit privilege <policykit>`. + + :param fingerprint: The fingerprint of the key. + :type fingerprint: s + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("RemoveVendorKey() was called: %s" % fingerprint) + return self._create_trans(enums.ROLE_REMOVE_VENDOR_KEY, + sender, kwargs={"fingerprint": fingerprint}) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="sb", out_signature="s", + sender_keyword="sender") + def InstallFile(self, path, force, sender): + """Install the given local package file. + + Requires the ``org.debian.apt.install-file`` + :ref:`PolicyKit privilege <policykit>`. + + :param path: The absolute path to the package file. + :param force: If the installation of a package which violates the + Debian/Ubuntu policy should be forced. + + :type path: s + :type force: b + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("InstallFile() was called: %s" % path) + # FIXME: Perform some checks + # FIXME: Should we already extract the package name here? + return self._create_trans(enums.ROLE_INSTALL_FILE, + sender, kwargs={"path": path, + "force": force}) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="", out_signature="s", + sender_keyword="sender") + def Clean(self, sender): + """Remove downloaded package files. + + Requires the ``org.debian.apt.clean`` + :ref:`PolicyKit privilege <policykit>`. + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("Clean() was called") + return self._create_trans(enums.ROLE_CLEAN, sender) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="ass", out_signature="s", + sender_keyword="sender") + def Reconfigure(self, packages, priority, sender): + """Reconfigure already installed packages. + + Requires the ``org.debian.apt.install-or-remove-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :param packages: List of package names which should be reconfigure. + :param priority: The minimum debconf priority of question to be + displayed. Can be of value "low", "medium", "high", "critical", + "default". + + :type packages: as + :type priority: s + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("Reconfigure() was called: %s" % " ".join(packages)) + return self._create_trans(enums.ROLE_RECONFIGURE, sender, + packages=[[], packages, [], [], [], []], + kwargs={"priority": priority}) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="sssasss", out_signature="s", + sender_keyword="sender") + def AddRepository(self, src_type, uri, dist, comps, comment, sourcesfile, + sender): + """Add given repository to the sources list. + + Requires the ``org.debian.apt.change-repositories`` + :ref:`PolicyKit privilege <policykit>`. + + :param src_type: The type of the repository (deb, deb-src). + :param uri: The main repository URI + (e.g. http://archive.ubuntu.com/ubuntu) + :param dist: The distribution to use (e.g. stable or lenny-backports). + :param comps: List of components (e.g. main, restricted). + :param comment: A comment which should be added to the sources.list. + :param sourcesfile: (Optoinal) filename in sources.list.d. + + :type src_type: s + :type uri: s + :type dist: s + :type comps: as + :type comment: s + :type sourcesfile: s + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("AddRepository() was called: type='%s' uri='%s' " + "dist='%s' comps='%s' comment='%s' sourcesfile='%s'", + src_type, uri, dist, comps, comment, sourcesfile) + return self._create_trans(enums.ROLE_ADD_REPOSITORY, sender, + kwargs={"src_type": src_type, "uri": uri, + "dist": dist, "comps": comps, + "comment": comment, + "sourcesfile": sourcesfile}) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="s", out_signature="s", + sender_keyword="sender") + def EnableDistroComponent(self, component, sender): + """Enable the component in the distribution repositories. This will + not affect third-party repositories. + + The repositories of a distribution are often separated into + different components because of policy reasons. E.g. Debian uses main + for DFSG-free software and non-free for re-distributable but not free + in the sense of the Debian Free Software Guidelines. + + Requires the ``org.debian.apt.change-repositories`` + :ref:`PolicyKit privilege <policykit>`. + + :param component: The component, e,g, main or non-free. + :type component: s + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("EnableComponent() was called: component='%s' ", component) + return self._create_trans(enums.ROLE_ENABLE_DISTRO_COMP, sender, + kwargs={"component": component}) + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="", out_signature="as", + sender_keyword="sender") + def GetTrustedVendorKeys(self, sender): + """Get the list of the installed vendor keys which are used to + authenticate packages. + + Requires the ``org.debian.apt.get-trusted-vendor-keys`` + :ref:`PolicyKit privilege <policykit>`. + + :returns: Fingerprints of all installed keys. + """ + log.info("GetTrustedVendorKeys() was called") + return self._get_trusted_vendor_keys(sender) + + @inline_callbacks + def _get_trusted_vendor_keys(self, sender): + action = policykit1.PK_ACTION_GET_TRUSTED_VENDOR_KEYS + yield policykit1.check_authorization_by_name(sender, action, + bus=self.bus) + fingerprints = self.worker.get_trusted_vendor_keys() + return_value(fingerprints) + + # pylint: disable-msg=C0103,C0322 + @dbus.service.method(APTDAEMON_DBUS_INTERFACE, + in_signature="", out_signature="sas") + def GetActiveTransactions(self): + """Return the currently running transaction and the list of queued + transactions. + """ + log.debug("GetActiveTransactions() was called") + queued = [trans.tid for trans in self.queue.items] + if self.queue.worker.trans: + current = self.queue.worker.trans.tid + else: + current = "" + return current, queued + + # pylint: disable-msg=C0103,C0322 + @dbus.service.method(APTDAEMON_DBUS_INTERFACE, + in_signature="", out_signature="", + sender_keyword="caller_name") + def Quit(self, caller_name): + """Request a shutdown of the daemon.""" + log.info("Quitting was requested") + log.debug("Quitting main loop...") + mainloop.quit() + log.debug("Exit") + + # pylint: disable-msg=C0103,C0322 + @dbus_deferred_method(APTDAEMON_DBUS_INTERFACE, + in_signature="sss", out_signature="s", + sender_keyword="sender") + def AddLicenseKey(self, pkg_name, json_token, server_name, sender): + """Install a license key to use a piece of proprietary software. + + Requires the ``org.debian.apt.install-or-remove-packages`` + :ref:`PolicyKit privilege <policykit>`. + + :param pkg_name: The name of the package which requires the license + :type pkg_name: s + :param json_token: The oauth token to use with the server in + json format + :type pkg_name: s + :param server_name: The name of the server to use (ubuntu-production, + ubuntu-staging) + :type pkg_name: s + + :returns: The D-Bus path of the new transaction object which + performs this action. + """ + log.info("AddLicenseKey() was called") + return self._create_trans(enums.ROLE_ADD_LICENSE_KEY, sender, + kwargs={'pkg_name': pkg_name, + 'json_token': json_token, + 'server_name': server_name}) + + @inline_callbacks + def _set_property(self, iface, name, value, sender): + """Helper to set a property on the properties D-Bus interface.""" + action = policykit1.PK_ACTION_CHANGE_CONFIG + yield policykit1.check_authorization_by_name(sender, action, + bus=self.bus) + if iface == APTDAEMON_DBUS_INTERFACE: + if name == "PopConParticipation": + self.worker.set_config(name, dbus.Boolean(value)) + elif name == "AutoUpdateInterval": + self.worker.set_config(name, dbus.Int32(value), "10periodic") + elif name == "AutoDownload": + self.worker.set_config(name, dbus.Boolean(value), "10periodic") + elif name == "AutoCleanInterval": + self.worker.set_config(name, dbus.Int32(value), "10periodic") + elif name == "UnattendedUpgrade": + self.worker.set_config(name, dbus.Boolean(value), "10periodic") + else: + raise dbus.exceptions.DBusException("Unknown or read only " + "property: %s" % name) + else: + raise dbus.exceptions.DBusException("Unknown interface: %s" % + iface) + + def _check_package_names(self, pkg_names): + """Check if the package names are valid. Otherwise raise an + exception. + """ + for fullname in pkg_names: + name, version, release = split_package_id(fullname) + name, sep, auto_flag = name.partition("#") + if not auto_flag in ("", "auto"): + raise errors.AptDaemonError("%s isn't a valid flag" % + auto_flag) + if not re.match(REGEX_VALID_PACKAGENAME, name): + raise errors.AptDaemonError("%s isn't a valid package name" % + name) + if (version is not None and + not re.match(REGEX_VALID_VERSION, version)): + raise errors.AptDaemonError("%s isn't a valid version" % + version) + if (release is not None and + not re.match(REGEX_VALID_RELEASE, release)): + raise errors.AptDaemonError("%s isn't a valid release" % + release) + + def _get_properties(self, iface): + """Helper get the properties of a D-Bus interface.""" + if iface == APTDAEMON_DBUS_INTERFACE: + return { + "AutoUpdateInterval": dbus.Int32( + self.worker.get_config("AutoUpdateInterval")), + "AutoDownload": dbus.Boolean( + self.worker.get_config("AutoDownload")), + "AutoCleanInterval": dbus.Int32( + self.worker.get_config("AutoCleanInterval")), + "UnattendedUpgrade": dbus.Int32( + self.worker.get_config("UnattendedUpgrade")), + "PopConParticipation": dbus.Boolean( + self.worker.get_config("PopConParticipation")), + "RebootRequired": dbus.Boolean( + self.worker.is_reboot_required())} + else: + return {} + + def _sigquit(self, data): + """Internal callback for the quit signal.""" + self.Quit(None) + + def _check_for_inactivity(self): + """Shutdown the daemon if it has been inactive for time specified + in APTDAEMON_IDLE_TIMEOUT. + """ + log.debug("Checking for inactivity") + timestamp = self.queue.worker.last_action_timestamp + if (not self.queue.worker.trans and + not GLib.main_context_default().pending() and + time.time() - timestamp > APTDAEMON_IDLE_TIMEOUT and + not self.queue): + log.info("Quitting due to inactivity") + self.Quit(None) + return False + return True + + +def get_dbus_string(text, encoding="UTF-8"): + """Convert the given string or unicode object to a dbus.String.""" + try: + return dbus.String(text) + except UnicodeDecodeError: + return dbus.String(text.decode(encoding, "ignore")) + + +def main(): + """Allow to run the daemon from the command line.""" + parser = OptionParser() + parser.add_option("-t", "--disable-timeout", + default=False, + action="store_true", dest="disable_timeout", + help=_("Do not shutdown the daemon because of " + "inactivity")) + parser.add_option("", "--disable-plugins", + default=False, + action="store_true", dest="disable_plugins", + help=_("Do not load any plugins")) + parser.add_option("-d", "--debug", + default=False, + action="store_true", dest="debug", + help=_("Show internal processing " + "information")) + parser.add_option("-r", "--replace", + default=False, + action="store_true", dest="replace", + help=_("Quit and replace an already running " + "daemon")) + parser.add_option("", "--session-bus", + default=False, + action="store_true", dest="session_bus", + help=_("Listen on the DBus session bus (Only required " + "for testing")) + parser.add_option("", "--chroot", default=None, + action="store", type="string", dest="chroot", + help=_("Perform operations in the given " + "chroot")) + parser.add_option("-p", "--profile", + default=False, + action="store", type="string", dest="profile", + help=_("Store profile stats in the specified " + "file")) + parser.add_option("--dummy", + default=False, + action="store_true", dest="dummy", + help=_("Do not make any changes to the system (Only " + "of use to developers)")) + options, args = parser.parse_args() + if options.debug is True: + log.setLevel(logging.DEBUG) + else: + log.setLevel(logging.INFO) + _console_handler.setLevel(logging.INFO) + if options.session_bus: + bus = dbus.SessionBus() + else: + bus = None + daemon = AptDaemon(options, bus=bus) + if options.profile: + import profile + profiler = profile.Profile() + profiler.runcall(daemon.run) + profiler.dump_stats(options.profile) + profiler.print_stats() + else: + daemon.run() + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/crash.py b/aptdaemon/crash.py new file mode 100644 index 0000000..570b684 --- /dev/null +++ b/aptdaemon/crash.py @@ -0,0 +1,79 @@ +"""Apport integration to provide better problem reports.""" +# Copyright (C) 2010 Sebastian Heinlein <devel@glatzor.de> +# +# Licensed under the GNU General Public License Version 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("create_report") + +import os + +import apport +import apport.fileutils +import apt_pkg + +from . import enums + + +def create_report(error, traceback, trans=None): + """Create an apport problem report for a given crash. + + :param error: The summary of the error. + :param traceback: The traceback of the exception. + :param trans: The optional transaction in which the crash occured. + """ + if not apport.packaging.enabled() or os.getcwd() != "/": + return + + uid = 0 + report = apport.Report("Crash") + report["Title"] = error + package = "aptdaemon" + try: + package_version = apport.packaging.get_version(package) + except ValueError as e: + if 'does not exist' in e.message: + package_version = 'unknown' + report['Package'] = '%s %s' % (package, package_version) + report["SourcePackage"] = "aptdaemon" + report["Traceback"] = traceback + report["ExecutablePath"] = "/usr/sbin/aptd" + report.add_os_info() + + # Attach information about the transaction + if trans: + report["Annotation"] = enums.get_role_error_from_enum(trans.role) + report["TransactionRole"] = trans.role + report["TransactionPackages"] = str([list(l) for l in trans.packages]) + report["TransactionDepends"] = str([list(l) for l in trans.depends]) + report["TransactionKwargs"] = str(trans.kwargs) + report["TransactionLocale"] = trans.locale + report["TransactionOutput"] = trans.output + report["TransactionErrorCode"] = trans._error_property[0] + report["TransactionErrorDetails"] = trans._error_property[1] + uid = os.path.basename(trans.tid) + + # Write report + with apport.fileutils.make_report_file(report, uid) as f: + report.write(f) + +if __name__ == "__main__": + apt_pkg.init_config() + create_report('test', 'testtrace') + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/debconf.py b/aptdaemon/debconf.py new file mode 100644 index 0000000..75a28ef --- /dev/null +++ b/aptdaemon/debconf.py @@ -0,0 +1,180 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Integration of debconf on the client side + +Provides the DebconfProxy class which allows to run the debconf frontend +as normal user by connecting to the root running debconf through the +socket of the passthrough frontend. +""" +# Copyright (C) 2009 Sebastian Heinlein <devel@glatzor.de> +# Copyright (C) 2009 Michael Vogt <michael.vogt@ubuntu.com> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__all__ = ("DebconfProxy",) + +import copy +import logging +import os +import os.path +import socket +import subprocess +import tempfile +import sys + +from gi.repository import GLib + +log = logging.getLogger("AptClient.DebconfProxy") + + +class DebconfProxy(object): + + """The DebconfProxy class allows to run the debconf frontend + as normal user by connecting to the root debconf through the socket of the + passthrough frontend. + """ + + def __init__(self, frontend="gnome", socket_path=None): + """Initialize a new DebconfProxy instance. + + Keyword arguments: + frontend -- the to be used debconf frontend (defaults to gnome) + socket_path -- the path to the socket of the passthrough frontend. + Will be created if not specified + """ + self.socket_path = socket_path + self.temp_dir = None + if socket_path is None: + self.temp_dir = tempfile.mkdtemp(prefix="aptdaemon-") + self.socket_path = os.path.join(self.temp_dir, "debconf.socket") + log.debug("debconf socket: %s" % self.socket_path) + self.socket = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) + self.socket.bind(self.socket_path) + self.frontend = frontend + self._listener_id = None + self._active_conn = None + self._watch_ids = [] + + def _get_debconf_env(self): + """Returns a dictonary of the environment variables required by + the debconf frontend. + """ + env = copy.copy(os.environ) + env["DEBCONF_DB_REPLACE"] = "configdb" + env["DEBCONF_DB_OVERRIDE"] = "Pipe{infd:none outfd:none}" + env["DEBIAN_FRONTEND"] = self.frontend + if log.level == logging.DEBUG: + env["DEBCONF_DEBUG"] = "." + return env + + def start(self): + """Start listening on the socket.""" + logging.debug("debconf.start()") + self.socket.listen(1) + self._listener_id = GLib.io_add_watch( + self.socket, GLib.PRIORITY_DEFAULT_IDLE, + GLib.IO_IN, self._accept_connection) + + def stop(self): + """Stop listening on the socket.""" + logging.debug("debconf.stop()") + self.socket.close() + if self._listener_id is not None: + GLib.source_remove(self._listener_id) + self._listener_id = None + if self.temp_dir: + try: + os.remove(self.socket_path) + os.rmdir(self.temp_dir) + except OSError: + pass + + def _accept_connection(self, socket, condition): + if self._active_conn: + log.debug("Delaying connection") + return True + conn, addr = socket.accept() + self._active_conn = conn + mask = GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP | GLib.IO_NVAL + self.helper = subprocess.Popen(["debconf-communicate"], + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + env=self._get_debconf_env()) + GLib.io_add_watch(conn, GLib.PRIORITY_HIGH_IDLE, + mask, self._copy_conn, self.helper.stdin) + GLib.io_add_watch(self.helper.stdout, GLib.PRIORITY_HIGH_IDLE, + mask, self._copy_stdout, conn) + return True + + def _copy_stdout(self, source, condition, conn): + """Callback to copy data from the stdout of debconf-communicate to + the passthrough frontend.""" + logging.debug("_copy_stdout") + try: + debconf_data = source.readline() + if debconf_data: + log.debug("From debconf: %s", debconf_data) + conn.send(debconf_data) + return True + except (socket.error, IOError) as error: + log.debug(error) + # error, stop listening + log.debug("Stop reading from stdout") + self.helper.stdout.close() + self._active_conn.close() + self._active_conn = None + return False + + def _copy_conn(self, source, condition, stdin): + """Callback to copy data from the passthrough frontend to stdin of + debconf-communicate.""" + logging.debug("_copy_conn") + try: + socket_data = source.recv(1024) + if socket_data: + log.debug("From socket: %s", socket_data) + stdin.write(socket_data) + stdin.flush() + return True + except (socket.error, IOError) as error: + log.debug(error) + # error, stop listening + log.debug("Stop reading from conn") + self.helper.stdin.close() + return False + + +def _test(): + """Run the DebconfProxy from the command line for testing purposes. + + You have to execute the following commands before in a separate terminal: + $ echo "fset debconf/frontend seen false" | debconf-communicate + $ export DEBCONF_PIPE=/tmp/debconf.socket + $ dpkg-reconfigure debconf -f passthrough + """ + logging.basicConfig(level=logging.DEBUG) + socket_path = "/tmp/debconf.socket" + if os.path.exists(socket_path): + os.remove(socket_path) + proxy = DebconfProxy("gnome", socket_path) + proxy.start() + loop = GLib.MainLoop() + loop.run() + +if __name__ == "__main__": + _test() + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/enums.py b/aptdaemon/enums.py new file mode 100644 index 0000000..3642199 --- /dev/null +++ b/aptdaemon/enums.py @@ -0,0 +1,718 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""enums - Enumerates for apt daemon dbus messages""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("PKGS_INSTALL", "PKGS_REINSTALL", "PKGS_REMOVE", "PKGS_PURGE", + "PKGS_UPGRADE", "PKGS_DOWNGRADE", "PKGS_KEEP", + "EXIT_SUCCESS", "EXIT_CANCELLED", "EXIT_FAILED", "EXIT_UNFINISHED", + "ERROR_PACKAGE_DOWNLOAD_FAILED", "ERROR_REPO_DOWNLOAD_FAILED", + "ERROR_DEP_RESOLUTION_FAILED", + "ERROR_KEY_NOT_INSTALLED", "ERROR_KEY_NOT_REMOVED", "ERROR_NO_LOCK", + "ERROR_NO_CACHE", "ERROR_NO_PACKAGE", "ERROR_PACKAGE_UPTODATE", + "ERROR_PACKAGE_NOT_INSTALLED", "ERROR_PACKAGE_ALREADY_INSTALLED", + "ERROR_NOT_REMOVE_ESSENTIAL_PACKAGE", "ERROR_DAEMON_DIED", + "ERROR_PACKAGE_MANAGER_FAILED", "ERROR_CACHE_BROKEN", + "ERROR_PACKAGE_UNAUTHENTICATED", "ERROR_INCOMPLETE_INSTALL", + "ERROR_UNREADABLE_PACKAGE_FILE", "ERROR_INVALID_PACKAGE_FILE", + "ERROR_SYSTEM_ALREADY_UPTODATE", "ERROR_NOT_SUPPORTED", + "ERROR_LICENSE_KEY_INSTALL_FAILED", + "ERROR_LICENSE_KEY_DOWNLOAD_FAILED", + "ERROR_AUTH_FAILED", "ERROR_NOT_AUTHORIZED", + "ERROR_UNKNOWN", + "STATUS_SETTING_UP", "STATUS_WAITING", "STATUS_WAITING_MEDIUM", + "STATUS_WAITING_CONFIG_FILE_PROMPT", "STATUS_WAITING_LOCK", + "STATUS_RUNNING", "STATUS_LOADING_CACHE", "STATUS_DOWNLOADING", + "STATUS_COMMITTING", "STATUS_CLEANING_UP", "STATUS_RESOLVING_DEP", + "STATUS_FINISHED", "STATUS_CANCELLING", "STATUS_QUERY", + "STATUS_DOWNLOADING_REPO", "STATUS_AUTHENTICATING", + "ROLE_UNSET", "ROLE_INSTALL_PACKAGES", "ROLE_INSTALL_FILE", + "ROLE_UPGRADE_PACKAGES", "ROLE_UPGRADE_SYSTEM", "ROLE_UPDATE_CACHE", + "ROLE_REMOVE_PACKAGES", "ROLE_COMMIT_PACKAGES", + "ROLE_ADD_VENDOR_KEY_FILE", "ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER", + "ROLE_REMOVE_VENDOR_KEY", "ROLE_FIX_INCOMPLETE_INSTALL", + "ROLE_FIX_BROKEN_DEPENDS", "ROLE_ADD_REPOSITORY", + "ROLE_ENABLE_DISTRO_COMP", "ROLE_CLEAN", "ROLE_RECONFIGURE", + "ROLE_PK_QUERY", "ROLE_ADD_LICENSE_KEY", + "DOWNLOAD_DONE", "DOWNLOAD_AUTH_ERROR", "DOWNLOAD_ERROR", + "DOWNLOAD_FETCHING", "DOWNLOAD_IDLE", "DOWNLOAD_NETWORK_ERROR", + "PKG_INSTALLING", "PKG_CONFIGURING", "PKG_REMOVING", + "PKG_PURGING", "PKG_UPGRADING", "PKG_RUNNING_TRIGGER", + "PKG_DISAPPEARING", "PKG_PREPARING_REMOVE", "PKG_PREPARING_INSTALL", + "PKG_PREPARING_PURGE", "PKG_PREPARING_PURGE", "PKG_INSTALLED", + "PKG_REMOVED", "PKG_PURGED", "PKG_UNPACKING", "PKG_UNKNOWN", + "get_status_icon_name_from_enum", "get_role_icon_name_from_enum", + "get_status_animation_name_from_enum", + "get_package_status_from_enum", + "get_role_localised_past_from_enum", "get_exit_string_from_enum", + "get_role_localised_present_from_enum", "get_role_error_from_enum", + "get_error_description_from_enum", "get_error_string_from_enum", + "get_status_string_from_enum", "get_download_status_from_enum") + +import gettext + + +def _(msg): + return gettext.dgettext("aptdaemon", msg) + +# PACKAGE GROUP INDEXES +#: Index of the list of to be installed packages in the :attr:`dependencies` +#: and :attr:`packages` property of :class:`~aptdaemon.client.AptTransaction`. +PKGS_INSTALL = 0 +#: Index of the list of to be re-installed packages in the :attr:`dependencies` +#: and :attr:`packages` property of :class:`~aptdaemon.client.AptTransaction`. +PKGS_REINSTALL = 1 +#: Index of the list of to be removed packages in the :attr:`dependencies` +#: and :attr:`packages` property of :class:`~aptdaemon.client.AptTransaction`. +PKGS_REMOVE = 2 +#: Index of the list of to be purged packages in the :attr:`dependencies` +#: and :attr:`packages` property of :class:`~aptdaemon.client.AptTransaction`. +PKGS_PURGE = 3 +#: Index of the list of to be upgraded packages in the :attr:`dependencies` +#: and :attr:`packages` property of :class:`~aptdaemon.client.AptTransaction`. +PKGS_UPGRADE = 4 +#: Index of the list of to be downgraded packages in the :attr:`dependencies` +#: and :attr:`packages` property of :class:`~aptdaemon.client.AptTransaction`. +PKGS_DOWNGRADE = 5 +#: Index of the list of to be keept packages in the :attr:`dependencies` +#: property of :class:`~aptdaemon.client.AptTransaction`. +PKGS_KEEP = 6 + +# FINISH STATES +#: The transaction was successful. +EXIT_SUCCESS = "exit-success" +#: The transaction has been cancelled by the user. +EXIT_CANCELLED = "exit-cancelled" +#: The transaction has failed. +EXIT_FAILED = "exit-failed" +#: The transaction failed since a previous one in a chain failed. +EXIT_PREVIOUS_FAILED = "exit-previous-failed" +#: The transaction is still being queued or processed. +EXIT_UNFINISHED = "exit-unfinished" + +# ERROR CODES +#: Failed to download package files which should be installed. +ERROR_PACKAGE_DOWNLOAD_FAILED = "error-package-download-failed" +#: Failed to download package information (index files) from the repositories +ERROR_REPO_DOWNLOAD_FAILED = "error-repo-download-failed" +#: Failed to satisfy the dependencies or conflicts of packages. +ERROR_DEP_RESOLUTION_FAILED = "error-dep-resolution-failed" +#: The requested vendor key is not installed. +ERROR_KEY_NOT_INSTALLED = "error-key-not-installed" +#: The requested vendor could not be removed. +ERROR_KEY_NOT_REMOVED = "error-key-not-removed" +#: The package management system could not be locked. Eventually another +#: package manager is running. +ERROR_NO_LOCK = "error-no-lock" +#: The package cache could not be opened. This indicates a serious problem +#: on the system. +ERROR_NO_CACHE = "error-no-cache" +#: The requested package is not available. +ERROR_NO_PACKAGE = "error-no-package" +#: The package could not be upgraded since it is already up-to-date. +ERROR_PACKAGE_UPTODATE = "error-package-uptodate" +#: The package which was requested to install is already installed. +ERROR_PACKAGE_ALREADY_INSTALLED = "error-package-already-installed" +#: The package could not be removed since it is not installed. +ERROR_PACKAGE_NOT_INSTALLED = "error-package-not-installed" +#: It is not allowed to remove an essential system package. +ERROR_NOT_REMOVE_ESSENTIAL_PACKAGE = "error-not-remove-essential" +#: The aptdaemon crashed or could not be connected to on the D-Bus. +ERROR_DAEMON_DIED = "error-daemon-died" +#: On of the maintainer scripts during the dpkg call failed. +ERROR_PACKAGE_MANAGER_FAILED = "error-package-manager-failed" +#: There are packages with broken dependencies installed on the system. +#: This has to fixed before performing another transaction. +ERROR_CACHE_BROKEN = "error-cache-broken" +#: It is not allowed to install an unauthenticated packages. Packages are +#: authenticated by installing the vendor key. +ERROR_PACKAGE_UNAUTHENTICATED = "error-package-unauthenticated" +#: A previous installation has been aborted and is now incomplete. +#: Should be fixed by `dpkg --configure -a` or the :func:`FixIncomplete()` +#: transaction. +ERROR_INCOMPLETE_INSTALL = "error-incomplete-install" +#: Failed to open and read the package file +ERROR_UNREADABLE_PACKAGE_FILE = "error-unreadable-package-file" +#: The package file violates the Debian/Ubuntu policy +ERROR_INVALID_PACKAGE_FILE = "error-invalid-package-file" +#: The requested feature is not supported yet (mainly used by PackageKit +ERROR_NOT_SUPPORTED = "error-not-supported" +#: The license key download failed +ERROR_LICENSE_KEY_DOWNLOAD_FAILED = "error-license-key-download-failed" +#: The license key is invalid +ERROR_LICENSE_KEY_INSTALL_FAILED = "error-license-key-install-failed" +#: The system is already up-to-date and don't needs any upgrades +ERROR_SYSTEM_ALREADY_UPTODATE = "error-system-already-uptodate" +#: The user isn't allowed to perform the action at all +ERROR_NOT_AUTHORIZED = "error-not-authorized" +#: The user could not be authorized (e.g. wrong password) +ERROR_AUTH_FAILED = "error-auth-failed" +#: An unknown error occured. In most cases these are programming ones. +ERROR_UNKNOWN = "error-unknown" + +# TRANSACTION STATES +#: The transaction was created, but hasn't been queued. +STATUS_SETTING_UP = "status-setting-up" +#: The transaction performs a query +STATUS_QUERY = "status-query" +#: The transaction is waiting in the queue. +STATUS_WAITING = "status-waiting" +#: The transaction is paused and waits until a required medium is inserted. +#: See :func:`ProvideMedium()`. +STATUS_WAITING_MEDIUM = "status-waiting-medium" +#: The transaction is paused and waits for the user to resolve a configuration +#: file conflict. See :func:`ResolveConfigFileConflict()`. +STATUS_WAITING_CONFIG_FILE_PROMPT = "status-waiting-config-file-prompt" +#: Wait until the package management system can be locked. Most likely +#: another package manager is running currently. +STATUS_WAITING_LOCK = "status-waiting-lock" +#: The processing of the transaction has started. +STATUS_RUNNING = "status-running" +#: The package cache is opened. +STATUS_LOADING_CACHE = "status-loading-cache" +#: The information about available packages is downloaded +STATUS_DOWNLOADING_REPO = "status-downloading-repo" +#: The required package files to install are getting downloaded. +STATUS_DOWNLOADING = "status-downloading" +#: The actual installation/removal takes place. +STATUS_COMMITTING = "status-committing" +#: The package management system is cleaned up. +STATUS_CLEANING_UP = "status-cleaning-up" +#: The dependecies and conflicts are now getting resolved. +STATUS_RESOLVING_DEP = "status-resolving-dep" +#: The transaction has been completed. +STATUS_FINISHED = "status-finished" +#: The transaction has been cancelled. +STATUS_CANCELLING = "status-cancelling" +#: The transaction waits for authentication +STATUS_AUTHENTICATING = "status-authenticating" + +# PACKAGE STATES +#: The package gets unpacked +PKG_UNPACKING = "pkg-unpacking" +#: The installation of the package gets prepared +PKG_PREPARING_INSTALL = "pkg-preparing-install" +#: The package is installed +PKG_INSTALLED = "pkg-installed" +#: The package gets installed +PKG_INSTALLING = "pkg-installing" +#: The configuration of the package gets prepared +PKG_PREPARING_CONFIGURE = "pkg-preparing-configure" +#: The package gets configured +PKG_CONFIGURING = "pkg-configuring" +#: The removal of the package gets prepared +PKG_PREPARING_REMOVE = "pkg-preparing-removal" +#: The package gets removed +PKG_REMOVING = "pkg-removing" +#: The package is removed +PKG_REMOVED = "pkg-removed" +#: The purging of the package gets prepared +PKG_PREPARING_PURGE = "pkg-preparing-purge" +#: The package gets purged +PKG_PURGING = "pkg-purging" +#: The package was completely removed +PKG_PURGED = "pkg-purged" +#: The post installation trigger of the package is processed +PKG_RUNNING_TRIGGER = "pkg-running-trigger" +#: The package disappered - very rare +PKG_DISAPPEARING = "pkg-disappearing" +#: The package gets upgraded +PKG_UPGRADING = "pkg-upgrading" +#: Failed to get a current status of the package +PKG_UNKNOWN = "pkg-unknown" + +# TRANSACTION ROLES +#: The role of the transaction has not been specified yet. +ROLE_UNSET = "role-unset" +#: The transaction performs a query compatible to the PackageKit interface +ROLE_PK_QUERY = "role-pk-query" +#: The transaction will install one or more packages. +ROLE_INSTALL_PACKAGES = "role-install-packages" +#: The transaction will install a local package file. +ROLE_INSTALL_FILE = "role-install-file" +#: The transaction will upgrade one or more packages. +ROLE_UPGRADE_PACKAGES = "role-upgrade-packages" +#: The transaction will perform a system upgrade. +ROLE_UPGRADE_SYSTEM = "role-upgrade-system" +#: The transaction will update the package cache. +ROLE_UPDATE_CACHE = "role-update-cache" +#: The transaction will remove one or more packages. +ROLE_REMOVE_PACKAGES = "role-remove-packages" +#: The transaction will perform a combined install, remove, upgrade or +#: downgrade action. +ROLE_COMMIT_PACKAGES = "role-commit-packages" +#: The transaction will add a local vendor key file to authenticate packages. +ROLE_ADD_VENDOR_KEY_FILE = "role-add-vendor-key-file" +#: The transaction will download vendor key to authenticate packages from +#: a keyserver. +ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER = "role-add-vendor-key-from-keyserver" +#: The transaction will remove a vendor key which was used to authenticate +#: packages. +ROLE_REMOVE_VENDOR_KEY = "role-remove-vendor-key" +#: The transaction will try to finish a previous aborted installation. +ROLE_FIX_INCOMPLETE_INSTALL = "role-fix-incomplete-install" +#: The transaction will to resolve broken dependencies of already installed +#: packages. +ROLE_FIX_BROKEN_DEPENDS = "role-fix-broken-depends" +#: The transaction will enable a repository to download software from. +ROLE_ADD_REPOSITORY = "role-add-repository" +#: The transaction will enable a component in the distro repositories, +#: e.g main or universe +ROLE_ENABLE_DISTRO_COMP = "role-enable-distro-comp" +#: The transaction will reconfigure the given already installed packages +ROLE_RECONFIGURE = "role-reconfigure" +#: The transaction will remove all downloaded package files. +ROLE_CLEAN = "role-clean" +#: The transaction will add a license key to the system +ROLE_ADD_LICENSE_KEY = "role-add-license-key" + +# DOWNLOAD STATES +#: The download has been completed. +DOWNLOAD_DONE = "download-done" +#: The file could not be downloaded since the authentication for the repository +#: failed. +DOWNLOAD_AUTH_ERROR = "download-auth-error" +#: There file could not be downloaded, e.g. because it is not available (404). +DOWNLOAD_ERROR = "download-error" +#: The file is currently being downloaded. +DOWNLOAD_FETCHING = "download-fetching" +#: The download is currently idling. +DOWNLOAD_IDLE = "download-idle" +#: The download failed since there seem to be a networking problem. +DOWNLOAD_NETWORK_ERROR = "download-network-error" + +_ICONS_STATUS = { + STATUS_CANCELLING: 'aptdaemon-cleanup', + STATUS_CLEANING_UP: 'aptdaemon-cleanup', + STATUS_RESOLVING_DEP: 'aptdaemon-resolve', + STATUS_COMMITTING: 'aptdaemon-working', + STATUS_DOWNLOADING: 'aptdaemon-download', + STATUS_DOWNLOADING_REPO: 'aptdaemon-download', + STATUS_FINISHED: 'aptdaemon-cleanup', + STATUS_LOADING_CACHE: 'aptdaemon-update-cache', + STATUS_RUNNING: 'aptdaemon-working', + STATUS_SETTING_UP: 'aptdaemon-working', + STATUS_WAITING: 'aptdaemon-wait', + STATUS_WAITING_LOCK: 'aptdaemon-wait', + STATUS_WAITING_MEDIUM: 'aptdaemon-wait', + STATUS_WAITING_CONFIG_FILE_PROMPT: 'aptdaemon-wait'} + +_ICONS_ROLE = { + ROLE_INSTALL_FILE: 'aptdaemon-add', + ROLE_INSTALL_PACKAGES: 'aptdaemon-add', + ROLE_UPDATE_CACHE: 'aptdaemon-update-cache', + ROLE_REMOVE_PACKAGES: 'aptdaemon-delete', + ROLE_UPGRADE_PACKAGES: 'aptdaemon-upgrade', + ROLE_UPGRADE_SYSTEM: 'system-software-update'} + +_ANIMATIONS_STATUS = { + STATUS_CANCELLING: 'aptdaemon-action-cleaning-up', + STATUS_CLEANING_UP: 'aptdaemon-action-cleaning-up', + STATUS_RESOLVING_DEP: 'aptdaemon-action-resolving', + STATUS_DOWNLOADING: 'aptdaemon-action-downloading', + STATUS_DOWNLOADING_REPO: 'aptdaemon-action-downloading', + STATUS_LOADING_CACHE: 'aptdaemon-action-updating-cache', + STATUS_WAITING: 'aptdaemon-action-waiting', + STATUS_WAITING_LOCK: 'aptdaemon-action-waiting', + STATUS_WAITING_MEDIUM: 'aptdaemon-action-waiting', + STATUS_WAITING_CONFIG_FILE_PROMPT: 'aptdaemon-action-waiting'} + +_PAST_ROLE = { + ROLE_INSTALL_FILE: _("Installed file"), + ROLE_INSTALL_PACKAGES: _("Installed packages"), + ROLE_ADD_VENDOR_KEY_FILE: _("Added key from file"), + ROLE_UPDATE_CACHE: _("Updated cache"), + ROLE_PK_QUERY: _("Search done"), + ROLE_REMOVE_VENDOR_KEY: _("Removed trusted key"), + ROLE_REMOVE_PACKAGES: _("Removed packages"), + ROLE_UPGRADE_PACKAGES: _("Updated packages"), + ROLE_UPGRADE_SYSTEM: _("Upgraded system"), + ROLE_COMMIT_PACKAGES: _("Applied changes"), + ROLE_FIX_INCOMPLETE_INSTALL: _("Repaired incomplete installation"), + ROLE_FIX_BROKEN_DEPENDS: _("Repaired broken dependencies"), + ROLE_ADD_REPOSITORY: _("Added software source"), + ROLE_ENABLE_DISTRO_COMP: _("Enabled component of the distribution"), + ROLE_CLEAN: _("Removed downloaded package files"), + ROLE_RECONFIGURE: _("Reconfigured installed packages"), + ROLE_UNSET: ""} + +_STRING_EXIT = { + EXIT_SUCCESS: _("Successful"), + EXIT_CANCELLED: _("Canceled"), + EXIT_FAILED: _("Failed")} + +_PRESENT_ROLE = { + ROLE_INSTALL_FILE: _("Installing file"), + ROLE_INSTALL_PACKAGES: _("Installing packages"), + ROLE_ADD_VENDOR_KEY_FILE: _("Adding key from file"), + ROLE_UPDATE_CACHE: _("Updating cache"), + ROLE_REMOVE_VENDOR_KEY: _("Removing trusted key"), + ROLE_REMOVE_PACKAGES: _("Removing packages"), + ROLE_UPGRADE_PACKAGES: _("Updating packages"), + ROLE_UPGRADE_SYSTEM: _("Upgrading system"), + ROLE_COMMIT_PACKAGES: _("Applying changes"), + ROLE_FIX_INCOMPLETE_INSTALL: _("Repairing incomplete installation"), + ROLE_FIX_BROKEN_DEPENDS: _("Repairing installed software"), + ROLE_ADD_REPOSITORY: _("Adding software source"), + ROLE_ENABLE_DISTRO_COMP: _("Enabling component of the distribution"), + ROLE_CLEAN: _("Removing downloaded package files"), + ROLE_RECONFIGURE: _("Reconfiguring installed packages"), + ROLE_PK_QUERY: _("Searching"), + ROLE_UNSET: ""} + +_ERROR_ROLE = { + ROLE_INSTALL_FILE: _("Installation of the package file failed"), + ROLE_INSTALL_PACKAGES: _("Installation of software failed"), + ROLE_ADD_VENDOR_KEY_FILE: _("Adding the key to the list of trusted " + "software vendors failed"), + ROLE_UPDATE_CACHE: _("Refreshing the software list failed"), + ROLE_REMOVE_VENDOR_KEY: _("Removing the vendor from the list of trusted " + "ones failed"), + ROLE_REMOVE_PACKAGES: _("Removing software failed"), + ROLE_UPGRADE_PACKAGES: _("Updating software failed"), + ROLE_UPGRADE_SYSTEM: _("Upgrading the system failed"), + ROLE_COMMIT_PACKAGES: _("Applying software changes failed"), + ROLE_FIX_INCOMPLETE_INSTALL: _("Repairing incomplete installation " + "failed"), + ROLE_FIX_BROKEN_DEPENDS: _("Repairing broken dependencies failed"), + ROLE_ADD_REPOSITORY: _("Adding software source failed"), + ROLE_ENABLE_DISTRO_COMP: _("Enabling component of the distribution " + "failed"), + ROLE_CLEAN: _("Removing downloaded package files failed"), + ROLE_RECONFIGURE: _("Removing downloaded package files failed"), + ROLE_PK_QUERY: _("Search failed"), + ROLE_ADD_LICENSE_KEY: _("Adding license key"), + ROLE_UNSET: ""} + +_DESCS_ERROR = { + ERROR_PACKAGE_DOWNLOAD_FAILED: _("Check your Internet connection."), + ERROR_REPO_DOWNLOAD_FAILED: _("Check your Internet connection."), + ERROR_CACHE_BROKEN: _("Check if you are using third party " + "repositories. If so disable them, since " + "they are a common source of problems.\n" + "Furthermore run the following command in a " + "Terminal: apt-get install -f"), + ERROR_KEY_NOT_INSTALLED: _("The selected file may not be a GPG key file " + "or it might be corrupt."), + ERROR_KEY_NOT_REMOVED: _("The selected key couldn't be removed. " + "Check that you provided a valid fingerprint."), + ERROR_NO_LOCK: _("Check if you are currently running another " + "software management tool, e.g. Synaptic or aptitude. " + "Only one tool is allowed to make changes at a time."), + ERROR_NO_CACHE: _("This is a serious problem. Try again later. If this " + "problem appears again, please report an error to the " + "developers."), + ERROR_NO_PACKAGE: _("Check the spelling of the package name, and " + "that the appropriate repository is enabled."), + ERROR_PACKAGE_UPTODATE: _("There isn't any need for an update."), + ERROR_PACKAGE_ALREADY_INSTALLED: _("There isn't any need for an " + "installation"), + ERROR_PACKAGE_NOT_INSTALLED: _("There isn't any need for a removal."), + ERROR_NOT_REMOVE_ESSENTIAL_PACKAGE: _("You requested to remove a " + "package which is an essential " + "part of your system."), + ERROR_DAEMON_DIED: _("The connection to the daemon was lost. Most likely " + "the background daemon crashed."), + ERROR_PACKAGE_MANAGER_FAILED: _("The installation or removal of a " + "software package failed."), + ERROR_NOT_SUPPORTED: _("The requested feature is not supported."), + ERROR_UNKNOWN: _("There seems to be a programming error in aptdaemon, " + "the software that allows you to install/remove " + "software and to perform other package management " + "related tasks."), + ERROR_DEP_RESOLUTION_FAILED: _("This error could be caused by required " + "additional software packages which are " + "missing or not installable. Furthermore " + "there could be a conflict between " + "software packages which are not allowed " + "to be installed at the same time."), + ERROR_PACKAGE_UNAUTHENTICATED: _("This requires installing packages " + "from unauthenticated sources."), + ERROR_INCOMPLETE_INSTALL: _("The installation could have failed because " + "of an error in the corresponding software " + "package or it was cancelled in an unfriendly " + "way. " + "You have to repair this before you can " + "install or remove any further software."), + ERROR_UNREADABLE_PACKAGE_FILE: _("Please copy the file to your local " + "computer and check the file " + "permissions."), + ERROR_INVALID_PACKAGE_FILE: _("The installation of a package which " + "violates the quality standards isn't " + "allowed. This could cause serious " + "problems on your computer. Please contact " + "the person or organisation who provided " + "this package file and include the details " + "beneath."), + ERROR_LICENSE_KEY_INSTALL_FAILED: _("The downloaded license key which is " + "required to run this piece of " + "software is not valid or could not " + "be installed correctly.\n" + "See the details for more " + "information."), + ERROR_SYSTEM_ALREADY_UPTODATE: _("All available upgrades have already " + "been installed."), + ERROR_LICENSE_KEY_DOWNLOAD_FAILED: _("The license key which allows you to " + "use this piece of software could " + "not be downloaded. Please check " + "your network connection."), + ERROR_NOT_AUTHORIZED: _("You don't have the required privileges to " + "perform this action."), + ERROR_AUTH_FAILED: _("You either provided a wrong password or " + "cancelled the authorization.\n" + "Furthermore there could also be a technical reason " + "for this error if you haven't seen a password " + "dialog: your desktop environment doesn't provide a " + "PolicyKit session agent.")} + +_STRINGS_ERROR = { + ERROR_PACKAGE_DOWNLOAD_FAILED: _("Failed to download package files"), + ERROR_REPO_DOWNLOAD_FAILED: _("Failed to download repository " + "information"), + ERROR_DEP_RESOLUTION_FAILED: _("Package dependencies cannot be resolved"), + ERROR_CACHE_BROKEN: _("The package system is broken"), + ERROR_KEY_NOT_INSTALLED: _("Key was not installed"), + ERROR_KEY_NOT_REMOVED: _("Key was not removed"), + ERROR_NO_LOCK: _("Failed to lock the package manager"), + ERROR_NO_CACHE: _("Failed to load the package list"), + ERROR_NO_PACKAGE: _("Package does not exist"), + ERROR_PACKAGE_UPTODATE: _("Package is already up to date"), + ERROR_PACKAGE_ALREADY_INSTALLED: _("Package is already installed"), + ERROR_PACKAGE_NOT_INSTALLED: _("Package isn't installed"), + ERROR_NOT_REMOVE_ESSENTIAL_PACKAGE: _("Failed to remove essential " + "system package"), + ERROR_DAEMON_DIED: _("Task cannot be monitored or controlled"), + ERROR_PACKAGE_MANAGER_FAILED: _("Package operation failed"), + ERROR_PACKAGE_UNAUTHENTICATED: _("Requires installation of untrusted " + "packages"), + ERROR_INCOMPLETE_INSTALL: _("Previous installation hasn't been completed"), + ERROR_INVALID_PACKAGE_FILE: _("The package is of bad quality"), + ERROR_UNREADABLE_PACKAGE_FILE: _("Package file could not be opened"), + ERROR_NOT_SUPPORTED: _("Not supported feature"), + ERROR_LICENSE_KEY_DOWNLOAD_FAILED: _("Failed to download the license key"), + ERROR_LICENSE_KEY_INSTALL_FAILED: _("Failed to install the license key"), + ERROR_SYSTEM_ALREADY_UPTODATE: _("The system is already up to date"), + ERROR_AUTH_FAILED: _("You could not be authorized"), + ERROR_NOT_AUTHORIZED: _("You are not allowed to perform this action"), + ERROR_UNKNOWN: _("An unhandlable error occured")} + +_STRINGS_STATUS = { + STATUS_SETTING_UP: _("Waiting for service to start"), + STATUS_QUERY: _("Searching"), + STATUS_WAITING: _("Waiting"), + STATUS_WAITING_MEDIUM: _("Waiting for required medium"), + STATUS_WAITING_LOCK: _("Waiting for other software managers to quit"), + STATUS_WAITING_CONFIG_FILE_PROMPT: _("Waiting for configuration file " + "prompt"), + STATUS_RUNNING: _("Running task"), + STATUS_DOWNLOADING: _("Downloading"), + STATUS_DOWNLOADING_REPO: _("Querying software sources"), + STATUS_CLEANING_UP: _("Cleaning up"), + STATUS_RESOLVING_DEP: _("Resolving dependencies"), + STATUS_COMMITTING: _("Applying changes"), + STATUS_FINISHED: _("Finished"), + STATUS_CANCELLING: _("Cancelling"), + STATUS_LOADING_CACHE: _("Loading software list"), + STATUS_AUTHENTICATING: _("Waiting for authentication")} + +STRINGS_PKG_STATUS = { + # TRANSLATORS: %s is the name of a package + PKG_INSTALLING: _("Installing %s"), + # TRANSLATORS: %s is the name of a package + PKG_CONFIGURING: _("Configuring %s"), + # TRANSLATORS: %s is the name of a package + PKG_REMOVING: _("Removing %s"), + # TRANSLATORS: %s is the name of a package + PKG_PURGING: _("Completely removing %s"), + # TRANSLATORS: %s is the name of a package + PKG_PURGING: _("Noting disappearance of %s"), + # TRANSLATORS: %s is the name of a package + PKG_RUNNING_TRIGGER: _("Running post-installation trigger %s"), + # TRANSLATORS: %s is the name of a package + PKG_UPGRADING: _("Upgrading %s"), + # TRANSLATORS: %s is the name of a package + PKG_UNPACKING: _("Unpacking %s"), + # TRANSLATORS: %s is the name of a package + PKG_PREPARING_INSTALL: _("Preparing installation of %s"), + # TRANSLATORS: %s is the name of a package + PKG_PREPARING_CONFIGURE: _("Preparing configuration of %s"), + # TRANSLATORS: %s is the name of a package + PKG_PREPARING_REMOVE: _("Preparing removal of %s"), + # TRANSLATORS: %s is the name of a package + PKG_PREPARING_PURGE: _("Preparing complete removal of %s"), + # TRANSLATORS: %s is the name of a package + PKG_INSTALLED: _("Installed %s"), + # TRANSLATORS: %s is the name of a package + PKG_PURGED: _("Completely removed %s"), + # TRANSLATORS: %s is the name of a package + PKG_REMOVED: _("Removed %s")} + +STRINGS_DOWNLOAD = { + DOWNLOAD_DONE: _("Done"), + DOWNLOAD_AUTH_ERROR: _("Authentication failed"), + DOWNLOAD_ERROR: _("Failed"), + DOWNLOAD_FETCHING: _("Fetching"), + DOWNLOAD_IDLE: _("Idle"), + DOWNLOAD_NETWORK_ERROR: _("Network isn't available")} + + +def get_status_icon_name_from_enum(enum): + """Get the icon name for a transaction status. + + :param enum: The transaction status enum, e.g. :data:`STATUS_WAITING`. + :returns: The icon name string. + """ + try: + return _ICONS_STATUS[enum] + except KeyError: + return "aptdaemon-working" + + +def get_role_icon_name_from_enum(enum): + """Get an icon to represent the role of a transaction. + + :param enum: The transaction role enum, e.g. :data:`ROLE_UPDATE_CACHE`. + :returns: The icon name string. + """ + try: + return _ICONS_ROLE[enum] + except KeyError: + return "aptdaemon-working" + + +def get_status_animation_name_from_enum(enum): + """Get an animation to represent a transaction status. + + :param enum: The transaction status enum, e.g. :data:`STATUS_WAITING`. + :returns: The animation name string. + """ + try: + return _ANIMATIONS_STATUS[enum] + except KeyError: + return None + + +def get_role_localised_past_from_enum(enum): + """Get the description of a completed transaction action. + + :param enum: The transaction role enum, e.g. :data:`ROLE_UPDATE_CACHE`. + :returns: The description string. + """ + try: + return _PAST_ROLE[enum] + except KeyError: + return None + + +def get_exit_string_from_enum(enum): + """Get the description of a transaction exit status. + + :param enum: The transaction exit status enum, e.g. :data:`EXIT_FAILED`. + :returns: The description string. + """ + try: + return _STRING_EXIT[enum] + except: + return None + + +def get_role_localised_present_from_enum(enum): + """Get the description of a present transaction action. + + :param enum: The transaction role enum, e.g. :data:`ROLE_UPDATE_CACHE`. + :returns: The description string. + """ + try: + return _PRESENT_ROLE[enum] + except KeyError: + return None + + +def get_role_error_from_enum(enum): + """Get the description of a failed transaction action. + + :param enum: The transaction role enum, e.g. :data:`ROLE_UPDATE_CACHE`. + :returns: The description string. + """ + try: + return _ERROR_ROLE[enum] + except KeyError: + return None + + +def get_error_description_from_enum(enum): + """Get a long description of an error. + + :param enum: The transaction error enum, e.g. :data:`ERROR_NO_LOCK`. + :returns: The description string. + """ + try: + return _DESCS_ERROR[enum] + except KeyError: + return None + + +def get_error_string_from_enum(enum): + """Get a short description of an error. + + :param enum: The transaction error enum, e.g. :data:`ERROR_NO_LOCK`. + :returns: The description string. + """ + try: + return _STRINGS_ERROR[enum] + except KeyError: + return None + + +def get_status_string_from_enum(enum): + """Get the description of a transaction status. + + :param enum: The transaction status enum, e.g. :data:`STATUS_WAITING`. + :returns: The description string. + """ + try: + return _STRINGS_STATUS[enum] + except KeyError: + return None + + +def get_package_status_from_enum(enum): + """Get the description of a package status. + + :param enum: The download status enum, e.g. :data:`PKG_INSTALLING`. + :returns: The description string. + """ + try: + return STRINGS_PKG_STATUS[enum] + except KeyError: + return _("Processing %s") + + +def get_download_status_from_enum(enum): + """Get the description of a download status. + + :param enum: The download status enum, e.g. :data:`DOWNLOAD_DONE`. + :returns: The description string. + """ + try: + return STRINGS_DOWNLOAD[enum] + except KeyError: + return None + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/errors.py b/aptdaemon/errors.py new file mode 100644 index 0000000..cd98d6e --- /dev/null +++ b/aptdaemon/errors.py @@ -0,0 +1,235 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Exception classes""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("AptDaemonError", "ForeignTransaction", "InvalidMetaDataError", + "InvalidProxyError", "RepositoryInvalidError", + "TransactionAlreadyRunning", "TransactionCancelled", + "TransactionAlreadySimulating", + "TransactionFailed", "TransactionRoleAlreadySet", + "NotAuthorizedError", "convert_dbus_exception", + "get_native_exception") + +import inspect +from functools import wraps +import sys + +import dbus + +import aptdaemon.enums + +PY3K = sys.version_info.major > 2 + + +class AptDaemonError(dbus.DBusException): + + """Internal error of the aptdaemon""" + + _dbus_error_name = "org.debian.apt" + + def __init__(self, message=""): + message = _convert_unicode(message) + dbus.DBusException.__init__(self, message) + self._message = message + + def get_dbus_message(self): + """Overwrite the DBusException method, since it calls + Exception.__str__() internally which doesn't support unicode or + or non-ascii encodings.""" + if PY3K: + return dbus.DBusException.get_dbus_message(self) + else: + return self._message.encode("UTF-8") + + +class TransactionRoleAlreadySet(AptDaemonError): + + """Error if a transaction has already been configured.""" + + _dbus_error_name = "org.debian.apt.TransactionRoleAlreadySet" + + +class TransactionAlreadyRunning(AptDaemonError): + + """Error if a transaction has already been configured.""" + + _dbus_error_name = "org.debian.apt.TransactionAlreadyRunning" + + +class TransactionAlreadySimulating(AptDaemonError): + + """Error if a transaction should be simulated but a simulation is + already processed. + """ + + _dbus_error_name = "org.debian.apt.TransactionAlreadySimulating" + + +class ForeignTransaction(AptDaemonError): + + """Error if a transaction was initialized by a different user.""" + + _dbus_error_name = "org.debian.apt.TransactionAlreadyRunning" + + +class TransactionFailed(AptDaemonError): + + """Internal error if a transaction could not be processed successfully.""" + + _dbus_error_name = "org.debian.apt.TransactionFailed" + + def __init__(self, code, details="", *args): + if not args: + # Avoid string replacements if not used + details = details.replace("%", "%%") + args = tuple([_convert_unicode(arg) for arg in args]) + details = _convert_unicode(details) + self.code = code + self.details = details + self.details_args = args + AptDaemonError.__init__(self, "%s: %s" % (code, details % args)) + + def __unicode__(self): + return "Transaction failed: %s\n%s" % \ + (aptdaemon.enums.get_error_string_from_enum(self.code), + self.details) + + def __str__(self): + if PY3K: + return self.__unicode__() + else: + return self.__unicode__().encode("utf-8") + + +class InvalidMetaDataError(AptDaemonError): + + """Invalid meta data given""" + + _dbus_error_name = "org.debian.apt.InvalidMetaData" + + +class InvalidProxyError(AptDaemonError): + + """Invalid proxy given""" + + _dbus_error_name = "org.debian.apt.InvalidProxy" + + def __init__(self, proxy): + AptDaemonError.__init__(self, "InvalidProxyError: %s" % proxy) + + +class TransactionCancelled(AptDaemonError): + + """Internal error if a transaction was cancelled.""" + + _dbus_error_name = "org.debian.apt.TransactionCancelled" + + +class RepositoryInvalidError(AptDaemonError): + + """The added repository is invalid""" + + _dbus_error_name = "org.debian.apt.RepositoryInvalid" + + +class PolicyKitError(dbus.DBusException): + pass + + +class NotAuthorizedError(PolicyKitError): + + _dbus_error_name = "org.freedesktop.PolicyKit.Error.NotAuthorized" + + def __init__(self, subject, action_id): + dbus.DBusException.__init__(self, "%s: %s" % (subject, action_id)) + self.action_id = action_id + self.subject = subject + + +class AuthorizationFailed(NotAuthorizedError): + + _dbus_error_name = "org.freedesktop.PolicyKit.Error.Failed" + + +def convert_dbus_exception(func): + """A decorator which maps a raised DBbus exception to a native one. + + This decorator requires introspection to the decorated function. So it + cannot be used on any already decorated method. + """ + argnames, varargs, kwargs, defaults = inspect.getfullargspec(func)[:4] + + @wraps(func) + def _convert_dbus_exception(*args, **kwargs): + try: + error_handler = kwargs["error_handler"] + except KeyError: + _args = list(args) + try: + index = argnames.index("error_handler") + error_handler = _args[index] + except (IndexError, ValueError): + pass + else: + _args[index] = lambda err: error_handler( + get_native_exception(err)) + args = tuple(_args) + else: + kwargs["error_handler"] = lambda err: error_handler( + get_native_exception(err)) + try: + return func(*args, **kwargs) + except dbus.exceptions.DBusException as error: + raise get_native_exception(error) + return _convert_dbus_exception + + +def get_native_exception(error): + """Map a DBus exception to a native one. This allows to make use of + try/except on the client side without having to check for the error name. + """ + if not isinstance(error, dbus.DBusException): + return error + dbus_name = error.get_dbus_name() + dbus_msg = error.get_dbus_message() + if dbus_name == TransactionFailed._dbus_error_name: + return TransactionFailed(*dbus_msg.split(":", 1)) + elif dbus_name == AuthorizationFailed._dbus_error_name: + return AuthorizationFailed(*dbus_msg.split(":", 1)) + elif dbus_name == NotAuthorizedError._dbus_error_name: + return NotAuthorizedError(*dbus_msg.split(":", 1)) + for error_cls in [AptDaemonError, TransactionRoleAlreadySet, + TransactionAlreadyRunning, ForeignTransaction, + InvalidMetaDataError, InvalidProxyError, + TransactionCancelled, RepositoryInvalidError]: + if dbus_name == error_cls._dbus_error_name: + return error_cls(dbus_msg) + return error + + +def _convert_unicode(text, encoding="UTF-8"): + """Always return an unicode.""" + if PY3K and not isinstance(text, str): + text = str(text, encoding, errors="ignore") + elif not PY3K and not isinstance(text, unicode): + text = unicode(text, encoding, errors="ignore") + return text + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/gtk3widgets.py b/aptdaemon/gtk3widgets.py new file mode 100644 index 0000000..cf273a2 --- /dev/null +++ b/aptdaemon/gtk3widgets.py @@ -0,0 +1,1206 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +This module provides widgets to use aptdaemon in a GTK application. +""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# Licensed under the GNU General Public License Version 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("AptConfigFileConflictDialog", "AptCancelButton", + "AptConfirmDialog", + "AptProgressDialog", "AptTerminalExpander", "AptStatusIcon", + "AptRoleIcon", "AptStatusAnimation", "AptRoleLabel", + "AptStatusLabel", "AptMediumRequiredDialog", "AptMessageDialog", + "AptErrorDialog", "AptProgressBar", "DiffView", + "AptTerminal" + ) + +import difflib +import gettext +import os +import pty +import re + +import gi +gi.require_version("Vte", "2.91") +gi.require_version("Gdk", "3.0") +gi.require_version("Gtk", "3.0") + +import apt_pkg +from gi.repository import GObject +from gi.repository import GLib +from gi.repository import Gdk +from gi.repository import Gtk +from gi.repository import Pango +from gi.repository import Vte + +from . import client +from .enums import * +from defer import inline_callbacks +from defer.utils import deferable + +_ = lambda msg: gettext.dgettext("aptdaemon", msg) + +(COLUMN_ID, + COLUMN_PACKAGE) = list(range(2)) + + +class AptStatusIcon(Gtk.Image): + """ + Provides a Gtk.Image which shows an icon representing the status of a + aptdaemon transaction + """ + def __init__(self, transaction=None, size=Gtk.IconSize.DIALOG): + Gtk.Image.__init__(self) + # note: icon_size is a property which you can't set with GTK 2, so use + # a different name + self._icon_size = size + self.icon_name = None + self._signals = [] + self.set_alignment(0, 0) + if transaction is not None: + self.set_transaction(transaction) + + def set_transaction(self, transaction): + """Connect to the given transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals = [] + self._signals.append(transaction.connect("status-changed", + self._on_status_changed)) + + def set_icon_size(self, size): + """Set the icon size to gtk stock icon size value""" + self._icon_size = size + + def _on_status_changed(self, transaction, status): + """Set the status icon according to the changed status""" + icon_name = get_status_icon_name_from_enum(status) + if icon_name is None: + icon_name = Gtk.STOCK_MISSING_IMAGE + if icon_name != self.icon_name: + self.set_from_icon_name(icon_name, self._icon_size) + self.icon_name = icon_name + + +class AptRoleIcon(AptStatusIcon): + """ + Provides a Gtk.Image which shows an icon representing the role of an + aptdaemon transaction + """ + def set_transaction(self, transaction): + for sig in self._signals: + GLib.source_remove(sig) + self._signals = [] + self._signals.append(transaction.connect("role-changed", + self._on_role_changed)) + self._on_role_changed(transaction, transaction.role) + + def _on_role_changed(self, transaction, role_enum): + """Show an icon representing the role""" + icon_name = get_role_icon_name_from_enum(role_enum) + if icon_name is None: + icon_name = Gtk.STOCK_MISSING_IMAGE + if icon_name != self.icon_name: + self.set_from_icon_name(icon_name, self._icon_size) + self.icon_name = icon_name + + +class AptStatusAnimation(AptStatusIcon): + """ + Provides a Gtk.Image which shows an animation representing the + transaction status + """ + def __init__(self, transaction=None, size=Gtk.IconSize.DIALOG): + AptStatusIcon.__init__(self, transaction, size) + self.animation = [] + self.ticker = 0 + self.frame_counter = 0 + self.iter = 0 + name = get_status_animation_name_from_enum(STATUS_WAITING) + fallback = get_status_icon_name_from_enum(STATUS_WAITING) + self.set_animation(name, fallback) + + def set_animation(self, name, fallback=None, size=None): + """Show and start the animation of the given name and size""" + if name == self.icon_name: + return + if size is not None: + self._icon_size = size + self.stop_animation() + animation = [] + (width, height) = Gtk.icon_size_lookup(self._icon_size) + theme = Gtk.IconTheme.get_default() + if name is not None and theme.has_icon(name): + pixbuf = theme.load_icon(name, width, 0) + rows = pixbuf.get_height() / height + cols = pixbuf.get_width() / width + for r in range(rows): + for c in range(cols): + animation.append(pixbuf.subpixbuf(c * width, r * height, + width, height)) + if len(animation) > 0: + self.animation = animation + self.iter = 0 + self.set_from_pixbuf(self.animation[0]) + self.start_animation() + else: + self.set_from_pixbuf(pixbuf) + self.icon_name = name + elif fallback is not None and theme.has_icon(fallback): + self.set_from_icon_name(fallback, self._icon_size) + self.icon_name = fallback + else: + self.set_from_icon_name(Gtk.STOCK_MISSING_IMAGE) + + def start_animation(self): + """Start the animation""" + if self.ticker == 0: + self.ticker = GLib.timeout_add(200, self._advance) + + def stop_animation(self): + """Stop the animation""" + if self.ticker != 0: + GLib.source_remove(self.ticker) + self.ticker = 0 + + def _advance(self): + """ + Show the next frame of the animation and stop the animation if the + widget is no longer visible + """ + if self.get_property("visible") is False: + self.ticker = 0 + return False + self.iter = self.iter + 1 + if self.iter >= len(self.animation): + self.iter = 0 + self.set_from_pixbuf(self.animation[self.iter]) + return True + + def _on_status_changed(self, transaction, status): + """ + Set the animation according to the changed status + """ + name = get_status_animation_name_from_enum(status) + fallback = get_status_icon_name_from_enum(status) + self.set_animation(name, fallback) + + +class AptRoleLabel(Gtk.Label): + """ + Status label for the running aptdaemon transaction + """ + def __init__(self, transaction=None): + GtkLabel.__init__(self) + self.set_alignment(0, 0) + self.set_ellipsize(Pango.EllipsizeMode.END) + self.set_max_width_chars(15) + self._signals = [] + if transaction is not None: + self.set_transaction(transaction) + + def set_transaction(self, transaction): + """Connect the status label to the given aptdaemon transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals = [] + self._on_role_changed(transaction, transaction.role) + self._signals.append(transaction.connect("role-changed", + self._on_role_changed)) + + def _on_role_changed(self, transaction, role): + """Set the role text.""" + self.set_markup(get_role_localised_present_from_enum(role)) + + +class AptStatusLabel(Gtk.Label): + """ + Status label for the running aptdaemon transaction + """ + def __init__(self, transaction=None): + Gtk.Label.__init__(self) + self.set_alignment(0, 0) + self.set_ellipsize(Pango.EllipsizeMode.END) + self.set_max_width_chars(15) + self._signals = [] + if transaction is not None: + self.set_transaction(transaction) + + def set_transaction(self, transaction): + """Connect the status label to the given aptdaemon transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals = [] + self._signals.append( + transaction.connect("status-changed", self._on_status_changed)) + self._signals.append( + transaction.connect("status-details-changed", + self._on_status_details_changed)) + + def _on_status_changed(self, transaction, status): + """Set the status text according to the changed status""" + self.set_markup(get_status_string_from_enum(status)) + + def _on_status_details_changed(self, transaction, text): + """Set the status text to the one reported by apt""" + self.set_markup(text) + + +class AptProgressBar(Gtk.ProgressBar): + """ + Provides a Gtk.Progress which represents the progress of an aptdaemon + transactions + """ + def __init__(self, transaction=None): + Gtk.ProgressBar.__init__(self) + self.set_ellipsize(Pango.EllipsizeMode.END) + self.set_text(" ") + self.set_pulse_step(0.05) + self._signals = [] + if transaction is not None: + self.set_transaction(transaction) + + def set_transaction(self, transaction): + """Connect the progress bar to the given aptdaemon transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals = [] + self._signals.append( + transaction.connect("finished", self._on_finished)) + self._signals.append( + transaction.connect("progress-changed", self._on_progress_changed)) + self._signals.append(transaction.connect("progress-details-changed", + self._on_progress_details)) + + def _on_progress_changed(self, transaction, progress): + """ + Update the progress according to the latest progress information + """ + if progress > 100: + self.pulse() + else: + self.set_fraction(progress / 100.0) + + def _on_progress_details(self, transaction, items_done, items_total, + bytes_done, bytes_total, speed, eta): + """ + Update the progress bar text according to the latest progress details + """ + if items_total == 0 and bytes_total == 0: + self.set_text(" ") + return + if speed != 0: + self.set_text(_("Downloaded %sB of %sB at %sB/s") % + (client.get_size_string(bytes_done), + client.get_size_string(bytes_total), + client.get_size_string(speed))) + else: + self.set_text(_("Downloaded %sB of %sB") % + (client.get_size_string(bytes_done), + client.get_size_string(bytes_total))) + + def _on_finished(self, transaction, exit): + """Set the progress to 100% when the transaction is complete""" + self.set_fraction(1) + + +class AptDetailsExpander(Gtk.Expander): + + def __init__(self, transaction=None, terminal=True): + Gtk.Expander.__init__(self, label=_("Details")) + self.show_terminal = terminal + self._signals = [] + self.set_sensitive(False) + self.set_expanded(False) + if self.show_terminal: + self.terminal = AptTerminal() + else: + self.terminal = None + self.download_view = AptDownloadsView() + self.download_scrolled = Gtk.ScrolledWindow() + self.download_scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + self.download_scrolled.set_policy(Gtk.PolicyType.NEVER, + Gtk.PolicyType.AUTOMATIC) + self.download_scrolled.add(self.download_view) + self.download_scrolled.set_min_content_height(200) + hbox = Gtk.HBox() + hbox.pack_start(self.download_scrolled, True, True, 0) + if self.terminal: + hbox.pack_start(self.terminal, True, True, 0) + self.add(hbox) + if transaction is not None: + self.set_transaction(transaction) + + def set_transaction(self, transaction): + """Connect the status label to the given aptdaemon transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals.append( + transaction.connect("status-changed", self._on_status_changed)) + self._signals.append( + transaction.connect("terminal-attached-changed", + self._on_terminal_attached_changed)) + if self.terminal: + self.terminal.set_transaction(transaction) + self.download_view.set_transaction(transaction) + + def _on_status_changed(self, trans, status): + if status in (STATUS_DOWNLOADING, STATUS_DOWNLOADING_REPO): + self.set_sensitive(True) + self.download_scrolled.show() + if self.terminal: + self.terminal.hide() + elif status == STATUS_COMMITTING: + self.download_scrolled.hide() + if self.terminal: + self.terminal.show() + self.set_sensitive(True) + else: + self.set_expanded(False) + self.set_sensitive(False) + else: + self.download_scrolled.hide() + if self.terminal: + self.terminal.hide() + self.set_sensitive(False) + self.set_expanded(False) + + def _on_terminal_attached_changed(self, transaction, attached): + """Connect the terminal to the pty device""" + if attached and self.terminal: + self.set_sensitive(True) + + +class AptTerminal(Vte.Terminal): + + def __init__(self, transaction=None): + Vte.Terminal.__init__(self) + self._signals = [] + self._master, self._slave = pty.openpty() + self._ttyname = os.ttyname(self._slave) + self.set_size(80, 24) + self.set_vexpand(True) + self.set_pty(Vte.Pty.new_foreign_sync(self._master)) + if transaction is not None: + self.set_transaction(transaction) + + def set_transaction(self, transaction): + """Connect the status label to the given aptdaemon transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals.append( + transaction.connect("terminal-attached-changed", + self._on_terminal_attached_changed)) + self._transaction = transaction + self._transaction.set_terminal(self._ttyname) + + def _on_terminal_attached_changed(self, transaction, attached): + """Show the terminal""" + self.set_sensitive(attached) + + +class AptCancelButton(Gtk.Button): + """ + Provides a Gtk.Button which allows to cancel a running aptdaemon + transaction + """ + def __init__(self, transaction=None): + Gtk.Button.__init__(self) + self.set_use_stock(True) + self.set_label(Gtk.STOCK_CANCEL) + self.set_sensitive(True) + self._signals = [] + if transaction is not None: + self.set_transaction(transaction) + + def set_transaction(self, transaction): + """Connect the status label to the given aptdaemon transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals = [] + self._signals.append( + transaction.connect("finished", self._on_finished)) + self._signals.append( + transaction.connect("cancellable-changed", + self._on_cancellable_changed)) + self.connect("clicked", self._on_clicked, transaction) + + def _on_cancellable_changed(self, transaction, cancellable): + """ + Enable the button if cancel is allowed and disable it in the other case + """ + self.set_sensitive(cancellable) + + def _on_finished(self, transaction, status): + self.set_sensitive(False) + + def _on_clicked(self, button, transaction): + transaction.cancel() + self.set_sensitive(False) + + +class AptDownloadsView(Gtk.TreeView): + + """A Gtk.TreeView which displays the progress and status of each dowload + of a transaction. + """ + + COL_TEXT, COL_PROGRESS, COL_URI = list(range(3)) + + def __init__(self, transaction=None): + Gtk.TreeView.__init__(self) + model = Gtk.ListStore(GObject.TYPE_STRING, GObject.TYPE_INT, + GObject.TYPE_STRING) + self.set_model(model) + self.props.headers_visible = False + self.set_rules_hint(True) + self._download_map = {} + self._signals = [] + if transaction is not None: + self.set_transaction(transaction) + cell_uri = Gtk.CellRendererText() + cell_uri.props.ellipsize = Pango.EllipsizeMode.END + column_download = Gtk.TreeViewColumn(_("File")) + column_download.pack_start(cell_uri, True) + column_download.add_attribute(cell_uri, "markup", self.COL_TEXT) + cell_progress = Gtk.CellRendererProgress() + # TRANSLATORS: header of the progress download column + column_progress = Gtk.TreeViewColumn(_("%")) + column_progress.pack_start(cell_progress, True) + column_progress.set_cell_data_func(cell_progress, self._data_progress, + None) + self.append_column(column_progress) + self.append_column(column_download) + self.set_tooltip_column(self.COL_URI) + + def set_transaction(self, transaction): + """Connect the download view to the given aptdaemon transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals = [] + self._signals.append(transaction.connect("progress-download-changed", + self._on_download_changed)) + + def _on_download_changed(self, transaction, uri, status, desc, full_size, + downloaded, message): + """Callback for a changed download progress.""" + try: + progress = int(downloaded * 100 / full_size) + except ZeroDivisionError: + progress = -1 + if status == DOWNLOAD_DONE: + progress = 100 + if progress > 100: + progress = 100 + text = desc[:] + text += "\n<small>" + # TRANSLATORS: %s is the full size in Bytes, e.g. 198M + if status == DOWNLOAD_FETCHING: + text += (_("Downloaded %sB of %sB") % + (client.get_size_string(downloaded), + client.get_size_string(full_size))) + elif status == DOWNLOAD_DONE: + if full_size != 0: + text += (_("Downloaded %sB") % + client.get_size_string(full_size)) + else: + text += _("Downloaded") + else: + text += get_download_status_from_enum(status) + text += "</small>" + model = self.get_model() + # LP: #1266844 - we don't know how this can happy, model + # is never unset. but it does happen + if not model: + return + try: + iter = self._download_map[uri] + except KeyError: + # we we haven't seen the uri yet, add it now + iter = model.append((text, progress, uri)) + self._download_map[uri] = iter + # and update the adj if needed + adj = self.get_vadjustment() + # this may be None (LP: #1024590) + if adj: + is_scrolled_down = ( + adj.get_value() + adj.get_page_size() == adj.get_upper()) + if is_scrolled_down: + # If the treeview was scrolled to the end, do this again + # after appending a new item + self.scroll_to_cell( + model.get_path(iter), None, False, False, False) + else: + model.set_value(iter, self.COL_TEXT, text) + model.set_value(iter, self.COL_PROGRESS, progress) + + def _data_progress(self, column, cell, model, iter, data): + progress = model.get_value(iter, self.COL_PROGRESS) + if progress == -1: + cell.props.pulse = progress + else: + cell.props.value = progress + + +class AptProgressDialog(Gtk.Dialog): + """ + Complete progress dialog for long taking aptdaemon transactions, which + features a progress bar, cancel button, status icon and label + """ + + __gsignals__ = {"finished": (GObject.SignalFlags.RUN_FIRST, + GObject.TYPE_NONE, ())} + + def __init__(self, transaction=None, parent=None, terminal=True, + debconf=True): + Gtk.Dialog.__init__(self, parent=parent) + self._expanded_size = None + self.debconf = debconf + # Setup the dialog + self.set_border_width(6) + self.set_resizable(False) + self.get_content_area().set_spacing(6) + # Setup the cancel button + self.button_cancel = AptCancelButton(transaction) + self.get_action_area().pack_start(self.button_cancel, False, False, 0) + # Setup the status icon, label and progressbar + hbox = Gtk.HBox() + hbox.set_spacing(12) + hbox.set_border_width(6) + self.icon = AptRoleIcon() + hbox.pack_start(self.icon, False, True, 0) + vbox = Gtk.VBox() + vbox.set_spacing(12) + self.label_role = Gtk.Label() + self.label_role.set_alignment(0, 0) + vbox.pack_start(self.label_role, False, True, 0) + vbox_progress = Gtk.VBox() + vbox_progress.set_spacing(6) + self.progress = AptProgressBar() + vbox_progress.pack_start(self.progress, False, True, 0) + self.label = AptStatusLabel() + self.label._on_status_changed(None, STATUS_WAITING) + vbox_progress.pack_start(self.label, False, True, 0) + vbox.pack_start(vbox_progress, False, True, 0) + hbox.pack_start(vbox, True, True, 0) + self.expander = AptDetailsExpander(terminal=terminal) + self.expander.connect("notify::expanded", self._on_expanded) + vbox.pack_start(self.expander, True, True, 0) + self.get_content_area().pack_start(hbox, True, True, 0) + self._transaction = None + self._signals = [] + self.set_title("") + self.realize() + self.progress.set_size_request(350, -1) + functions = Gdk.WMFunction.MOVE | Gdk.WMFunction.RESIZE + try: + self.get_window().set_functions(functions) + except TypeError: + # workaround for older and broken GTK typelibs + self.get_window().set_functions(Gdk.WMFunction(functions)) + if transaction is not None: + self.set_transaction(transaction) + # catch ESC and behave as if cancel was clicked + self.connect("delete-event", self._on_dialog_delete_event) + + def _on_dialog_delete_event(self, dialog, event): + self.button_cancel.clicked() + return True + + def _on_expanded(self, expander, param): + # Make the dialog resizable if the expander is expanded + # try to restore a previous size + if not expander.get_expanded(): + self._expanded_size = (self.expander.terminal.get_visible(), + self.get_size()) + self.set_resizable(False) + elif self._expanded_size: + self.set_resizable(True) + term_visible, (stored_width, stored_height) = self._expanded_size + # Check if the stored size was for the download details or + # the terminal widget + if term_visible != self.expander.terminal.get_visible(): + # The stored size was for the download details, so we need + # get a new size for the terminal widget + self._resize_to_show_details() + else: + self.resize(stored_width, stored_height) + else: + self.set_resizable(True) + self._resize_to_show_details() + + def _resize_to_show_details(self): + """Resize the window to show the expanded details. + + Unfortunately the expander only expands to the preferred size of the + child widget (e.g showing all 80x24 chars of the Vte terminal) if + the window is rendered the first time and the terminal is also visible. + If the expander is expanded afterwards the window won't change its + size anymore. So we have to do this manually. See LP#840942 + """ + win_width, win_height = self.get_size() + exp_width = self.expander.get_allocation().width + exp_height = self.expander.get_allocation().height + if self.expander.terminal.get_visible(): + terminal_width = self.expander.terminal.get_char_width() * 80 + terminal_height = self.expander.terminal.get_char_height() * 24 + self.resize(terminal_width - exp_width + win_width, + terminal_height - exp_height + win_height) + else: + self.resize(win_width + 100, win_height + 200) + + def _on_status_changed(self, trans, status): + # Also resize the window if we switch from download details to + # the terminal window + if (status == STATUS_COMMITTING and + self.expander.terminal.get_visible()): + self._resize_to_show_details() + + @deferable + def run(self, attach=False, close_on_finished=True, show_error=True, + reply_handler=None, error_handler=None): + """Run the transaction and show the progress in the dialog. + + Keyword arguments: + attach -- do not start the transaction but instead only monitor + an already running one + close_on_finished -- if the dialog should be closed when the + transaction is complete + show_error -- show a dialog with the error message + """ + return self._run(attach, close_on_finished, show_error, + reply_handler, error_handler) + + @inline_callbacks + def _run(self, attach, close_on_finished, show_error, + reply_handler, error_handler): + try: + sig = self._transaction.connect("finished", self._on_finished, + close_on_finished, show_error) + self._signals.append(sig) + if attach: + yield self._transaction.sync() + else: + if self.debconf: + yield self._transaction.set_debconf_frontend("gnome") + yield self._transaction.run() + self.show_all() + except Exception as error: + if error_handler: + error_handler(error) + else: + raise + else: + if reply_handler: + reply_handler() + + def _on_role_changed(self, transaction, role_enum): + """Show the role of the transaction in the dialog interface""" + role = get_role_localised_present_from_enum(role_enum) + self.set_title(role) + self.label_role.set_markup("<big><b>%s</b></big>" % role) + + def set_transaction(self, transaction): + """Connect the dialog to the given aptdaemon transaction""" + for sig in self._signals: + GLib.source_remove(sig) + self._signals = [] + self._signals.append( + transaction.connect_after("status-changed", + self._on_status_changed)) + self._signals.append(transaction.connect("role-changed", + self._on_role_changed)) + self._signals.append(transaction.connect("medium-required", + self._on_medium_required)) + self._signals.append(transaction.connect("config-file-conflict", + self._on_config_file_conflict)) + self._on_role_changed(transaction, transaction.role) + self.progress.set_transaction(transaction) + self.icon.set_transaction(transaction) + self.label.set_transaction(transaction) + self.expander.set_transaction(transaction) + self._transaction = transaction + + def _on_medium_required(self, transaction, medium, drive): + dialog = AptMediumRequiredDialog(medium, drive, self) + res = dialog.run() + dialog.hide() + if res == Gtk.ResponseType.OK: + self._transaction.provide_medium(medium) + else: + self._transaction.cancel() + + def _on_config_file_conflict(self, transaction, old, new): + dialog = AptConfigFileConflictDialog(old, new, self) + res = dialog.run() + dialog.hide() + if res == Gtk.ResponseType.YES: + self._transaction.resolve_config_file_conflict(old, "replace") + else: + self._transaction.resolve_config_file_conflict(old, "keep") + + def _on_finished(self, transaction, status, close, show_error): + if close: + self.hide() + if status == EXIT_FAILED and show_error: + err_dia = AptErrorDialog(self._transaction.error, self) + err_dia.run() + err_dia.hide() + self.emit("finished") + + +class _ExpandableDialog(Gtk.Dialog): + + """Dialog with an expander.""" + + def __init__(self, parent=None, stock_type=None, expanded_child=None, + expander_label=None, title=None, message=None, buttons=None): + """Return an _AptDaemonDialog instance. + + Keyword arguments: + parent -- set the dialog transient for the given Gtk.Window + stock_type -- type of the Dialog, defaults to Gtk.STOCK_DIALOG_QUESTION + expanded_child -- Widget which should be expanded + expander_label -- label for the expander + title -- a news header like title of the dialog + message -- the message which should be shown in the dialog + buttons -- tuple containing button text/reponse id pairs, defaults + to a close button + """ + if not buttons: + buttons = (Gtk.STOCK_CLOSE, Gtk.ResponseType.CLOSE) + Gtk.Dialog.__init__(self, parent=parent) + self.set_title("") + self.add_buttons(*buttons) + self.set_resizable(False) + self.set_border_width(6) + self.get_content_area().set_spacing(12) + if not stock_type: + stock_type = Gtk.STOCK_DIALOG_QUESTION + icon = Gtk.Image.new_from_stock(stock_type, Gtk.IconSize.DIALOG) + icon.set_alignment(0, 0) + hbox_base = Gtk.HBox() + hbox_base.set_spacing(12) + hbox_base.set_border_width(6) + vbox_left = Gtk.VBox() + vbox_left.set_spacing(12) + hbox_base.pack_start(icon, False, True, 0) + hbox_base.pack_start(vbox_left, True, True, 0) + self.label = Gtk.Label() + self.label.set_selectable(True) + self.label.set_alignment(0, 0) + self.label.set_line_wrap(True) + vbox_left.pack_start(self.label, False, True, 0) + self.get_content_area().pack_start(hbox_base, True, True, 0) + # The expander widget + self.expander = Gtk.Expander(label=expander_label) + self.expander.set_spacing(6) + self.expander.set_use_underline(True) + self.expander.connect("notify::expanded", self._on_expanded) + self._expanded_size = None + vbox_left.pack_start(self.expander, True, True, 0) + # Set some initial data + text = "" + if title: + text = "<b><big>%s</big></b>" % title + if message: + if text: + text += "\n\n" + text += message + self.label.set_markup(text) + if expanded_child: + self.expander.add(expanded_child) + else: + self.expander.set_sensitive(False) + + def _on_expanded(self, expander, param): + if expander.get_expanded(): + self.set_resizable(True) + if self._expanded_size: + # Workaround a random crash during progress dialog expanding + # It seems that either the gtk.Window.get_size() method + # doesn't always return a tuple or that the + # gtk.Window.set_size() method doesn't correctly handle * + # arguments correctly, see LP#898851 + try: + self.resize(self._expanded_size[0], self._expanded_size[1]) + except (IndexError, TypeError): + pass + else: + self._expanded_size = self.get_size() + self.set_resizable(False) + + +class AptMediumRequiredDialog(Gtk.MessageDialog): + + """Dialog to ask for medium change.""" + + def __init__(self, medium, drive, parent=None): + Gtk.MessageDialog.__init__(self, parent=parent, + type=Gtk.MessageType.INFO) + # TRANSLATORS: %s represents the name of a CD or DVD + text = _("CD/DVD '%s' is required") % medium + # TRANSLATORS: %s is the name of the CD/DVD drive + desc = _("Please insert the above CD/DVD into the drive '%s' to " + "install software packages from it.") % drive + self.set_markup("<big><b>%s</b></big>\n\n%s" % (text, desc)) + self.add_buttons(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL, + _("C_ontinue"), Gtk.ResponseType.OK) + self.set_default_response(Gtk.ResponseType.OK) + + +class AptConfirmDialog(Gtk.Dialog): + + """Dialog to confirm the changes that would be required by a + transaction. + """ + + def __init__(self, trans, cache=None, parent=None): + """Return an AptConfirmDialog instance. + + Keyword arguments: + trans -- the transaction of which the dependencies should be shown + cache -- an optional apt.cache.Cache() instance to provide more details + about packages + parent -- set the dialog transient for the given Gtk.Window + """ + Gtk.Dialog.__init__(self, parent=parent) + self.add_button(Gtk.STOCK_CANCEL, Gtk.ResponseType.CANCEL) + self.add_button(_("C_ontinue"), Gtk.ResponseType.OK) + self.cache = cache + self.trans = trans + if isinstance(parent, Gdk.Window): + self.realize() + self.window.set_transient_for(parent) + else: + self.set_transient_for(parent) + self.set_resizable(True) + self.set_border_width(6) + self.get_content_area().set_spacing(12) + icon = Gtk.Image.new_from_stock(Gtk.STOCK_DIALOG_QUESTION, + Gtk.IconSize.DIALOG) + icon.set_alignment(0, 0) + hbox_base = Gtk.HBox() + hbox_base.set_spacing(12) + hbox_base.set_border_width(6) + vbox_left = Gtk.VBox() + vbox_left.set_spacing(12) + hbox_base.pack_start(icon, False, True, 0) + hbox_base.pack_start(vbox_left, True, True, 0) + self.label = Gtk.Label() + self.label.set_selectable(True) + self.label.set_alignment(0, 0) + vbox_left.pack_start(self.label, False, True, 0) + self.get_content_area().pack_start(hbox_base, True, True, 0) + self.treestore = Gtk.TreeStore(GObject.TYPE_STRING) + self.treeview = Gtk.TreeView.new_with_model(self.treestore) + self.treeview.set_headers_visible(False) + self.treeview.set_rules_hint(True) + self.column = Gtk.TreeViewColumn() + self.treeview.append_column(self.column) + cell_icon = Gtk.CellRendererPixbuf() + self.column.pack_start(cell_icon, False) + self.column.set_cell_data_func(cell_icon, self.render_package_icon, + None) + cell_desc = Gtk.CellRendererText() + self.column.pack_start(cell_desc, True) + self.column.set_cell_data_func(cell_desc, self.render_package_desc, + None) + self.scrolled = Gtk.ScrolledWindow() + self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.AUTOMATIC) + self.scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + self.scrolled.set_min_content_height(200) + self.scrolled.add(self.treeview) + vbox_left.pack_start(self.scrolled, True, True, 0) + self.set_default_response(Gtk.ResponseType.CANCEL) + + def _show_changes(self): + """Show a message and the dependencies in the dialog.""" + self.treestore.clear() + for index, msg in enumerate([_("Install"), + _("Reinstall"), + _("Remove"), + _("Purge"), + _("Upgrade"), + _("Downgrade"), + _("Skip upgrade")]): + if self.trans.dependencies[index]: + piter = self.treestore.append(None, ["<b>%s</b>" % msg]) + for pkg in self.trans.dependencies[index]: + for object in self.map_package(pkg): + self.treestore.append(piter, [str(object)]) + # If there is only one type of changes (e.g. only installs) expand the + # tree + # FIXME: adapt the title and message accordingly + # FIXME: Should we have different modes? Only show dependencies, only + # initial packages or both? + msg = _("Please take a look at the list of changes below.") + if len(self.treestore) == 1: + filtered_store = self.treestore.filter_new( + Gtk.TreePath.new_first()) + self.treeview.expand_all() + self.treeview.set_model(filtered_store) + self.treeview.set_show_expanders(False) + if self.trans.dependencies[PKGS_INSTALL]: + title = _("Additional software has to be installed") + elif self.trans.dependencies[PKGS_REINSTALL]: + title = _("Additional software has to be re-installed") + elif self.trans.dependencies[PKGS_REMOVE]: + title = _("Additional software has to be removed") + elif self.trans.dependencies[PKGS_PURGE]: + title = _("Additional software has to be purged") + elif self.trans.dependencies[PKGS_UPGRADE]: + title = _("Additional software has to be upgraded") + elif self.trans.dependencies[PKGS_DOWNGRADE]: + title = _("Additional software has to be downgraded") + elif self.trans.dependencies[PKGS_KEEP]: + title = _("Updates will be skipped") + if len(filtered_store) < 6: + self.set_resizable(False) + self.scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, + Gtk.PolicyType.NEVER) + else: + self.treeview.set_size_request(350, 200) + else: + title = _("Additional changes are required") + self.treeview.set_size_request(350, 200) + self.treeview.collapse_all() + if self.trans.download: + msg += "\n" + msg += (_("%sB will be downloaded in total.") % + client.get_size_string(self.trans.download)) + if self.trans.space < 0: + msg += "\n" + msg += (_("%sB of disk space will be freed.") % + client.get_size_string(self.trans.space)) + elif self.trans.space > 0: + msg += "\n" + msg += (_("%sB more disk space will be used.") % + client.get_size_string(self.trans.space)) + self.label.set_markup("<b><big>%s</big></b>\n\n%s" % (title, msg)) + + def map_package(self, pkg): + """Map a package to a different object type, e.g. applications + and return a list of those. + + By default return the package itself inside a list. + + Override this method if you don't want to store package names + in the treeview. + """ + return [pkg] + + def render_package_icon(self, column, cell, model, iter, data): + """Data func for the Gtk.CellRendererPixbuf which shows the package. + + Override this method if you want to show custom icons for + a package or map it to applications. + """ + path = model.get_path(iter) + if path.get_depth() == 0: + cell.props.visible = False + else: + cell.props.visible = True + cell.props.icon_name = "applications-other" + + def render_package_desc(self, column, cell, model, iter, data): + """Data func for the Gtk.CellRendererText which shows the package. + + Override this method if you want to show more information about + a package or map it to applications. + """ + value = model.get_value(iter, 0) + if not value: + return + try: + pkg_name, pkg_version = value.split("=")[0:2] + except ValueError: + pkg_name = value + pkg_version = None + try: + if pkg_version: + text = "%s (%s)\n<small>%s</small>" % ( + pkg_name, pkg_version, self.cache[pkg_name].summary) + else: + text = "%s\n<small>%s</small>" % ( + pkg_name, self.cache[pkg_name].summary) + except (KeyError, TypeError): + if pkg_version: + text = "%s (%s)" % (pkg_name, pkg_version) + else: + text = "%s" % pkg_name + cell.set_property("markup", text) + + def run(self): + self._show_changes() + self.show_all() + return Gtk.Dialog.run(self) + + +class AptConfigFileConflictDialog(_ExpandableDialog): + + """Dialog to resolve conflicts between local and shipped + configuration files. + """ + + def __init__(self, from_path, to_path, parent=None): + self.from_path = from_path + self.to_path = to_path + # TRANSLATORS: %s is a file path + title = _("Replace your changes in '%s' with a later version of " + "the configuration file?") % from_path + msg = _("If you don't know why the file is there already, it is " + "usually safe to replace it.") + scrolled = Gtk.ScrolledWindow(hexpand=True, vexpand=True) + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + self.diffview = DiffView() + self.diffview.set_size_request(-1, 200) + scrolled.add(self.diffview) + _ExpandableDialog.__init__(self, parent=parent, + expander_label=_("_Changes"), + expanded_child=scrolled, + title=title, message=msg, + buttons=(_("_Keep"), Gtk.ResponseType.NO, + _("_Replace"), + Gtk.ResponseType.YES)) + self.set_default_response(Gtk.ResponseType.YES) + + def run(self): + self.show_all() + self.diffview.show_diff(self.from_path, self.to_path) + return _ExpandableDialog.run(self) + + +REGEX_RANGE = "^@@ \\-(?P<from_start>[0-9]+)(?:,(?P<from_context>[0-9]+))? " \ + "\\+(?P<to_start>[0-9]+)(?:,(?P<to_context>[0-9]+))? @@" + + +class DiffView(Gtk.TextView): + + """Shows the difference between two files.""" + + ELLIPSIS = "[…]\n" + + def __init__(self): + self.textbuffer = Gtk.TextBuffer() + Gtk.TextView.__init__(self, buffer=self.textbuffer) + self.set_property("editable", False) + self.set_cursor_visible(False) + tags = self.textbuffer.get_tag_table() + # FIXME: How to get better colors? + tag_default = Gtk.TextTag.new("default") + tag_default.set_properties(font="Mono") + tags.add(tag_default) + tag_add = Gtk.TextTag.new("add") + tag_add.set_properties(font="Mono", + background='#8ae234') + tags.add(tag_add) + tag_remove = Gtk.TextTag.new("remove") + tag_remove.set_properties(font="Mono", + background='#ef2929') + tags.add(tag_remove) + tag_num = Gtk.TextTag.new("number") + tag_num.set_properties(font="Mono", + background='#eee') + tags.add(tag_num) + + def show_diff(self, from_path, to_path): + """Show the difference between two files.""" + # FIXME: Use gio + try: + with open(from_path) as fp: + from_lines = fp.readlines() + with open(to_path) as fp: + to_lines = fp.readlines() + except IOError: + return + + # helper function to work around current un-introspectability of + # varargs methods like insert_with_tags_by_name() + def insert_tagged_text(iter, text, tag): + # self.textbuffer.insert_with_tags_by_name(iter, text, tag) + offset = iter.get_offset() + self.textbuffer.insert(iter, text) + self.textbuffer.apply_tag_by_name( + tag, self.textbuffer.get_iter_at_offset(offset), iter) + + line_number = 0 + iter = self.textbuffer.get_start_iter() + for line in difflib.unified_diff(from_lines, to_lines, lineterm=""): + if line.startswith("@@"): + match = re.match(REGEX_RANGE, line) + if not match: + continue + line_number = int(match.group("from_start")) + if line_number > 1: + insert_tagged_text(iter, self.ELLIPSIS, "default") + elif line.startswith("---") or line.startswith("+++"): + continue + elif line.startswith(" "): + line_number += 1 + insert_tagged_text(iter, str(line_number), "number") + insert_tagged_text(iter, line, "default") + elif line.startswith("-"): + line_number += 1 + insert_tagged_text(iter, str(line_number), "number") + insert_tagged_text(iter, line, "remove") + elif line.startswith("+"): + spaces = " " * len(str(line_number)) + insert_tagged_text(iter, spaces, "number") + insert_tagged_text(iter, line, "add") + + +class _DetailsExpanderMessageDialog(_ExpandableDialog): + """ + Common base class for Apt*Dialog + """ + def __init__(self, text, desc, type, details=None, parent=None): + scrolled = Gtk.ScrolledWindow(hexpand=True, vexpand=True) + scrolled.set_policy(Gtk.PolicyType.AUTOMATIC, Gtk.PolicyType.AUTOMATIC) + scrolled.set_shadow_type(Gtk.ShadowType.ETCHED_IN) + textview = Gtk.TextView() + textview.set_wrap_mode(Gtk.WrapMode.WORD) + buffer = textview.get_buffer() + scrolled.add(textview) + # TRANSLATORS: expander label in the error dialog + _ExpandableDialog.__init__(self, parent=parent, + expander_label=_("_Details"), + expanded_child=scrolled, + title=text, message=desc, + stock_type=type) + self.show_all() + if details: + buffer.insert_at_cursor(details) + else: + self.expander.set_visible(False) + + +class AptErrorDialog(_DetailsExpanderMessageDialog): + """ + Dialog for aptdaemon errors with details in an expandable text view + """ + def __init__(self, error=None, parent=None): + text = get_error_string_from_enum(error.code) + desc = get_error_description_from_enum(error.code) + _DetailsExpanderMessageDialog.__init__( + self, text, desc, Gtk.STOCK_DIALOG_ERROR, error.details, parent) diff --git a/aptdaemon/lock.py b/aptdaemon/lock.py new file mode 100644 index 0000000..39b1795 --- /dev/null +++ b/aptdaemon/lock.py @@ -0,0 +1,198 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Handles the apt system lock""" +# Copyright (C) 2010 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("LockFailedError", "system") + +import fcntl +import os +import struct + +import apt_pkg +from gi.repository import GLib + +from aptdaemon import enums +from aptdaemon.errors import TransactionCancelled + + +class LockFailedError(Exception): + + """The locking of file failed.""" + + def __init__(self, flock, process=None): + """Return a new LockFailedError instance. + + Keyword arguments: + flock -- the path of the file lock + process -- the process which holds the lock or None + """ + msg = "Could not acquire lock on %s." % flock + if process: + msg += " The lock is held by %s." % process + Exception.__init__(self, msg) + self.flock = flock + self.process = process + + +class FileLock(object): + + """Represents a file lock.""" + + def __init__(self, path): + self.path = path + self.fd = None + + @property + def locked(self): + return self.fd is not None + + def acquire(self): + """Return the file descriptor of the lock file or raise + LockFailedError if the lock cannot be obtained. + """ + if self.fd: + return self.fd + fd_lock = apt_pkg.get_lock(self.path) + if fd_lock < 0: + process = get_locking_process_name(self.path) + raise LockFailedError(self.path, process) + else: + self.fd = fd_lock + return fd_lock + + def release(self): + """Relase the lock.""" + if self.fd: + os.close(self.fd) + self.fd = None + + +def get_locking_process_name(lock_path): + """Return the name of a process which holds a lock. It will be None if + the name cannot be retrivied. + """ + try: + fd_lock_read = open(lock_path, "r") + except IOError: + return None + else: + # Get the pid of the locking application + flk = struct.pack('hhQQi', fcntl.F_WRLCK, os.SEEK_SET, 0, 0, 0) + flk_ret = fcntl.fcntl(fd_lock_read, fcntl.F_GETLK, flk) + pid = struct.unpack("hhQQi", flk_ret)[4] + # Get the command of the pid + try: + with open("/proc/%s/status" % pid, "r") as fd_status: + try: + for key, value in (line.split(":") for line in + fd_status.readlines()): + if key == "Name": + return value.strip() + except Exception: + return None + except IOError: + return None + finally: + fd_lock_read.close() + return None + +apt_pkg.init() + +#: The lock for dpkg status file +_status_dir = os.path.dirname(apt_pkg.config.find_file("Dir::State::status")) +status_lock = FileLock(os.path.join(_status_dir, "lock")) +frontend_lock = FileLock(os.path.join(_status_dir, "lock-frontend")) + +#: The lock for the package archive +_archives_dir = apt_pkg.config.find_dir("Dir::Cache::Archives") +archive_lock = FileLock(os.path.join(_archives_dir, "lock")) + +#: The lock for the repository indexes +lists_lock = FileLock(os.path.join( + apt_pkg.config.find_dir("Dir::State::lists"), "lock")) + + +def acquire(): + """Acquire an exclusive lock for the package management system.""" + try: + for lock in frontend_lock, status_lock, archive_lock, lists_lock: + if not lock.locked: + lock.acquire() + except: + release() + raise + + os.environ['DPKG_FRONTEND_LOCKED'] = '1' + +def release(): + """Release an exclusive lock for the package management system.""" + for lock in lists_lock, archive_lock, status_lock, frontend_lock: + lock.release() + + try: + del os.environ['DPKG_FRONTEND_LOCKED'] + except KeyError: + pass + + +def wait_for_lock(trans, alt_lock=None): + """Acquire the system lock or the optionally given one. If the lock + cannot be obtained pause the transaction in the meantime. + + :param trans: the transaction + :param lock: optional alternative lock + """ + def watch_lock(): + """Helper to unpause the transaction if the lock can be obtained. + + Keyword arguments: + trans -- the corresponding transaction + alt_lock -- alternative lock to the system lock + """ + try: + if alt_lock: + alt_lock.acquire() + else: + acquire() + except LockFailedError: + return True + trans.paused = False + return True + + try: + if alt_lock: + alt_lock.acquire() + else: + acquire() + except LockFailedError as error: + trans.paused = True + trans.status = enums.STATUS_WAITING_LOCK + if error.process: + # TRANSLATORS: %s is the name of a package manager + msg = trans.gettext("Waiting for %s to exit") + trans.status_details = msg % error.process + lock_watch = GLib.timeout_add_seconds(3, watch_lock) + while trans.paused and not trans.cancelled: + GLib.main_context_default().iteration() + GLib.source_remove(lock_watch) + if trans.cancelled: + raise TransactionCancelled() + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/logger.py b/aptdaemon/logger.py new file mode 100644 index 0000000..291fb52 --- /dev/null +++ b/aptdaemon/logger.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Logging facilities for aptdaemon +""" +# Copyright (C) 2013 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("ColoredFormatter") + +import logging +import os + +# Define some foreground colors +BLACK = 30 +RED = 31 +GREEN = 32 +YELLOW = 33 +BLUE = 34 +MAGENTA = 35 +CYAN = 36 +WHITE = 37 + +# Terminal control sequences to format output +RESET_SEQ = "\033[0m" +COLOR_SEQ = "\033[1;%dm" +BOLD_SEQ = "\033[1m" + +COLORS = { + logging.WARN: YELLOW, + logging.INFO: BLUE, + logging.DEBUG: CYAN, + logging.CRITICAL: RED, + logging.ERROR: RED +} + + +class ColoredFormatter(logging.Formatter): + + """Adds some color to the log messages. + + http://stackoverflow.com/questions/384076/\ + how-can-i-color-python-logging-output + """ + + def __init__(self, fmt=None, datefmt=None, use_color=True): + logging.Formatter.__init__(self, fmt, datefmt) + if os.getenv("TERM") in ["xterm", "xterm-colored", "linux"]: + self.use_color = use_color + else: + self.use_color = False + + def format(self, record): + """Return the formated output string.""" + if self.use_color and record.levelno in COLORS: + record.levelname = (COLOR_SEQ % COLORS[record.levelno] + + record.levelname + + RESET_SEQ) + record.name = COLOR_SEQ % GREEN + record.name + RESET_SEQ + if record.levelno in [logging.CRITICAL, logging.ERROR]: + record.msg = COLOR_SEQ % RED + record.msg + RESET_SEQ + return logging.Formatter.format(self, record) diff --git a/aptdaemon/loop.py b/aptdaemon/loop.py new file mode 100644 index 0000000..cf66b33 --- /dev/null +++ b/aptdaemon/loop.py @@ -0,0 +1,33 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Main loop for aptdaemon.""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("mainloop", "get_main_loop") + +from gi.repository import GLib + +mainloop = GLib.MainLoop() + + +def get_main_loop(): + """Return the glib main loop as a singleton.""" + return mainloop + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/networking.py b/aptdaemon/networking.py new file mode 100644 index 0000000..94f993f --- /dev/null +++ b/aptdaemon/networking.py @@ -0,0 +1,267 @@ +# networking - Monitor the network status +# +# Copyright (c) 2010 Mohamed Amine IL Idrissi +# Copyright (c) 2011 Canonical +# Copyright (c) 2011 Sebastian Heinlein +# +# Author: Alex Chiang <achiang@canonical.com> +# Michael Vogt <michael.vogt@ubuntu.com> +# Mohamed Amine IL Idrissi <ilidrissiamine@gmail.com> +# Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License as +# published by the Free Software Foundation; either version 2 of the +# License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 +# USA + +from defer import Deferred, inline_callbacks, return_value +from gi.repository import GObject +from gi.repository import Gio +from gi.repository import GLib +from gi.repository import PackageKitGlib as pk +import dbus +from dbus.mainloop.glib import DBusGMainLoop +DBusGMainLoop(set_as_default=True) +import logging +import os + + +log = logging.getLogger("AptDaemon.NetMonitor") + + +class NetworkMonitorBase(GObject.GObject): + + """Check the network state.""" + + __gsignals__ = {"network-state-changed": (GObject.SignalFlags.RUN_FIRST, + None, + (GObject.TYPE_PYOBJECT,))} + + def __init__(self): + log.debug("Initializing network monitor") + GObject.GObject.__init__(self) + self._state = pk.NetworkEnum.ONLINE + + def _set_state(self, enum): + if self._state != enum: + log.debug("Network state changed: %s", enum) + self._state = enum + self.emit("network-state-changed", enum) + + def _get_state(self): + return self._state + + state = property(_get_state, _set_state) + + @inline_callbacks + def get_network_state(self): + """Update the network state.""" + return_value(self._state) + + +class ProcNetworkMonitor(NetworkMonitorBase): + + """Use the route information of the proc filesystem to detect + the network state. + """ + + def __init__(self): + log.debug("Initializing proc based network monitor") + NetworkMonitorBase.__init__(self) + self._state = pk.NetworkEnum.OFFLINE + self._file = Gio.File.new_for_path("/proc/net/route") + self._monitor = Gio.File.monitor(self._file, + Gio.FileMonitorFlags.NONE, + None) + self._monitor.connect("changed", + self._on_route_file_changed) + + def _on_route_file_changed(self, *args): + self.get_network_state() + + def _parse_route_file(self): + """Parse the route file - taken from PackageKit""" + with open("/proc/net/route") as route_file: + for line in route_file.readlines(): + rows = line.split("\t") + # The header line? + if rows[0] == "Iface": + continue + # A loopback device? + elif rows[0] == "lo": + continue + # Correct number of rows? + elif len(rows) != 11: + continue + # The route is a default gateway + elif rows[1] == "00000000": + break + # A gateway is set + elif rows[2] != "00000000": + break + else: + return pk.NetworkEnum.OFFLINE + return pk.NetworkEnum.ONLINE + + @inline_callbacks + def get_network_state(self): + """Update the network state.""" + self.state = self._parse_route_file() + return_value(self.state) + + +class NetworkManagerMonitor(NetworkMonitorBase): + + """Use NetworkManager to monitor network state.""" + + NM_DBUS_IFACE = "org.freedesktop.NetworkManager" + NM_ACTIVE_CONN_DBUS_IFACE = NM_DBUS_IFACE + ".Connection.Active" + NM_DEVICE_DBUS_IFACE = NM_DBUS_IFACE + ".Device" + + # The device type is unknown + NM_DEVICE_TYPE_UNKNOWN = 0 + # The device is wired Ethernet device + NM_DEVICE_TYPE_ETHERNET = 1 + # The device is an 802.11 WiFi device + NM_DEVICE_TYPE_WIFI = 2 + # The device is a GSM-based cellular WAN device + NM_DEVICE_TYPE_GSM = 3 + # The device is a CDMA/IS-95-based cellular WAN device + NM_DEVICE_TYPE_CDMA = 4 + + def __init__(self): + log.debug("Initializing NetworkManager monitor") + NetworkMonitorBase.__init__(self) + self.bus = dbus.SystemBus() + self.proxy = self.bus.get_object("org.freedesktop.NetworkManager", + "/org/freedesktop/NetworkManager") + self.proxy.connect_to_signal("PropertiesChanged", + self._on_nm_properties_changed, + dbus_interface=self.NM_DBUS_IFACE) + self.bus.add_signal_receiver( + self._on_nm_active_conn_props_changed, + signal_name="PropertiesChanged", + dbus_interface=self.NM_ACTIVE_CONN_DBUS_IFACE) + + @staticmethod + def get_dbus_property(proxy, interface, property): + """Small helper to get the property value of a dbus object.""" + props = dbus.Interface(proxy, "org.freedesktop.DBus.Properties") + deferred = Deferred() + props.Get(interface, property, + reply_handler=deferred.callback, + error_handler=deferred.errback) + return deferred + + @inline_callbacks + def _on_nm_properties_changed(self, props): + """Callback if NetworkManager properties changed.""" + if "ActiveConnections" in props: + if not props["ActiveConnections"]: + log.debug("There aren't any active connections") + self.state = pk.NetworkEnum.OFFLINE + else: + yield self.get_network_state() + + @inline_callbacks + def _on_nm_active_conn_props_changed(self, props): + """Callback if properties of the active connection changed.""" + if "Default" not in props: + return + yield self.get_network_state() + + @inline_callbacks + def _query_network_manager(self): + """Query NetworkManager about the network state.""" + state = pk.NetworkEnum.OFFLINE + try: + active_conns = yield self.get_dbus_property(self.proxy, + self.NM_DBUS_IFACE, + "ActiveConnections") + except dbus.DBusException: + log.warning("Failed to determinate network state") + return_value(state) + + for conn in active_conns: + conn_obj = self.bus.get_object(self.NM_DBUS_IFACE, conn) + try: + is_default = yield self.get_dbus_property( + conn_obj, self.NM_ACTIVE_CONN_DBUS_IFACE, "Default") + if not is_default: + continue + devs = yield self.get_dbus_property( + conn_obj, self.NM_ACTIVE_CONN_DBUS_IFACE, "Devices") + except dbus.DBusException: + log.warning("Failed to determinate network state") + break + priority_device_type = -1 + for dev in devs: + try: + dev_obj = self.bus.get_object(self.NM_DBUS_IFACE, dev) + dev_type = yield self.get_dbus_property( + dev_obj, self.NM_DEVICE_DBUS_IFACE, "DeviceType") + except dbus.DBusException: + log.warning("Failed to determinate network state") + return_value(pk.NetworkEnum.UNKNOWN) + # prioterizse device types, since a bridged GSM/CDMA connection + # should be returned as a GSM/CDMA one + # The NM_DEVICE_TYPE_* enums are luckly ordered in this sense. + if dev_type <= priority_device_type: + continue + priority_device_type = dev_type + + if dev_type in (self.NM_DEVICE_TYPE_GSM, + self.NM_DEVICE_TYPE_CDMA): + state = pk.NetworkEnum.MOBILE + elif dev_type == self.NM_DEVICE_TYPE_ETHERNET: + state = pk.NetworkEnum.WIRED + elif dev_type == self.NM_DEVICE_TYPE_WIFI: + state = pk.NetworkEnum.WIFI + elif dev_type == self.NM_DEVICE_TYPE_UNKNOWN: + state = pk.NetworkEnum.OFFLINE + else: + state = pk.NetworkEnum.ONLINE + return_value(state) + + @inline_callbacks + def get_network_state(self): + """Update the network state.""" + self.state = yield self._query_network_manager() + return_value(self.state) + + +def get_network_monitor(fallback=False): + """Return a network monitor.""" + if fallback: + return ProcNetworkMonitor() + try: + return NetworkManagerMonitor() + except dbus.DBusException: + pass + if os.path.exists("/proc/net/route"): + return ProcNetworkMonitor() + return NetworkMonitorBase() + + +if __name__ == "__main__": + @inline_callbacks + def _call_monitor(): + state = yield monitor.get_network_state() + print(("Initial network state: %s" % state)) + log_handler = logging.StreamHandler() + log.addHandler(log_handler) + log.setLevel(logging.DEBUG) + monitor = get_network_monitor(True) + _call_monitor() + loop = GLib.MainLoop() + loop.run() diff --git a/aptdaemon/pkenums.py b/aptdaemon/pkenums.py new file mode 100644 index 0000000..60a212d --- /dev/null +++ b/aptdaemon/pkenums.py @@ -0,0 +1,969 @@ +# This file was autogenerated from ../../../lib/packagekit-glib2/pk-enum.c by enum-converter.py + +class PackageKitEnum: + exit = ( "unknown", "success", "failed", "cancelled", "key-required", "eula-required", "media-change-required", "killed", "need-untrusted", "cancelled-priority", "skip-transaction", "repair-required", ) + status = ( "unknown", "wait", "setup", "running", "query", "info", "refresh-cache", "remove", "download", "install", "update", "cleanup", "obsolete", "dep-resolve", "sig-check", "test-commit", "commit", "request", "finished", "cancel", "download-repository", "download-packagelist", "download-filelist", "download-changelog", "download-group", "download-updateinfo", "repackaging", "loading-cache", "scan-applications", "generate-package-list", "waiting-for-lock", "waiting-for-auth", "scan-process-list", "check-executable-files", "check-libraries", "copy-files", ) + role = ( "unknown", "cancel", "get-depends", "get-details", "get-files", "get-packages", "get-repo-list", "get-requires", "get-update-detail", "get-updates", "install-files", "install-packages", "install-signature", "refresh-cache", "remove-packages", "repo-enable", "repo-set-data", "resolve", "search-details", "search-file", "search-group", "search-name", "update-packages", "what-provides", "accept-eula", "download-packages", "get-distro-upgrades", "get-categories", "get-old-transactions", "upgrade-system", "repair-system", ) + error = ( "unknown", "out-of-memory", "no-cache", "no-network", "not-supported", "internal-error", "gpg-failure", "filter-invalid", "package-id-invalid", "transaction-error", "transaction-cancelled", "package-not-installed", "package-not-found", "package-already-installed", "package-download-failed", "group-not-found", "group-list-invalid", "dep-resolution-failed", "create-thread-failed", "repo-not-found", "cannot-remove-system-package", "process-kill", "failed-initialization", "failed-finalise", "failed-config-parsing", "cannot-cancel", "cannot-get-lock", "no-packages-to-update", "cannot-write-repo-config", "local-install-failed", "bad-gpg-signature", "missing-gpg-signature", "cannot-install-source-package", "repo-configuration-error", "no-license-agreement", "file-conflicts", "package-conflicts", "repo-not-available", "invalid-package-file", "package-install-blocked", "package-corrupt", "all-packages-already-installed", "file-not-found", "no-more-mirrors-to-try", "no-distro-upgrade-data", "incompatible-architecture", "no-space-on-device", "media-change-required", "not-authorized", "update-not-found", "cannot-install-repo-unsigned", "cannot-update-repo-unsigned", "cannot-get-filelist", "cannot-get-requires", "cannot-disable-repository", "restricted-download", "package-failed-to-configure", "package-failed-to-build", "package-failed-to-install", "package-failed-to-remove", "failed-due-to-running-process", "package-database-changed", "provide-type-not-supported", "install-root-invalid", "cannot-fetch-sources", "cancelled-priority", "unfinished-transaction", "lock-required", ) + restart = ( "unknown", "none", "system", "session", "application", "security-system", "security-session", ) + message = ( "unknown", "broken-mirror", "connection-refused", "parameter-invalid", "priority-invalid", "backend-error", "daemon-error", "cache-being-rebuilt", "newer-package-exists", "could-not-find-package", "config-files-changed", "package-already-installed", "autoremove-ignored", "repo-metadata-download-failed", "repo-for-developers-only", "other-updates-held-back", ) + filter = ( "unknown", "none", "installed", "~installed", "devel", "~devel", "gui", "~gui", "free", "~free", "visible", "~visible", "supported", "~supported", "basename", "~basename", "newest", "~newest", "arch", "~arch", "source", "~source", "collections", "~collections", "application", "~application", ) + group = ( "unknown", "accessibility", "accessories", "education", "games", "graphics", "internet", "office", "other", "programming", "multimedia", "system", "desktop-gnome", "desktop-kde", "desktop-xfce", "desktop-other", "publishing", "servers", "fonts", "admin-tools", "legacy", "localization", "virtualization", "power-management", "security", "communication", "network", "maps", "repos", "science", "documentation", "electronics", "collections", "vendor", "newest", ) + update_state = ( "unknown", "testing", "unstable", "stable", ) + info = ( "unknown", "installed", "available", "low", "normal", "important", "security", "bugfix", "enhancement", "blocked", "downloading", "updating", "installing", "removing", "cleanup", "obsoleting", "collection-installed", "collection-available", "finished", "reinstalling", "downgrading", "preparing", "decompressing", "untrusted", "trusted", ) + sig_type = ( "unknown", "gpg", ) + upgrade = ( "unknown", "stable", "unstable", ) + provides = ( "unknown", "any", "modalias", "codec", "mimetype", "driver", "font", "postscript-driver", "plasma-service", "shared-library", "python-module", "language-support", ) + network = ( "unknown", "offline", "online", "wired", "wifi", "mobile", ) + media_type = ( "unknown", "cd", "dvd", "disc", ) + authorize_type = ( "unknown", "yes", "no", "interactive", ) + upgrade_kind = ( "unknown", "minimal", "default", "complete", ) + transaction_flag = ( "none", "only-trusted", "simulate", "only-download", ) + +# Constants + +AUTHORIZE_INTERACTIVE = "interactive" +AUTHORIZE_NO = "no" +AUTHORIZE_UNKNOWN = "unknown" +AUTHORIZE_YES = "yes" +DISTRO_UPGRADE_STABLE = "stable" +DISTRO_UPGRADE_UNKNOWN = "unknown" +DISTRO_UPGRADE_UNSTABLE = "unstable" +ERROR_ALL_PACKAGES_ALREADY_INSTALLED = "all-packages-already-installed" +ERROR_BAD_GPG_SIGNATURE = "bad-gpg-signature" +ERROR_CANCELLED_PRIORITY = "cancelled-priority" +ERROR_CANNOT_CANCEL = "cannot-cancel" +ERROR_CANNOT_DISABLE_REPOSITORY = "cannot-disable-repository" +ERROR_CANNOT_FETCH_SOURCES = "cannot-fetch-sources" +ERROR_CANNOT_GET_FILELIST = "cannot-get-filelist" +ERROR_CANNOT_GET_LOCK = "cannot-get-lock" +ERROR_CANNOT_GET_REQUIRES = "cannot-get-requires" +ERROR_CANNOT_INSTALL_REPO_UNSIGNED = "cannot-install-repo-unsigned" +ERROR_CANNOT_INSTALL_SOURCE_PACKAGE = "cannot-install-source-package" +ERROR_CANNOT_REMOVE_SYSTEM_PACKAGE = "cannot-remove-system-package" +ERROR_CANNOT_UPDATE_REPO_UNSIGNED = "cannot-update-repo-unsigned" +ERROR_CANNOT_WRITE_REPO_CONFIG = "cannot-write-repo-config" +ERROR_CREATE_THREAD_FAILED = "create-thread-failed" +ERROR_DEP_RESOLUTION_FAILED = "dep-resolution-failed" +ERROR_FAILED_CONFIG_PARSING = "failed-config-parsing" +ERROR_FAILED_FINALISE = "failed-finalise" +ERROR_FAILED_INITIALIZATION = "failed-initialization" +ERROR_FILE_CONFLICTS = "file-conflicts" +ERROR_FILE_NOT_FOUND = "file-not-found" +ERROR_FILTER_INVALID = "filter-invalid" +ERROR_GPG_FAILURE = "gpg-failure" +ERROR_GROUP_LIST_INVALID = "group-list-invalid" +ERROR_GROUP_NOT_FOUND = "group-not-found" +ERROR_INCOMPATIBLE_ARCHITECTURE = "incompatible-architecture" +ERROR_INSTALL_ROOT_INVALID = "install-root-invalid" +ERROR_INTERNAL_ERROR = "internal-error" +ERROR_INVALID_PACKAGE_FILE = "invalid-package-file" +ERROR_LOCAL_INSTALL_FAILED = "local-install-failed" +ERROR_LOCK_REQUIRED = "lock-required" +ERROR_MEDIA_CHANGE_REQUIRED = "media-change-required" +ERROR_MISSING_GPG_SIGNATURE = "missing-gpg-signature" +ERROR_NOT_AUTHORIZED = "not-authorized" +ERROR_NOT_SUPPORTED = "not-supported" +ERROR_NO_CACHE = "no-cache" +ERROR_NO_DISTRO_UPGRADE_DATA = "no-distro-upgrade-data" +ERROR_NO_LICENSE_AGREEMENT = "no-license-agreement" +ERROR_NO_MORE_MIRRORS_TO_TRY = "no-more-mirrors-to-try" +ERROR_NO_NETWORK = "no-network" +ERROR_NO_PACKAGES_TO_UPDATE = "no-packages-to-update" +ERROR_NO_SPACE_ON_DEVICE = "no-space-on-device" +ERROR_OOM = "out-of-memory" +ERROR_PACKAGE_ALREADY_INSTALLED = "package-already-installed" +ERROR_PACKAGE_CONFLICTS = "package-conflicts" +ERROR_PACKAGE_CORRUPT = "package-corrupt" +ERROR_PACKAGE_DATABASE_CHANGED = "package-database-changed" +ERROR_PACKAGE_DOWNLOAD_FAILED = "package-download-failed" +ERROR_PACKAGE_FAILED_TO_BUILD = "package-failed-to-build" +ERROR_PACKAGE_FAILED_TO_CONFIGURE = "package-failed-to-configure" +ERROR_PACKAGE_FAILED_TO_INSTALL = "package-failed-to-install" +ERROR_PACKAGE_FAILED_TO_REMOVE = "package-failed-to-remove" +ERROR_PACKAGE_ID_INVALID = "package-id-invalid" +ERROR_PACKAGE_INSTALL_BLOCKED = "package-install-blocked" +ERROR_PACKAGE_NOT_FOUND = "package-not-found" +ERROR_PACKAGE_NOT_INSTALLED = "package-not-installed" +ERROR_PROCESS_KILL = "process-kill" +ERROR_PROVIDE_TYPE_NOT_SUPPORTED = "provide-type-not-supported" +ERROR_REPO_CONFIGURATION_ERROR = "repo-configuration-error" +ERROR_REPO_NOT_AVAILABLE = "repo-not-available" +ERROR_REPO_NOT_FOUND = "repo-not-found" +ERROR_RESTRICTED_DOWNLOAD = "restricted-download" +ERROR_TRANSACTION_CANCELLED = "transaction-cancelled" +ERROR_TRANSACTION_ERROR = "transaction-error" +ERROR_UNFINISHED_TRANSACTION = "unfinished-transaction" +ERROR_UNKNOWN = "unknown" +ERROR_UPDATE_FAILED_DUE_TO_RUNNING_PROCESS = "failed-due-to-running-process" +ERROR_UPDATE_NOT_FOUND = "update-not-found" +EXIT_CANCELLED = "cancelled" +EXIT_CANCELLED_PRIORITY = "cancelled-priority" +EXIT_EULA_REQUIRED = "eula-required" +EXIT_FAILED = "failed" +EXIT_KEY_REQUIRED = "key-required" +EXIT_KILLED = "killed" +EXIT_MEDIA_CHANGE_REQUIRED = "media-change-required" +EXIT_NEED_UNTRUSTED = "need-untrusted" +EXIT_REPAIR_REQUIRED = "repair-required" +EXIT_SKIP_TRANSACTION = "skip-transaction" +EXIT_SUCCESS = "success" +EXIT_UNKNOWN = "unknown" +FILTER_APPLICATION = "application" +FILTER_ARCH = "arch" +FILTER_BASENAME = "basename" +FILTER_COLLECTIONS = "collections" +FILTER_DEVELOPMENT = "devel" +FILTER_FREE = "free" +FILTER_GUI = "gui" +FILTER_INSTALLED = "installed" +FILTER_NEWEST = "newest" +FILTER_NONE = "none" +FILTER_NOT_APPLICATION = "~application" +FILTER_NOT_ARCH = "~arch" +FILTER_NOT_BASENAME = "~basename" +FILTER_NOT_COLLECTIONS = "~collections" +FILTER_NOT_DEVELOPMENT = "~devel" +FILTER_NOT_FREE = "~free" +FILTER_NOT_GUI = "~gui" +FILTER_NOT_INSTALLED = "~installed" +FILTER_NOT_NEWEST = "~newest" +FILTER_NOT_SOURCE = "~source" +FILTER_NOT_SUPPORTED = "~supported" +FILTER_NOT_VISIBLE = "~visible" +FILTER_SOURCE = "source" +FILTER_SUPPORTED = "supported" +FILTER_UNKNOWN = "unknown" +FILTER_VISIBLE = "visible" +GROUP_ACCESSIBILITY = "accessibility" +GROUP_ACCESSORIES = "accessories" +GROUP_ADMIN_TOOLS = "admin-tools" +GROUP_COLLECTIONS = "collections" +GROUP_COMMUNICATION = "communication" +GROUP_DESKTOP_GNOME = "desktop-gnome" +GROUP_DESKTOP_KDE = "desktop-kde" +GROUP_DESKTOP_OTHER = "desktop-other" +GROUP_DESKTOP_XFCE = "desktop-xfce" +GROUP_DOCUMENTATION = "documentation" +GROUP_EDUCATION = "education" +GROUP_ELECTRONICS = "electronics" +GROUP_FONTS = "fonts" +GROUP_GAMES = "games" +GROUP_GRAPHICS = "graphics" +GROUP_INTERNET = "internet" +GROUP_LEGACY = "legacy" +GROUP_LOCALIZATION = "localization" +GROUP_MAPS = "maps" +GROUP_MULTIMEDIA = "multimedia" +GROUP_NETWORK = "network" +GROUP_NEWEST = "newest" +GROUP_OFFICE = "office" +GROUP_OTHER = "other" +GROUP_POWER_MANAGEMENT = "power-management" +GROUP_PROGRAMMING = "programming" +GROUP_PUBLISHING = "publishing" +GROUP_REPOS = "repos" +GROUP_SCIENCE = "science" +GROUP_SECURITY = "security" +GROUP_SERVERS = "servers" +GROUP_SYSTEM = "system" +GROUP_UNKNOWN = "unknown" +GROUP_VENDOR = "vendor" +GROUP_VIRTUALIZATION = "virtualization" +INFO_AVAILABLE = "available" +INFO_BLOCKED = "blocked" +INFO_BUGFIX = "bugfix" +INFO_CLEANUP = "cleanup" +INFO_COLLECTION_AVAILABLE = "collection-available" +INFO_COLLECTION_INSTALLED = "collection-installed" +INFO_DECOMPRESSING = "decompressing" +INFO_DOWNGRADING = "downgrading" +INFO_DOWNLOADING = "downloading" +INFO_ENHANCEMENT = "enhancement" +INFO_FINISHED = "finished" +INFO_IMPORTANT = "important" +INFO_INSTALLED = "installed" +INFO_INSTALLING = "installing" +INFO_LOW = "low" +INFO_NORMAL = "normal" +INFO_OBSOLETING = "obsoleting" +INFO_PREPARING = "preparing" +INFO_REINSTALLING = "reinstalling" +INFO_REMOVING = "removing" +INFO_SECURITY = "security" +INFO_TRUSTED = "trusted" +INFO_UNKNOWN = "unknown" +INFO_UNTRUSTED = "untrusted" +INFO_UPDATING = "updating" +MEDIA_TYPE_CD = "cd" +MEDIA_TYPE_DISC = "disc" +MEDIA_TYPE_DVD = "dvd" +MEDIA_TYPE_UNKNOWN = "unknown" +MESSAGE_AUTOREMOVE_IGNORED = "autoremove-ignored" +MESSAGE_BACKEND_ERROR = "backend-error" +MESSAGE_BROKEN_MIRROR = "broken-mirror" +MESSAGE_CACHE_BEING_REBUILT = "cache-being-rebuilt" +MESSAGE_CONFIG_FILES_CHANGED = "config-files-changed" +MESSAGE_CONNECTION_REFUSED = "connection-refused" +MESSAGE_COULD_NOT_FIND_PACKAGE = "could-not-find-package" +MESSAGE_DAEMON_ERROR = "daemon-error" +MESSAGE_NEWER_PACKAGE_EXISTS = "newer-package-exists" +MESSAGE_OTHER_UPDATES_HELD_BACK = "other-updates-held-back" +MESSAGE_PACKAGE_ALREADY_INSTALLED = "package-already-installed" +MESSAGE_PARAMETER_INVALID = "parameter-invalid" +MESSAGE_PRIORITY_INVALID = "priority-invalid" +MESSAGE_REPO_FOR_DEVELOPERS_ONLY = "repo-for-developers-only" +MESSAGE_REPO_METADATA_DOWNLOAD_FAILED = "repo-metadata-download-failed" +MESSAGE_UNKNOWN = "unknown" +NETWORK_MOBILE = "mobile" +NETWORK_OFFLINE = "offline" +NETWORK_ONLINE = "online" +NETWORK_UNKNOWN = "unknown" +NETWORK_WIFI = "wifi" +NETWORK_WIRED = "wired" +PROVIDES_ANY = "any" +PROVIDES_CODEC = "codec" +PROVIDES_FONT = "font" +PROVIDES_HARDWARE_DRIVER = "driver" +PROVIDES_LANGUAGE_SUPPORT = "language-support" +PROVIDES_MIMETYPE = "mimetype" +PROVIDES_MODALIAS = "modalias" +PROVIDES_PLASMA_SERVICE = "plasma-service" +PROVIDES_POSTSCRIPT_DRIVER = "postscript-driver" +PROVIDES_PYTHON = "python-module" +PROVIDES_SHARED_LIB = "shared-library" +PROVIDES_UNKNOWN = "unknown" +RESTART_APPLICATION = "application" +RESTART_NONE = "none" +RESTART_SECURITY_SESSION = "security-session" +RESTART_SECURITY_SYSTEM = "security-system" +RESTART_SESSION = "session" +RESTART_SYSTEM = "system" +RESTART_UNKNOWN = "unknown" +ROLE_ACCEPT_EULA = "accept-eula" +ROLE_CANCEL = "cancel" +ROLE_DOWNLOAD_PACKAGES = "download-packages" +ROLE_GET_CATEGORIES = "get-categories" +ROLE_GET_DEPENDS = "get-depends" +ROLE_GET_DETAILS = "get-details" +ROLE_GET_DISTRO_UPGRADES = "get-distro-upgrades" +ROLE_GET_FILES = "get-files" +ROLE_GET_OLD_TRANSACTIONS = "get-old-transactions" +ROLE_GET_PACKAGES = "get-packages" +ROLE_GET_REPO_LIST = "get-repo-list" +ROLE_GET_REQUIRES = "get-requires" +ROLE_GET_UPDATES = "get-updates" +ROLE_GET_UPDATE_DETAIL = "get-update-detail" +ROLE_INSTALL_FILES = "install-files" +ROLE_INSTALL_PACKAGES = "install-packages" +ROLE_INSTALL_SIGNATURE = "install-signature" +ROLE_REFRESH_CACHE = "refresh-cache" +ROLE_REMOVE_PACKAGES = "remove-packages" +ROLE_REPAIR_SYSTEM = "repair-system" +ROLE_REPO_ENABLE = "repo-enable" +ROLE_REPO_SET_DATA = "repo-set-data" +ROLE_RESOLVE = "resolve" +ROLE_SEARCH_DETAILS = "search-details" +ROLE_SEARCH_FILE = "search-file" +ROLE_SEARCH_GROUP = "search-group" +ROLE_SEARCH_NAME = "search-name" +ROLE_UNKNOWN = "unknown" +ROLE_UPDATE_PACKAGES = "update-packages" +ROLE_UPGRADE_SYSTEM = "upgrade-system" +ROLE_WHAT_PROVIDES = "what-provides" +SIGTYPE_GPG = "gpg" +SIGTYPE_UNKNOWN = "unknown" +STATUS_CANCEL = "cancel" +STATUS_CHECK_EXECUTABLE_FILES = "check-executable-files" +STATUS_CHECK_LIBRARIES = "check-libraries" +STATUS_CLEANUP = "cleanup" +STATUS_COMMIT = "commit" +STATUS_COPY_FILES = "copy-files" +STATUS_DEP_RESOLVE = "dep-resolve" +STATUS_DOWNLOAD = "download" +STATUS_DOWNLOAD_CHANGELOG = "download-changelog" +STATUS_DOWNLOAD_FILELIST = "download-filelist" +STATUS_DOWNLOAD_GROUP = "download-group" +STATUS_DOWNLOAD_PACKAGELIST = "download-packagelist" +STATUS_DOWNLOAD_REPOSITORY = "download-repository" +STATUS_DOWNLOAD_UPDATEINFO = "download-updateinfo" +STATUS_FINISHED = "finished" +STATUS_GENERATE_PACKAGE_LIST = "generate-package-list" +STATUS_INFO = "info" +STATUS_INSTALL = "install" +STATUS_LOADING_CACHE = "loading-cache" +STATUS_OBSOLETE = "obsolete" +STATUS_QUERY = "query" +STATUS_REFRESH_CACHE = "refresh-cache" +STATUS_REMOVE = "remove" +STATUS_REPACKAGING = "repackaging" +STATUS_REQUEST = "request" +STATUS_RUNNING = "running" +STATUS_SCAN_APPLICATIONS = "scan-applications" +STATUS_SCAN_PROCESS_LIST = "scan-process-list" +STATUS_SETUP = "setup" +STATUS_SIG_CHECK = "sig-check" +STATUS_TEST_COMMIT = "test-commit" +STATUS_UNKNOWN = "unknown" +STATUS_UPDATE = "update" +STATUS_WAIT = "wait" +STATUS_WAITING_FOR_AUTH = "waiting-for-auth" +STATUS_WAITING_FOR_LOCK = "waiting-for-lock" +TRANSACTION_FLAG_NONE = "none" +TRANSACTION_FLAG_ONLY_DOWNLOAD = "only-download" +TRANSACTION_FLAG_ONLY_TRUSTED = "only-trusted" +TRANSACTION_FLAG_SIMULATE = "simulate" +UPDATE_STATE_STABLE = "stable" +UPDATE_STATE_TESTING = "testing" +UPDATE_STATE_UNKNOWN = "unknown" +UPDATE_STATE_UNSTABLE = "unstable" +UPGRADE_KIND_COMPLETE = "complete" +UPGRADE_KIND_DEFAULT = "default" +UPGRADE_KIND_MINIMAL = "minimal" +UPGRADE_KIND_UNKNOWN = "unknown" +# This file was autogenerated from ../../lib/packagekit-glib2/pk-enum.c by enum-converter.py + +class PackageKitEnum: + exit = ( "unknown", "success", "failed", "cancelled", "key-required", "eula-required", "media-change-required", "killed", "need-untrusted", "cancelled-priority", "skip-transaction", "repair-required", ) + status = ( "unknown", "wait", "setup", "running", "query", "info", "refresh-cache", "remove", "download", "install", "update", "cleanup", "obsolete", "dep-resolve", "sig-check", "test-commit", "commit", "request", "finished", "cancel", "download-repository", "download-packagelist", "download-filelist", "download-changelog", "download-group", "download-updateinfo", "repackaging", "loading-cache", "scan-applications", "generate-package-list", "waiting-for-lock", "waiting-for-auth", "scan-process-list", "check-executable-files", "check-libraries", "copy-files", ) + role = ( "unknown", "cancel", "get-depends", "get-details", "get-files", "get-packages", "get-repo-list", "get-requires", "get-update-detail", "get-updates", "install-files", "install-packages", "install-signature", "refresh-cache", "remove-packages", "repo-enable", "repo-set-data", "resolve", "search-details", "search-file", "search-group", "search-name", "update-packages", "what-provides", "accept-eula", "download-packages", "get-distro-upgrades", "get-categories", "get-old-transactions", "upgrade-system", "repair-system", ) + error = ( "unknown", "out-of-memory", "no-cache", "no-network", "not-supported", "internal-error", "gpg-failure", "filter-invalid", "package-id-invalid", "transaction-error", "transaction-cancelled", "package-not-installed", "package-not-found", "package-already-installed", "package-download-failed", "group-not-found", "group-list-invalid", "dep-resolution-failed", "create-thread-failed", "repo-not-found", "cannot-remove-system-package", "process-kill", "failed-initialization", "failed-finalise", "failed-config-parsing", "cannot-cancel", "cannot-get-lock", "no-packages-to-update", "cannot-write-repo-config", "local-install-failed", "bad-gpg-signature", "missing-gpg-signature", "cannot-install-source-package", "repo-configuration-error", "no-license-agreement", "file-conflicts", "package-conflicts", "repo-not-available", "invalid-package-file", "package-install-blocked", "package-corrupt", "all-packages-already-installed", "file-not-found", "no-more-mirrors-to-try", "no-distro-upgrade-data", "incompatible-architecture", "no-space-on-device", "media-change-required", "not-authorized", "update-not-found", "cannot-install-repo-unsigned", "cannot-update-repo-unsigned", "cannot-get-filelist", "cannot-get-requires", "cannot-disable-repository", "restricted-download", "package-failed-to-configure", "package-failed-to-build", "package-failed-to-install", "package-failed-to-remove", "failed-due-to-running-process", "package-database-changed", "provide-type-not-supported", "install-root-invalid", "cannot-fetch-sources", "cancelled-priority", "unfinished-transaction", "lock-required", ) + restart = ( "unknown", "none", "system", "session", "application", "security-system", "security-session", ) + message = ( "unknown", "broken-mirror", "connection-refused", "parameter-invalid", "priority-invalid", "backend-error", "daemon-error", "cache-being-rebuilt", "newer-package-exists", "could-not-find-package", "config-files-changed", "package-already-installed", "autoremove-ignored", "repo-metadata-download-failed", "repo-for-developers-only", "other-updates-held-back", ) + filter = ( "unknown", "none", "installed", "~installed", "devel", "~devel", "gui", "~gui", "free", "~free", "visible", "~visible", "supported", "~supported", "basename", "~basename", "newest", "~newest", "arch", "~arch", "source", "~source", "collections", "~collections", "application", "~application", ) + group = ( "unknown", "accessibility", "accessories", "education", "games", "graphics", "internet", "office", "other", "programming", "multimedia", "system", "desktop-gnome", "desktop-kde", "desktop-xfce", "desktop-other", "publishing", "servers", "fonts", "admin-tools", "legacy", "localization", "virtualization", "power-management", "security", "communication", "network", "maps", "repos", "science", "documentation", "electronics", "collections", "vendor", "newest", ) + update_state = ( "unknown", "testing", "unstable", "stable", ) + info = ( "unknown", "installed", "available", "low", "normal", "important", "security", "bugfix", "enhancement", "blocked", "downloading", "updating", "installing", "removing", "cleanup", "obsoleting", "collection-installed", "collection-available", "finished", "reinstalling", "downgrading", "preparing", "decompressing", "untrusted", "trusted", ) + sig_type = ( "unknown", "gpg", ) + upgrade = ( "unknown", "stable", "unstable", ) + provides = ( "unknown", "any", "modalias", "codec", "mimetype", "driver", "font", "postscript-driver", "plasma-service", "shared-library", "python-module", "language-support", ) + network = ( "unknown", "offline", "online", "wired", "wifi", "mobile", ) + media_type = ( "unknown", "cd", "dvd", "disc", ) + authorize_type = ( "unknown", "yes", "no", "interactive", ) + upgrade_kind = ( "unknown", "minimal", "default", "complete", ) + transaction_flag = ( "none", "only-trusted", "simulate", "only-download", ) + +# Constants + +AUTHORIZE_INTERACTIVE = "interactive" +AUTHORIZE_NO = "no" +AUTHORIZE_UNKNOWN = "unknown" +AUTHORIZE_YES = "yes" +DISTRO_UPGRADE_STABLE = "stable" +DISTRO_UPGRADE_UNKNOWN = "unknown" +DISTRO_UPGRADE_UNSTABLE = "unstable" +ERROR_ALL_PACKAGES_ALREADY_INSTALLED = "all-packages-already-installed" +ERROR_BAD_GPG_SIGNATURE = "bad-gpg-signature" +ERROR_CANCELLED_PRIORITY = "cancelled-priority" +ERROR_CANNOT_CANCEL = "cannot-cancel" +ERROR_CANNOT_DISABLE_REPOSITORY = "cannot-disable-repository" +ERROR_CANNOT_FETCH_SOURCES = "cannot-fetch-sources" +ERROR_CANNOT_GET_FILELIST = "cannot-get-filelist" +ERROR_CANNOT_GET_LOCK = "cannot-get-lock" +ERROR_CANNOT_GET_REQUIRES = "cannot-get-requires" +ERROR_CANNOT_INSTALL_REPO_UNSIGNED = "cannot-install-repo-unsigned" +ERROR_CANNOT_INSTALL_SOURCE_PACKAGE = "cannot-install-source-package" +ERROR_CANNOT_REMOVE_SYSTEM_PACKAGE = "cannot-remove-system-package" +ERROR_CANNOT_UPDATE_REPO_UNSIGNED = "cannot-update-repo-unsigned" +ERROR_CANNOT_WRITE_REPO_CONFIG = "cannot-write-repo-config" +ERROR_CREATE_THREAD_FAILED = "create-thread-failed" +ERROR_DEP_RESOLUTION_FAILED = "dep-resolution-failed" +ERROR_FAILED_CONFIG_PARSING = "failed-config-parsing" +ERROR_FAILED_FINALISE = "failed-finalise" +ERROR_FAILED_INITIALIZATION = "failed-initialization" +ERROR_FILE_CONFLICTS = "file-conflicts" +ERROR_FILE_NOT_FOUND = "file-not-found" +ERROR_FILTER_INVALID = "filter-invalid" +ERROR_GPG_FAILURE = "gpg-failure" +ERROR_GROUP_LIST_INVALID = "group-list-invalid" +ERROR_GROUP_NOT_FOUND = "group-not-found" +ERROR_INCOMPATIBLE_ARCHITECTURE = "incompatible-architecture" +ERROR_INSTALL_ROOT_INVALID = "install-root-invalid" +ERROR_INTERNAL_ERROR = "internal-error" +ERROR_INVALID_PACKAGE_FILE = "invalid-package-file" +ERROR_LOCAL_INSTALL_FAILED = "local-install-failed" +ERROR_LOCK_REQUIRED = "lock-required" +ERROR_MEDIA_CHANGE_REQUIRED = "media-change-required" +ERROR_MISSING_GPG_SIGNATURE = "missing-gpg-signature" +ERROR_NOT_AUTHORIZED = "not-authorized" +ERROR_NOT_SUPPORTED = "not-supported" +ERROR_NO_CACHE = "no-cache" +ERROR_NO_DISTRO_UPGRADE_DATA = "no-distro-upgrade-data" +ERROR_NO_LICENSE_AGREEMENT = "no-license-agreement" +ERROR_NO_MORE_MIRRORS_TO_TRY = "no-more-mirrors-to-try" +ERROR_NO_NETWORK = "no-network" +ERROR_NO_PACKAGES_TO_UPDATE = "no-packages-to-update" +ERROR_NO_SPACE_ON_DEVICE = "no-space-on-device" +ERROR_OOM = "out-of-memory" +ERROR_PACKAGE_ALREADY_INSTALLED = "package-already-installed" +ERROR_PACKAGE_CONFLICTS = "package-conflicts" +ERROR_PACKAGE_CORRUPT = "package-corrupt" +ERROR_PACKAGE_DATABASE_CHANGED = "package-database-changed" +ERROR_PACKAGE_DOWNLOAD_FAILED = "package-download-failed" +ERROR_PACKAGE_FAILED_TO_BUILD = "package-failed-to-build" +ERROR_PACKAGE_FAILED_TO_CONFIGURE = "package-failed-to-configure" +ERROR_PACKAGE_FAILED_TO_INSTALL = "package-failed-to-install" +ERROR_PACKAGE_FAILED_TO_REMOVE = "package-failed-to-remove" +ERROR_PACKAGE_ID_INVALID = "package-id-invalid" +ERROR_PACKAGE_INSTALL_BLOCKED = "package-install-blocked" +ERROR_PACKAGE_NOT_FOUND = "package-not-found" +ERROR_PACKAGE_NOT_INSTALLED = "package-not-installed" +ERROR_PROCESS_KILL = "process-kill" +ERROR_PROVIDE_TYPE_NOT_SUPPORTED = "provide-type-not-supported" +ERROR_REPO_CONFIGURATION_ERROR = "repo-configuration-error" +ERROR_REPO_NOT_AVAILABLE = "repo-not-available" +ERROR_REPO_NOT_FOUND = "repo-not-found" +ERROR_RESTRICTED_DOWNLOAD = "restricted-download" +ERROR_TRANSACTION_CANCELLED = "transaction-cancelled" +ERROR_TRANSACTION_ERROR = "transaction-error" +ERROR_UNFINISHED_TRANSACTION = "unfinished-transaction" +ERROR_UNKNOWN = "unknown" +ERROR_UPDATE_FAILED_DUE_TO_RUNNING_PROCESS = "failed-due-to-running-process" +ERROR_UPDATE_NOT_FOUND = "update-not-found" +EXIT_CANCELLED = "cancelled" +EXIT_CANCELLED_PRIORITY = "cancelled-priority" +EXIT_EULA_REQUIRED = "eula-required" +EXIT_FAILED = "failed" +EXIT_KEY_REQUIRED = "key-required" +EXIT_KILLED = "killed" +EXIT_MEDIA_CHANGE_REQUIRED = "media-change-required" +EXIT_NEED_UNTRUSTED = "need-untrusted" +EXIT_REPAIR_REQUIRED = "repair-required" +EXIT_SKIP_TRANSACTION = "skip-transaction" +EXIT_SUCCESS = "success" +EXIT_UNKNOWN = "unknown" +FILTER_APPLICATION = "application" +FILTER_ARCH = "arch" +FILTER_BASENAME = "basename" +FILTER_COLLECTIONS = "collections" +FILTER_DEVELOPMENT = "devel" +FILTER_FREE = "free" +FILTER_GUI = "gui" +FILTER_INSTALLED = "installed" +FILTER_NEWEST = "newest" +FILTER_NONE = "none" +FILTER_NOT_APPLICATION = "~application" +FILTER_NOT_ARCH = "~arch" +FILTER_NOT_BASENAME = "~basename" +FILTER_NOT_COLLECTIONS = "~collections" +FILTER_NOT_DEVELOPMENT = "~devel" +FILTER_NOT_FREE = "~free" +FILTER_NOT_GUI = "~gui" +FILTER_NOT_INSTALLED = "~installed" +FILTER_NOT_NEWEST = "~newest" +FILTER_NOT_SOURCE = "~source" +FILTER_NOT_SUPPORTED = "~supported" +FILTER_NOT_VISIBLE = "~visible" +FILTER_SOURCE = "source" +FILTER_SUPPORTED = "supported" +FILTER_UNKNOWN = "unknown" +FILTER_VISIBLE = "visible" +GROUP_ACCESSIBILITY = "accessibility" +GROUP_ACCESSORIES = "accessories" +GROUP_ADMIN_TOOLS = "admin-tools" +GROUP_COLLECTIONS = "collections" +GROUP_COMMUNICATION = "communication" +GROUP_DESKTOP_GNOME = "desktop-gnome" +GROUP_DESKTOP_KDE = "desktop-kde" +GROUP_DESKTOP_OTHER = "desktop-other" +GROUP_DESKTOP_XFCE = "desktop-xfce" +GROUP_DOCUMENTATION = "documentation" +GROUP_EDUCATION = "education" +GROUP_ELECTRONICS = "electronics" +GROUP_FONTS = "fonts" +GROUP_GAMES = "games" +GROUP_GRAPHICS = "graphics" +GROUP_INTERNET = "internet" +GROUP_LEGACY = "legacy" +GROUP_LOCALIZATION = "localization" +GROUP_MAPS = "maps" +GROUP_MULTIMEDIA = "multimedia" +GROUP_NETWORK = "network" +GROUP_NEWEST = "newest" +GROUP_OFFICE = "office" +GROUP_OTHER = "other" +GROUP_POWER_MANAGEMENT = "power-management" +GROUP_PROGRAMMING = "programming" +GROUP_PUBLISHING = "publishing" +GROUP_REPOS = "repos" +GROUP_SCIENCE = "science" +GROUP_SECURITY = "security" +GROUP_SERVERS = "servers" +GROUP_SYSTEM = "system" +GROUP_UNKNOWN = "unknown" +GROUP_VENDOR = "vendor" +GROUP_VIRTUALIZATION = "virtualization" +INFO_AVAILABLE = "available" +INFO_BLOCKED = "blocked" +INFO_BUGFIX = "bugfix" +INFO_CLEANUP = "cleanup" +INFO_COLLECTION_AVAILABLE = "collection-available" +INFO_COLLECTION_INSTALLED = "collection-installed" +INFO_DECOMPRESSING = "decompressing" +INFO_DOWNGRADING = "downgrading" +INFO_DOWNLOADING = "downloading" +INFO_ENHANCEMENT = "enhancement" +INFO_FINISHED = "finished" +INFO_IMPORTANT = "important" +INFO_INSTALLED = "installed" +INFO_INSTALLING = "installing" +INFO_LOW = "low" +INFO_NORMAL = "normal" +INFO_OBSOLETING = "obsoleting" +INFO_PREPARING = "preparing" +INFO_REINSTALLING = "reinstalling" +INFO_REMOVING = "removing" +INFO_SECURITY = "security" +INFO_TRUSTED = "trusted" +INFO_UNKNOWN = "unknown" +INFO_UNTRUSTED = "untrusted" +INFO_UPDATING = "updating" +MEDIA_TYPE_CD = "cd" +MEDIA_TYPE_DISC = "disc" +MEDIA_TYPE_DVD = "dvd" +MEDIA_TYPE_UNKNOWN = "unknown" +MESSAGE_AUTOREMOVE_IGNORED = "autoremove-ignored" +MESSAGE_BACKEND_ERROR = "backend-error" +MESSAGE_BROKEN_MIRROR = "broken-mirror" +MESSAGE_CACHE_BEING_REBUILT = "cache-being-rebuilt" +MESSAGE_CONFIG_FILES_CHANGED = "config-files-changed" +MESSAGE_CONNECTION_REFUSED = "connection-refused" +MESSAGE_COULD_NOT_FIND_PACKAGE = "could-not-find-package" +MESSAGE_DAEMON_ERROR = "daemon-error" +MESSAGE_NEWER_PACKAGE_EXISTS = "newer-package-exists" +MESSAGE_OTHER_UPDATES_HELD_BACK = "other-updates-held-back" +MESSAGE_PACKAGE_ALREADY_INSTALLED = "package-already-installed" +MESSAGE_PARAMETER_INVALID = "parameter-invalid" +MESSAGE_PRIORITY_INVALID = "priority-invalid" +MESSAGE_REPO_FOR_DEVELOPERS_ONLY = "repo-for-developers-only" +MESSAGE_REPO_METADATA_DOWNLOAD_FAILED = "repo-metadata-download-failed" +MESSAGE_UNKNOWN = "unknown" +NETWORK_MOBILE = "mobile" +NETWORK_OFFLINE = "offline" +NETWORK_ONLINE = "online" +NETWORK_UNKNOWN = "unknown" +NETWORK_WIFI = "wifi" +NETWORK_WIRED = "wired" +PROVIDES_ANY = "any" +PROVIDES_CODEC = "codec" +PROVIDES_FONT = "font" +PROVIDES_HARDWARE_DRIVER = "driver" +PROVIDES_LANGUAGE_SUPPORT = "language-support" +PROVIDES_MIMETYPE = "mimetype" +PROVIDES_MODALIAS = "modalias" +PROVIDES_PLASMA_SERVICE = "plasma-service" +PROVIDES_POSTSCRIPT_DRIVER = "postscript-driver" +PROVIDES_PYTHON = "python-module" +PROVIDES_SHARED_LIB = "shared-library" +PROVIDES_UNKNOWN = "unknown" +RESTART_APPLICATION = "application" +RESTART_NONE = "none" +RESTART_SECURITY_SESSION = "security-session" +RESTART_SECURITY_SYSTEM = "security-system" +RESTART_SESSION = "session" +RESTART_SYSTEM = "system" +RESTART_UNKNOWN = "unknown" +ROLE_ACCEPT_EULA = "accept-eula" +ROLE_CANCEL = "cancel" +ROLE_DOWNLOAD_PACKAGES = "download-packages" +ROLE_GET_CATEGORIES = "get-categories" +ROLE_GET_DEPENDS = "get-depends" +ROLE_GET_DETAILS = "get-details" +ROLE_GET_DISTRO_UPGRADES = "get-distro-upgrades" +ROLE_GET_FILES = "get-files" +ROLE_GET_OLD_TRANSACTIONS = "get-old-transactions" +ROLE_GET_PACKAGES = "get-packages" +ROLE_GET_REPO_LIST = "get-repo-list" +ROLE_GET_REQUIRES = "get-requires" +ROLE_GET_UPDATES = "get-updates" +ROLE_GET_UPDATE_DETAIL = "get-update-detail" +ROLE_INSTALL_FILES = "install-files" +ROLE_INSTALL_PACKAGES = "install-packages" +ROLE_INSTALL_SIGNATURE = "install-signature" +ROLE_REFRESH_CACHE = "refresh-cache" +ROLE_REMOVE_PACKAGES = "remove-packages" +ROLE_REPAIR_SYSTEM = "repair-system" +ROLE_REPO_ENABLE = "repo-enable" +ROLE_REPO_SET_DATA = "repo-set-data" +ROLE_RESOLVE = "resolve" +ROLE_SEARCH_DETAILS = "search-details" +ROLE_SEARCH_FILE = "search-file" +ROLE_SEARCH_GROUP = "search-group" +ROLE_SEARCH_NAME = "search-name" +ROLE_UNKNOWN = "unknown" +ROLE_UPDATE_PACKAGES = "update-packages" +ROLE_UPGRADE_SYSTEM = "upgrade-system" +ROLE_WHAT_PROVIDES = "what-provides" +SIGTYPE_GPG = "gpg" +SIGTYPE_UNKNOWN = "unknown" +STATUS_CANCEL = "cancel" +STATUS_CHECK_EXECUTABLE_FILES = "check-executable-files" +STATUS_CHECK_LIBRARIES = "check-libraries" +STATUS_CLEANUP = "cleanup" +STATUS_COMMIT = "commit" +STATUS_COPY_FILES = "copy-files" +STATUS_DEP_RESOLVE = "dep-resolve" +STATUS_DOWNLOAD = "download" +STATUS_DOWNLOAD_CHANGELOG = "download-changelog" +STATUS_DOWNLOAD_FILELIST = "download-filelist" +STATUS_DOWNLOAD_GROUP = "download-group" +STATUS_DOWNLOAD_PACKAGELIST = "download-packagelist" +STATUS_DOWNLOAD_REPOSITORY = "download-repository" +STATUS_DOWNLOAD_UPDATEINFO = "download-updateinfo" +STATUS_FINISHED = "finished" +STATUS_GENERATE_PACKAGE_LIST = "generate-package-list" +STATUS_INFO = "info" +STATUS_INSTALL = "install" +STATUS_LOADING_CACHE = "loading-cache" +STATUS_OBSOLETE = "obsolete" +STATUS_QUERY = "query" +STATUS_REFRESH_CACHE = "refresh-cache" +STATUS_REMOVE = "remove" +STATUS_REPACKAGING = "repackaging" +STATUS_REQUEST = "request" +STATUS_RUNNING = "running" +STATUS_SCAN_APPLICATIONS = "scan-applications" +STATUS_SCAN_PROCESS_LIST = "scan-process-list" +STATUS_SETUP = "setup" +STATUS_SIG_CHECK = "sig-check" +STATUS_TEST_COMMIT = "test-commit" +STATUS_UNKNOWN = "unknown" +STATUS_UPDATE = "update" +STATUS_WAIT = "wait" +STATUS_WAITING_FOR_AUTH = "waiting-for-auth" +STATUS_WAITING_FOR_LOCK = "waiting-for-lock" +TRANSACTION_FLAG_NONE = "none" +TRANSACTION_FLAG_ONLY_DOWNLOAD = "only-download" +TRANSACTION_FLAG_ONLY_TRUSTED = "only-trusted" +TRANSACTION_FLAG_SIMULATE = "simulate" +UPDATE_STATE_STABLE = "stable" +UPDATE_STATE_TESTING = "testing" +UPDATE_STATE_UNKNOWN = "unknown" +UPDATE_STATE_UNSTABLE = "unstable" +UPGRADE_KIND_COMPLETE = "complete" +UPGRADE_KIND_DEFAULT = "default" +UPGRADE_KIND_MINIMAL = "minimal" +UPGRADE_KIND_UNKNOWN = "unknown" +# This file was autogenerated from ../../lib/packagekit-glib2/pk-enum.c by enum-converter.py + +class PackageKitEnum: + exit = ( "unknown", "success", "failed", "cancelled", "key-required", "eula-required", "media-change-required", "killed", "need-untrusted", "cancelled-priority", "skip-transaction", "repair-required", ) + status = ( "unknown", "wait", "setup", "running", "query", "info", "refresh-cache", "remove", "download", "install", "update", "cleanup", "obsolete", "dep-resolve", "sig-check", "test-commit", "commit", "request", "finished", "cancel", "download-repository", "download-packagelist", "download-filelist", "download-changelog", "download-group", "download-updateinfo", "repackaging", "loading-cache", "scan-applications", "generate-package-list", "waiting-for-lock", "waiting-for-auth", "scan-process-list", "check-executable-files", "check-libraries", "copy-files", ) + role = ( "unknown", "cancel", "get-depends", "get-details", "get-files", "get-packages", "get-repo-list", "get-requires", "get-update-detail", "get-updates", "install-files", "install-packages", "install-signature", "refresh-cache", "remove-packages", "repo-enable", "repo-set-data", "resolve", "search-details", "search-file", "search-group", "search-name", "update-packages", "what-provides", "accept-eula", "download-packages", "get-distro-upgrades", "get-categories", "get-old-transactions", "upgrade-system", "repair-system", ) + error = ( "unknown", "out-of-memory", "no-cache", "no-network", "not-supported", "internal-error", "gpg-failure", "filter-invalid", "package-id-invalid", "transaction-error", "transaction-cancelled", "package-not-installed", "package-not-found", "package-already-installed", "package-download-failed", "group-not-found", "group-list-invalid", "dep-resolution-failed", "create-thread-failed", "repo-not-found", "cannot-remove-system-package", "process-kill", "failed-initialization", "failed-finalise", "failed-config-parsing", "cannot-cancel", "cannot-get-lock", "no-packages-to-update", "cannot-write-repo-config", "local-install-failed", "bad-gpg-signature", "missing-gpg-signature", "cannot-install-source-package", "repo-configuration-error", "no-license-agreement", "file-conflicts", "package-conflicts", "repo-not-available", "invalid-package-file", "package-install-blocked", "package-corrupt", "all-packages-already-installed", "file-not-found", "no-more-mirrors-to-try", "no-distro-upgrade-data", "incompatible-architecture", "no-space-on-device", "media-change-required", "not-authorized", "update-not-found", "cannot-install-repo-unsigned", "cannot-update-repo-unsigned", "cannot-get-filelist", "cannot-get-requires", "cannot-disable-repository", "restricted-download", "package-failed-to-configure", "package-failed-to-build", "package-failed-to-install", "package-failed-to-remove", "failed-due-to-running-process", "package-database-changed", "provide-type-not-supported", "install-root-invalid", "cannot-fetch-sources", "cancelled-priority", "unfinished-transaction", "lock-required", ) + restart = ( "unknown", "none", "system", "session", "application", "security-system", "security-session", ) + message = ( "unknown", "broken-mirror", "connection-refused", "parameter-invalid", "priority-invalid", "backend-error", "daemon-error", "cache-being-rebuilt", "newer-package-exists", "could-not-find-package", "config-files-changed", "package-already-installed", "autoremove-ignored", "repo-metadata-download-failed", "repo-for-developers-only", "other-updates-held-back", ) + filter = ( "unknown", "none", "installed", "~installed", "devel", "~devel", "gui", "~gui", "free", "~free", "visible", "~visible", "supported", "~supported", "basename", "~basename", "newest", "~newest", "arch", "~arch", "source", "~source", "collections", "~collections", "application", "~application", ) + group = ( "unknown", "accessibility", "accessories", "education", "games", "graphics", "internet", "office", "other", "programming", "multimedia", "system", "desktop-gnome", "desktop-kde", "desktop-xfce", "desktop-other", "publishing", "servers", "fonts", "admin-tools", "legacy", "localization", "virtualization", "power-management", "security", "communication", "network", "maps", "repos", "science", "documentation", "electronics", "collections", "vendor", "newest", ) + update_state = ( "unknown", "testing", "unstable", "stable", ) + info = ( "unknown", "installed", "available", "low", "normal", "important", "security", "bugfix", "enhancement", "blocked", "downloading", "updating", "installing", "removing", "cleanup", "obsoleting", "collection-installed", "collection-available", "finished", "reinstalling", "downgrading", "preparing", "decompressing", "untrusted", "trusted", ) + sig_type = ( "unknown", "gpg", ) + upgrade = ( "unknown", "stable", "unstable", ) + provides = ( "unknown", "any", "modalias", "codec", "mimetype", "driver", "font", "postscript-driver", "plasma-service", "shared-library", "python-module", "language-support", ) + network = ( "unknown", "offline", "online", "wired", "wifi", "mobile", ) + media_type = ( "unknown", "cd", "dvd", "disc", ) + authorize_type = ( "unknown", "yes", "no", "interactive", ) + upgrade_kind = ( "unknown", "minimal", "default", "complete", ) + transaction_flag = ( "none", "only-trusted", "simulate", "only-download", ) + +# Constants + +AUTHORIZE_INTERACTIVE = "interactive" +AUTHORIZE_NO = "no" +AUTHORIZE_UNKNOWN = "unknown" +AUTHORIZE_YES = "yes" +DISTRO_UPGRADE_STABLE = "stable" +DISTRO_UPGRADE_UNKNOWN = "unknown" +DISTRO_UPGRADE_UNSTABLE = "unstable" +ERROR_ALL_PACKAGES_ALREADY_INSTALLED = "all-packages-already-installed" +ERROR_BAD_GPG_SIGNATURE = "bad-gpg-signature" +ERROR_CANCELLED_PRIORITY = "cancelled-priority" +ERROR_CANNOT_CANCEL = "cannot-cancel" +ERROR_CANNOT_DISABLE_REPOSITORY = "cannot-disable-repository" +ERROR_CANNOT_FETCH_SOURCES = "cannot-fetch-sources" +ERROR_CANNOT_GET_FILELIST = "cannot-get-filelist" +ERROR_CANNOT_GET_LOCK = "cannot-get-lock" +ERROR_CANNOT_GET_REQUIRES = "cannot-get-requires" +ERROR_CANNOT_INSTALL_REPO_UNSIGNED = "cannot-install-repo-unsigned" +ERROR_CANNOT_INSTALL_SOURCE_PACKAGE = "cannot-install-source-package" +ERROR_CANNOT_REMOVE_SYSTEM_PACKAGE = "cannot-remove-system-package" +ERROR_CANNOT_UPDATE_REPO_UNSIGNED = "cannot-update-repo-unsigned" +ERROR_CANNOT_WRITE_REPO_CONFIG = "cannot-write-repo-config" +ERROR_CREATE_THREAD_FAILED = "create-thread-failed" +ERROR_DEP_RESOLUTION_FAILED = "dep-resolution-failed" +ERROR_FAILED_CONFIG_PARSING = "failed-config-parsing" +ERROR_FAILED_FINALISE = "failed-finalise" +ERROR_FAILED_INITIALIZATION = "failed-initialization" +ERROR_FILE_CONFLICTS = "file-conflicts" +ERROR_FILE_NOT_FOUND = "file-not-found" +ERROR_FILTER_INVALID = "filter-invalid" +ERROR_GPG_FAILURE = "gpg-failure" +ERROR_GROUP_LIST_INVALID = "group-list-invalid" +ERROR_GROUP_NOT_FOUND = "group-not-found" +ERROR_INCOMPATIBLE_ARCHITECTURE = "incompatible-architecture" +ERROR_INSTALL_ROOT_INVALID = "install-root-invalid" +ERROR_INTERNAL_ERROR = "internal-error" +ERROR_INVALID_PACKAGE_FILE = "invalid-package-file" +ERROR_LOCAL_INSTALL_FAILED = "local-install-failed" +ERROR_LOCK_REQUIRED = "lock-required" +ERROR_MEDIA_CHANGE_REQUIRED = "media-change-required" +ERROR_MISSING_GPG_SIGNATURE = "missing-gpg-signature" +ERROR_NOT_AUTHORIZED = "not-authorized" +ERROR_NOT_SUPPORTED = "not-supported" +ERROR_NO_CACHE = "no-cache" +ERROR_NO_DISTRO_UPGRADE_DATA = "no-distro-upgrade-data" +ERROR_NO_LICENSE_AGREEMENT = "no-license-agreement" +ERROR_NO_MORE_MIRRORS_TO_TRY = "no-more-mirrors-to-try" +ERROR_NO_NETWORK = "no-network" +ERROR_NO_PACKAGES_TO_UPDATE = "no-packages-to-update" +ERROR_NO_SPACE_ON_DEVICE = "no-space-on-device" +ERROR_OOM = "out-of-memory" +ERROR_PACKAGE_ALREADY_INSTALLED = "package-already-installed" +ERROR_PACKAGE_CONFLICTS = "package-conflicts" +ERROR_PACKAGE_CORRUPT = "package-corrupt" +ERROR_PACKAGE_DATABASE_CHANGED = "package-database-changed" +ERROR_PACKAGE_DOWNLOAD_FAILED = "package-download-failed" +ERROR_PACKAGE_FAILED_TO_BUILD = "package-failed-to-build" +ERROR_PACKAGE_FAILED_TO_CONFIGURE = "package-failed-to-configure" +ERROR_PACKAGE_FAILED_TO_INSTALL = "package-failed-to-install" +ERROR_PACKAGE_FAILED_TO_REMOVE = "package-failed-to-remove" +ERROR_PACKAGE_ID_INVALID = "package-id-invalid" +ERROR_PACKAGE_INSTALL_BLOCKED = "package-install-blocked" +ERROR_PACKAGE_NOT_FOUND = "package-not-found" +ERROR_PACKAGE_NOT_INSTALLED = "package-not-installed" +ERROR_PROCESS_KILL = "process-kill" +ERROR_PROVIDE_TYPE_NOT_SUPPORTED = "provide-type-not-supported" +ERROR_REPO_CONFIGURATION_ERROR = "repo-configuration-error" +ERROR_REPO_NOT_AVAILABLE = "repo-not-available" +ERROR_REPO_NOT_FOUND = "repo-not-found" +ERROR_RESTRICTED_DOWNLOAD = "restricted-download" +ERROR_TRANSACTION_CANCELLED = "transaction-cancelled" +ERROR_TRANSACTION_ERROR = "transaction-error" +ERROR_UNFINISHED_TRANSACTION = "unfinished-transaction" +ERROR_UNKNOWN = "unknown" +ERROR_UPDATE_FAILED_DUE_TO_RUNNING_PROCESS = "failed-due-to-running-process" +ERROR_UPDATE_NOT_FOUND = "update-not-found" +EXIT_CANCELLED = "cancelled" +EXIT_CANCELLED_PRIORITY = "cancelled-priority" +EXIT_EULA_REQUIRED = "eula-required" +EXIT_FAILED = "failed" +EXIT_KEY_REQUIRED = "key-required" +EXIT_KILLED = "killed" +EXIT_MEDIA_CHANGE_REQUIRED = "media-change-required" +EXIT_NEED_UNTRUSTED = "need-untrusted" +EXIT_REPAIR_REQUIRED = "repair-required" +EXIT_SKIP_TRANSACTION = "skip-transaction" +EXIT_SUCCESS = "success" +EXIT_UNKNOWN = "unknown" +FILTER_APPLICATION = "application" +FILTER_ARCH = "arch" +FILTER_BASENAME = "basename" +FILTER_COLLECTIONS = "collections" +FILTER_DEVELOPMENT = "devel" +FILTER_FREE = "free" +FILTER_GUI = "gui" +FILTER_INSTALLED = "installed" +FILTER_NEWEST = "newest" +FILTER_NONE = "none" +FILTER_NOT_APPLICATION = "~application" +FILTER_NOT_ARCH = "~arch" +FILTER_NOT_BASENAME = "~basename" +FILTER_NOT_COLLECTIONS = "~collections" +FILTER_NOT_DEVELOPMENT = "~devel" +FILTER_NOT_FREE = "~free" +FILTER_NOT_GUI = "~gui" +FILTER_NOT_INSTALLED = "~installed" +FILTER_NOT_NEWEST = "~newest" +FILTER_NOT_SOURCE = "~source" +FILTER_NOT_SUPPORTED = "~supported" +FILTER_NOT_VISIBLE = "~visible" +FILTER_SOURCE = "source" +FILTER_SUPPORTED = "supported" +FILTER_UNKNOWN = "unknown" +FILTER_VISIBLE = "visible" +GROUP_ACCESSIBILITY = "accessibility" +GROUP_ACCESSORIES = "accessories" +GROUP_ADMIN_TOOLS = "admin-tools" +GROUP_COLLECTIONS = "collections" +GROUP_COMMUNICATION = "communication" +GROUP_DESKTOP_GNOME = "desktop-gnome" +GROUP_DESKTOP_KDE = "desktop-kde" +GROUP_DESKTOP_OTHER = "desktop-other" +GROUP_DESKTOP_XFCE = "desktop-xfce" +GROUP_DOCUMENTATION = "documentation" +GROUP_EDUCATION = "education" +GROUP_ELECTRONICS = "electronics" +GROUP_FONTS = "fonts" +GROUP_GAMES = "games" +GROUP_GRAPHICS = "graphics" +GROUP_INTERNET = "internet" +GROUP_LEGACY = "legacy" +GROUP_LOCALIZATION = "localization" +GROUP_MAPS = "maps" +GROUP_MULTIMEDIA = "multimedia" +GROUP_NETWORK = "network" +GROUP_NEWEST = "newest" +GROUP_OFFICE = "office" +GROUP_OTHER = "other" +GROUP_POWER_MANAGEMENT = "power-management" +GROUP_PROGRAMMING = "programming" +GROUP_PUBLISHING = "publishing" +GROUP_REPOS = "repos" +GROUP_SCIENCE = "science" +GROUP_SECURITY = "security" +GROUP_SERVERS = "servers" +GROUP_SYSTEM = "system" +GROUP_UNKNOWN = "unknown" +GROUP_VENDOR = "vendor" +GROUP_VIRTUALIZATION = "virtualization" +INFO_AVAILABLE = "available" +INFO_BLOCKED = "blocked" +INFO_BUGFIX = "bugfix" +INFO_CLEANUP = "cleanup" +INFO_COLLECTION_AVAILABLE = "collection-available" +INFO_COLLECTION_INSTALLED = "collection-installed" +INFO_DECOMPRESSING = "decompressing" +INFO_DOWNGRADING = "downgrading" +INFO_DOWNLOADING = "downloading" +INFO_ENHANCEMENT = "enhancement" +INFO_FINISHED = "finished" +INFO_IMPORTANT = "important" +INFO_INSTALLED = "installed" +INFO_INSTALLING = "installing" +INFO_LOW = "low" +INFO_NORMAL = "normal" +INFO_OBSOLETING = "obsoleting" +INFO_PREPARING = "preparing" +INFO_REINSTALLING = "reinstalling" +INFO_REMOVING = "removing" +INFO_SECURITY = "security" +INFO_TRUSTED = "trusted" +INFO_UNKNOWN = "unknown" +INFO_UNTRUSTED = "untrusted" +INFO_UPDATING = "updating" +MEDIA_TYPE_CD = "cd" +MEDIA_TYPE_DISC = "disc" +MEDIA_TYPE_DVD = "dvd" +MEDIA_TYPE_UNKNOWN = "unknown" +MESSAGE_AUTOREMOVE_IGNORED = "autoremove-ignored" +MESSAGE_BACKEND_ERROR = "backend-error" +MESSAGE_BROKEN_MIRROR = "broken-mirror" +MESSAGE_CACHE_BEING_REBUILT = "cache-being-rebuilt" +MESSAGE_CONFIG_FILES_CHANGED = "config-files-changed" +MESSAGE_CONNECTION_REFUSED = "connection-refused" +MESSAGE_COULD_NOT_FIND_PACKAGE = "could-not-find-package" +MESSAGE_DAEMON_ERROR = "daemon-error" +MESSAGE_NEWER_PACKAGE_EXISTS = "newer-package-exists" +MESSAGE_OTHER_UPDATES_HELD_BACK = "other-updates-held-back" +MESSAGE_PACKAGE_ALREADY_INSTALLED = "package-already-installed" +MESSAGE_PARAMETER_INVALID = "parameter-invalid" +MESSAGE_PRIORITY_INVALID = "priority-invalid" +MESSAGE_REPO_FOR_DEVELOPERS_ONLY = "repo-for-developers-only" +MESSAGE_REPO_METADATA_DOWNLOAD_FAILED = "repo-metadata-download-failed" +MESSAGE_UNKNOWN = "unknown" +NETWORK_MOBILE = "mobile" +NETWORK_OFFLINE = "offline" +NETWORK_ONLINE = "online" +NETWORK_UNKNOWN = "unknown" +NETWORK_WIFI = "wifi" +NETWORK_WIRED = "wired" +PROVIDES_ANY = "any" +PROVIDES_CODEC = "codec" +PROVIDES_FONT = "font" +PROVIDES_HARDWARE_DRIVER = "driver" +PROVIDES_LANGUAGE_SUPPORT = "language-support" +PROVIDES_MIMETYPE = "mimetype" +PROVIDES_MODALIAS = "modalias" +PROVIDES_PLASMA_SERVICE = "plasma-service" +PROVIDES_POSTSCRIPT_DRIVER = "postscript-driver" +PROVIDES_PYTHON = "python-module" +PROVIDES_SHARED_LIB = "shared-library" +PROVIDES_UNKNOWN = "unknown" +RESTART_APPLICATION = "application" +RESTART_NONE = "none" +RESTART_SECURITY_SESSION = "security-session" +RESTART_SECURITY_SYSTEM = "security-system" +RESTART_SESSION = "session" +RESTART_SYSTEM = "system" +RESTART_UNKNOWN = "unknown" +ROLE_ACCEPT_EULA = "accept-eula" +ROLE_CANCEL = "cancel" +ROLE_DOWNLOAD_PACKAGES = "download-packages" +ROLE_GET_CATEGORIES = "get-categories" +ROLE_GET_DEPENDS = "get-depends" +ROLE_GET_DETAILS = "get-details" +ROLE_GET_DISTRO_UPGRADES = "get-distro-upgrades" +ROLE_GET_FILES = "get-files" +ROLE_GET_OLD_TRANSACTIONS = "get-old-transactions" +ROLE_GET_PACKAGES = "get-packages" +ROLE_GET_REPO_LIST = "get-repo-list" +ROLE_GET_REQUIRES = "get-requires" +ROLE_GET_UPDATES = "get-updates" +ROLE_GET_UPDATE_DETAIL = "get-update-detail" +ROLE_INSTALL_FILES = "install-files" +ROLE_INSTALL_PACKAGES = "install-packages" +ROLE_INSTALL_SIGNATURE = "install-signature" +ROLE_REFRESH_CACHE = "refresh-cache" +ROLE_REMOVE_PACKAGES = "remove-packages" +ROLE_REPAIR_SYSTEM = "repair-system" +ROLE_REPO_ENABLE = "repo-enable" +ROLE_REPO_SET_DATA = "repo-set-data" +ROLE_RESOLVE = "resolve" +ROLE_SEARCH_DETAILS = "search-details" +ROLE_SEARCH_FILE = "search-file" +ROLE_SEARCH_GROUP = "search-group" +ROLE_SEARCH_NAME = "search-name" +ROLE_UNKNOWN = "unknown" +ROLE_UPDATE_PACKAGES = "update-packages" +ROLE_UPGRADE_SYSTEM = "upgrade-system" +ROLE_WHAT_PROVIDES = "what-provides" +SIGTYPE_GPG = "gpg" +SIGTYPE_UNKNOWN = "unknown" +STATUS_CANCEL = "cancel" +STATUS_CHECK_EXECUTABLE_FILES = "check-executable-files" +STATUS_CHECK_LIBRARIES = "check-libraries" +STATUS_CLEANUP = "cleanup" +STATUS_COMMIT = "commit" +STATUS_COPY_FILES = "copy-files" +STATUS_DEP_RESOLVE = "dep-resolve" +STATUS_DOWNLOAD = "download" +STATUS_DOWNLOAD_CHANGELOG = "download-changelog" +STATUS_DOWNLOAD_FILELIST = "download-filelist" +STATUS_DOWNLOAD_GROUP = "download-group" +STATUS_DOWNLOAD_PACKAGELIST = "download-packagelist" +STATUS_DOWNLOAD_REPOSITORY = "download-repository" +STATUS_DOWNLOAD_UPDATEINFO = "download-updateinfo" +STATUS_FINISHED = "finished" +STATUS_GENERATE_PACKAGE_LIST = "generate-package-list" +STATUS_INFO = "info" +STATUS_INSTALL = "install" +STATUS_LOADING_CACHE = "loading-cache" +STATUS_OBSOLETE = "obsolete" +STATUS_QUERY = "query" +STATUS_REFRESH_CACHE = "refresh-cache" +STATUS_REMOVE = "remove" +STATUS_REPACKAGING = "repackaging" +STATUS_REQUEST = "request" +STATUS_RUNNING = "running" +STATUS_SCAN_APPLICATIONS = "scan-applications" +STATUS_SCAN_PROCESS_LIST = "scan-process-list" +STATUS_SETUP = "setup" +STATUS_SIG_CHECK = "sig-check" +STATUS_TEST_COMMIT = "test-commit" +STATUS_UNKNOWN = "unknown" +STATUS_UPDATE = "update" +STATUS_WAIT = "wait" +STATUS_WAITING_FOR_AUTH = "waiting-for-auth" +STATUS_WAITING_FOR_LOCK = "waiting-for-lock" +TRANSACTION_FLAG_NONE = "none" +TRANSACTION_FLAG_ONLY_DOWNLOAD = "only-download" +TRANSACTION_FLAG_ONLY_TRUSTED = "only-trusted" +TRANSACTION_FLAG_SIMULATE = "simulate" +UPDATE_STATE_STABLE = "stable" +UPDATE_STATE_TESTING = "testing" +UPDATE_STATE_UNKNOWN = "unknown" +UPDATE_STATE_UNSTABLE = "unstable" +UPGRADE_KIND_COMPLETE = "complete" +UPGRADE_KIND_DEFAULT = "default" +UPGRADE_KIND_MINIMAL = "minimal" +UPGRADE_KIND_UNKNOWN = "unknown" diff --git a/aptdaemon/pkutils.py b/aptdaemon/pkutils.py new file mode 100644 index 0000000..2413ca4 --- /dev/null +++ b/aptdaemon/pkutils.py @@ -0,0 +1,46 @@ +# !/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Provides helper functions for the PackageKit layer + +Copyright (C) 2007 Ali Sabil <ali.sabil@gmail.com> +Copyright (C) 2007 Tom Parker <palfrey@tevp.net> +Copyright (C) 2008-2013 Sebastian Heinlein <glatzor@ubuntu.com> + +Licensed under the GNU General Public License Version 2 + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + + +def bitfield_summarize(*enums): + """Return the bitfield with the given PackageKit enums.""" + field = 0 + for enum in enums: + field |= 2 ** int(enum) + return field + + +def bitfield_add(field, enum): + """Add a PackageKit enum to a given field""" + field |= 2 ** int(enum) + return field + + +def bitfield_remove(field, enum): + """Remove a PackageKit enum to a given field""" + field = field ^ 2 ** int(enum) + return field + + +def bitfield_contains(field, enum): + """Return True if a bitfield contains the given PackageKit enum""" + return field & 2 ** int(enum) + + +# vim: ts=4 et sts=4 diff --git a/aptdaemon/policykit1.py b/aptdaemon/policykit1.py new file mode 100644 index 0000000..9a15513 --- /dev/null +++ b/aptdaemon/policykit1.py @@ -0,0 +1,175 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Provides access to PolicyKit privilege mangement using gdefer Deferreds.""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("check_authorization_by_name", "check_authorization_by_pid", + "get_pid_from_dbus_name", "get_uid_from_dbus_name", + "CHECK_AUTH_ALLOW_USER_INTERACTION", "CHECK_AUTH_NONE", + "PK_ACTION_ADD_REMOVE_VENDOR_KEY", "PK_ACTION_CANCEL_FOREIGN", + "PK_ACTION_CHANGE_REPOSITORY", + "PK_ACTION_CHANGE_CONIFG", + "PK_ACTION_GET_TRUSTED_VENDOR_KEYS", + "PK_ACTION_INSTALL_FILE", + "PK_ACTION_INSTALL_OR_REMOVE_PACKAGES", + "PK_ACTION_INSTALL_PACKAGES_FROM_NEW_REPO", + "PK_ACTION_INSTALL_PACKAGES_FROM_HIGH_TRUST_REPO", + "PK_ACTION_INSTALL_PURCHASED_PACKAGES", + "PK_ACTION_UPDATE_CACHE", "PK_ACTION_UPGRADE_PACKAGES", + "PK_ACTION_SET_PROXY", "PK_ACTION_CLEAN") + +import dbus + +from defer import Deferred, inline_callbacks, return_value +from .errors import NotAuthorizedError, AuthorizationFailed + +PK_ACTION_INSTALL_OR_REMOVE_PACKAGES = ( + "org.debian.apt.install-or-remove-packages") +PK_ACTION_INSTALL_PURCHASED_PACKAGES = ( + "org.debian.apt.install-purchased-packages") +PK_ACTION_INSTALL_PACKAGES_FROM_NEW_REPO = ( + "org.debian.apt.install-packages-from-new-repo") +PK_ACTION_INSTALL_PACKAGES_FROM_HIGH_TRUST_REPO = ( + "org.debian.apt.install-packages.high-trust-repo") +PK_ACTION_INSTALL_FILE = "org.debian.apt.install-file" +PK_ACTION_UPGRADE_PACKAGES = "org.debian.apt.upgrade-packages" +PK_ACTION_UPDATE_CACHE = "org.debian.apt.update-cache" +PK_ACTION_CANCEL_FOREIGN = "org.debian.apt.cancel-foreign" +PK_ACTION_GET_TRUSTED_VENDOR_KEYS = "org.debian.apt.get-trusted-vendor-keys" +PK_ACTION_CHANGE_REPOSITORY = "org.debian.apt.change-repository" +PK_ACTION_CHANGE_CONFIG = "org.debian.apt.change-config" +PK_ACTION_SET_PROXY = "org.debian.apt.set-proxy" +PK_ACTION_CLEAN = "org.debian.apt.clean" + +CHECK_AUTH_NONE = 0 +CHECK_AUTH_ALLOW_USER_INTERACTION = 1 + + +def check_authorization_by_name(dbus_name, action_id, timeout=86400, bus=None, + flags=None): + """Check if the given sender is authorized for the specified action. + + If the sender is not authorized raise NotAuthorizedError. + + Keyword arguments: + dbus_name -- D-Bus name of the subject + action_id -- the PolicyKit policy name of the action + timeout -- time in seconds for the user to authenticate + bus -- the D-Bus connection (defaults to the system bus) + flags -- optional flags to control the authentication process + """ + subject = ("system-bus-name", {"name": dbus_name}) + return _check_authorization(subject, action_id, timeout, bus, flags) + + +def check_authorization_by_pid(pid, action_id, timeout=86400, bus=None, + flags=None): + """Check if the given process is authorized for the specified action. + + If the sender is not authorized raise NotAuthorizedError. + + Keyword arguments: + pid -- id of the process + action_id -- the PolicyKit policy name of the action + timeout -- time in seconds for the user to authenticate + bus -- the D-Bus connection (defaults to the system bus) + flags -- optional flags to control the authentication process + """ + subject = ("unix-process", {"pid": pid}) + return _check_authorization(subject, action_id, timeout, bus, flags) + + +def _check_authorization(subject, action_id, timeout, bus, flags=None): + def policykit_done(xxx_todo_changeme): + (authorized, challenged, auth_details) = xxx_todo_changeme + if authorized: + deferred.callback(auth_details) + elif challenged: + deferred.errback(AuthorizationFailed(subject, action_id)) + else: + deferred.errback(NotAuthorizedError(subject, action_id)) + if not bus: + bus = dbus.SystemBus() + # Set the default flags + if flags is None: + flags = CHECK_AUTH_ALLOW_USER_INTERACTION + deferred = Deferred() + pk = bus.get_object("org.freedesktop.PolicyKit1", + "/org/freedesktop/PolicyKit1/Authority") + details = {} + pk.CheckAuthorization( + subject, action_id, details, flags, "", + dbus_interface="org.freedesktop.PolicyKit1.Authority", + timeout=timeout, + reply_handler=policykit_done, + error_handler=deferred.errback) + return deferred + + +def get_pid_from_dbus_name(dbus_name, bus=None): + """Return a deferred that gets the id of process owning the given + system D-Bus name. + """ + if not bus: + bus = dbus.SystemBus() + deferred = Deferred() + bus_obj = bus.get_object("org.freedesktop.DBus", + "/org/freedesktop/DBus/Bus") + bus_obj.GetConnectionUnixProcessID(dbus_name, + dbus_interface="org.freedesktop.DBus", + reply_handler=deferred.callback, + error_handler=deferred.errback) + return deferred + + +@inline_callbacks +def get_uid_from_dbus_name(dbus_name, bus=None): + """Return a deferred that gets the uid of the user owning the given + system D-Bus name. + """ + if not bus: + bus = dbus.SystemBus() + pid = yield get_pid_from_dbus_name(dbus_name, bus) + with open("/proc/%s/status" % pid) as proc: + values = [v for v in proc.readlines() if v.startswith("Uid:")] + uid = int(values[0].split()[1]) + return_value(uid) + + +@inline_callbacks +def get_proc_info_from_dbus_name(dbus_name, bus=None): + """Return a deferred that gets the pid, the uid of the user owning the + given system D-Bus name and its command line. + """ + if not bus: + bus = dbus.SystemBus() + pid = yield get_pid_from_dbus_name(dbus_name, bus) + with open("/proc/%s/status" % pid) as proc: + lines = proc.readlines() + uid_values = [v for v in lines if v.startswith("Uid:")] + gid_values = [v for v in lines if v.startswith("Gid:")] + # instead of ", encoding='utf8'" we use the "rb"/decode() here for + # py2 compatibility + with open("/proc/%s/cmdline" % pid, "rb") as cmdline_file: + cmdline = cmdline_file.read().decode("utf-8") + uid = int(uid_values[0].split()[1]) + gid = int(gid_values[0].split()[1]) + return_value((pid, uid, gid, cmdline)) + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/progress.py b/aptdaemon/progress.py new file mode 100644 index 0000000..5312c5a --- /dev/null +++ b/aptdaemon/progress.py @@ -0,0 +1,829 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Progress handlers for APT operations""" +# Copyright (C) 2008-2009 Sebastian Heinlein <glatzor@ubuntu.com> +# +# Licensed under the GNU General Public License Version 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("DaemonAcquireProgress", "DaemonOpenProgress", + "DaemonInstallProgress", "DaemonDpkgInstallProgress", + "DaemonForkProgress", "DaemonDpkgRecoverProgress") + +import locale +import logging +import os +import platform +import re +import signal +import sys +import termios +import time +import traceback +import tty + +import apt_pkg +import apt.progress.base +import apt.debfile +from gi.repository import GLib + +from . import enums +from . import lock +from .loop import mainloop +from .utils import IsoCodes + +# Required to get translatable strings extraced by xgettext +_ = lambda s: s + +log = logging.getLogger("AptDaemon.Worker") +log_terminal = logging.getLogger("AptDaemon.Worker.Terminal") + +INSTALL_TIMEOUT = 10 * 60 + +MAP_DPKG_STAGE = {"install": enums.PKG_INSTALLING, + "configure": enums.PKG_CONFIGURING, + "remove": enums.PKG_REMOVING, + "trigproc": enums.PKG_RUNNING_TRIGGER, + "purge": enums.PKG_PURGING, + "disappear": enums.PKG_DISAPPEARING, + "upgrade": enums.PKG_UPGRADING} + +REGEX_ANSI_ESCAPE_CODE = chr(27) + "\[[;?0-9]*[A-Za-z]" + + +class DaemonOpenProgress(apt.progress.base.OpProgress): + + """Handles the progress of the cache opening.""" + + def __init__(self, transaction, begin=0, end=100, quiet=False): + """Initialize a new DaemonOpenProgress instance. + + Keyword arguments: + transaction -- corresponding transaction D-Bus object + begin -- begin of the progress range (defaults to 0) + end -- end of the progress range (defaults to 100) + quiet -- do not emit any progress information for the transaction + """ + apt.progress.base.OpProgress.__init__(self) + self._transaction = transaction + self.steps = [begin + (end - begin) * modifier + # the final 1.00 will not be used but we still + # need it here for the final pop() + for modifier in [0.25, 0.50, 0.75, 1.00, 1.00]] + self.progress_begin = float(begin) + self.progress_end = self.steps.pop(0) + self.progress = 0 + self.quiet = quiet + + def update(self, percent=None): + """Callback for progress updates. + + Keyword argument: + percent - current progress in percent + """ + # python-apt 0.8 does not include "percent" anymore in the call + percent = percent or self.percent + if percent < 101: + progress = int(self.progress_begin + (percent / 100) * + (self.progress_end - self.progress_begin)) + if self.progress == progress: + return + else: + progress = 101 + self.progress = progress + if not self.quiet: + self._transaction.progress = progress + + def done(self): + """Callback after completing a step. + + Sets the progress range to the next interval.""" + # ensure that progress is updated + self.progress = self.progress_end + # switch to new progress_{begin, end} + self.progress_begin = self.progress_end + try: + self.progress_end = self.steps.pop(0) + except: + log.warning("An additional step to open the cache is required") + + +class DaemonAcquireProgress(apt.progress.base.AcquireProgress): + ''' + Handle the package download process + ''' + def __init__(self, transaction, begin=0, end=100): + apt.progress.base.AcquireProgress.__init__(self) + self.transaction = transaction + self.progress_end = end + self.progress_begin = begin + self.progress = 0 + + def _emit_acquire_item(self, item, total_size=0, current_size=0): + if item.owner.status == apt_pkg.AcquireItem.STAT_DONE: + status = enums.DOWNLOAD_DONE + # Workaround for a bug in python-apt, see lp: #581886 + current_size = item.owner.filesize + elif item.owner.status == apt_pkg.AcquireItem.STAT_AUTH_ERROR: + status = enums.DOWNLOAD_AUTH_ERROR + elif item.owner.status == apt_pkg.AcquireItem.STAT_FETCHING: + status = enums.DOWNLOAD_FETCHING + elif item.owner.status == apt_pkg.AcquireItem.STAT_ERROR: + status = enums.DOWNLOAD_ERROR + elif item.owner.status == apt_pkg.AcquireItem.STAT_IDLE: + status = enums.DOWNLOAD_IDLE + else: + # Workaround: The StatTransientNetworkError status isn't mapped + # by python-apt, see LP #602578 + status = enums.DOWNLOAD_NETWORK_ERROR + if (item.owner.status != apt_pkg.AcquireItem.STAT_DONE and + item.owner.error_text): + msg = item.owner.error_text + elif item.owner.mode: + msg = item.owner.mode + else: + msg = "" + self.transaction.progress_download = ( + item.uri, status, item.shortdesc, + total_size | item.owner.filesize, + current_size | item.owner.partialsize, + msg) + + def _emit_status_details(self, items): + """Emit the transaction status details.""" + names = set() + for item in items: + if item.owner.id: + names.add(item.owner.id) + else: + names.add(item.shortdesc) + if names: + # TRANSLATORS: %s is a list of package names + msg = self.transaction.ngettext("Downloading %(files)s", + "Downloading %(files)s", + len(items)) % {"files": + " ".join(names)} + self.transaction.status_details = msg + + def done(self, item): + """Invoked when an item is successfully and completely fetched.""" + self._emit_acquire_item(item) + + def fail(self, item): + """Invoked when an item could not be fetched.""" + self._emit_acquire_item(item) + + def fetch(self, item): + """Invoked when some of the item's data is fetched.""" + self._emit_acquire_item(item) + + def ims_hit(self, item): + """Invoked when an item is confirmed to be up-to-date. + + Invoked when an item is confirmed to be up-to-date. For instance, + when an HTTP download is informed that the file on the server was + not modified. + """ + self._emit_acquire_item(item) + + def pulse(self, owner): + """Callback to update progress information""" + if self.transaction.cancelled: + return False + self.transaction.progress_details = (self.current_items, + self.total_items, + self.current_bytes, + self.total_bytes, + self.current_cps, + self.elapsed_time) + percent = (((self.current_bytes + self.current_items) * 100.0) / + float(self.total_bytes + self.total_items)) + progress = int(self.progress_begin + percent / 100 * + (self.progress_end - self.progress_begin)) + # If the progress runs backwards emit an illegal progress value + # e.g. during cache updates. + if self.progress > progress: + self.transaction.progress = 101 + else: + self.transaction.progress = progress + self.progress = progress + # Show all currently downloaded files + items = [] + for worker in owner.workers: + if not worker.current_item: + continue + self._emit_acquire_item(worker.current_item, + worker.total_size, + worker.current_size) + items.append(worker.current_item) + self._emit_status_details(items) + while GLib.main_context_default().pending(): + GLib.main_context_default().iteration() + return True + + def start(self): + """Callback at the beginning of the operation""" + self.transaction.status = enums.STATUS_DOWNLOADING + self.transaction.cancellable = True + + def stop(self): + """Callback at the end of the operation""" + self.transaction.progress_details = (0, 0, 0, 0, 0.0, 0) + self.transaction.progress = self.progress_end + self.transaction.cancellable = False + + def media_change(self, medium, drive): + """Callback for media changes""" + self.transaction.required_medium = medium, drive + self.transaction.paused = True + self.transaction.status = enums.STATUS_WAITING_MEDIUM + while self.transaction.paused: + GLib.main_context_default().iteration() + self.transaction.status = enums.STATUS_DOWNLOADING + if self.transaction.cancelled: + return False + return True + + +class DaemonAcquireRepoProgress(DaemonAcquireProgress): + + """Handle the repository information download""" + + def __init__(self, transaction, begin=0, end=100): + DaemonAcquireProgress.__init__(self, transaction, begin, end) + self.languages = IsoCodes("iso_639", tag="iso_639_1_code", + fallback_tag="iso_639_2T_code") + self.regions = IsoCodes("iso_3166", "alpha_2_code") + self.progress = 101 + + def start(self): + """Callback at the beginning of the operation""" + self.transaction.status = enums.STATUS_DOWNLOADING_REPO + self.transaction.cancellable = True + + def _emit_status_details(self, items): + """Emit the transaction status details.""" + repos = set() + for item in items: + # We are only interested in the hostname currently + try: + repos.add(item.description.split()[0].split("://")[-1]) + except IndexError: + # TRANSLATORS: the string is used as a fallback if we cannot + # get the URI of a local repository + repos.add(self.transaction.gettext("local repository")) + if repos: + # TRANSLATORS: %s is a list of repository names + msg = self.transaction.ngettext("Downloading from %s", + "Downloading from %s", + len(repos)) % " ".join(repos) + self.transaction.status_details = msg + + def _emit_acquire_item(self, item, total_size=0, current_size=0): + if item.owner.status == apt_pkg.AcquireItem.STAT_DONE: + status = enums.DOWNLOAD_DONE + # Workaround for a bug in python-apt, see lp: #581886 + current_size = item.owner.filesize + elif item.owner.status == apt_pkg.AcquireItem.STAT_AUTH_ERROR: + status = enums.DOWNLOAD_AUTH_ERROR + elif item.owner.status == apt_pkg.AcquireItem.STAT_FETCHING: + status = enums.DOWNLOAD_FETCHING + elif item.owner.status == apt_pkg.AcquireItem.STAT_ERROR: + status = enums.DOWNLOAD_ERROR + elif item.owner.status == apt_pkg.AcquireItem.STAT_IDLE: + status = enums.DOWNLOAD_IDLE + else: + # Workaround: The StatTransientNetworkError status isn't mapped + # by python-apt, see LP #602578 + status = enums.DOWNLOAD_NETWORK_ERROR + if (item.owner.status != apt_pkg.AcquireItem.STAT_DONE and + item.owner.error_text): + msg = item.owner.error_text + elif item.owner.mode: + msg = item.owner.mode + else: + msg = "" + # Get a better description than e.g. Packages or Sources + host, dist = item.description.split()[0:2] + try: + host = host.split("://")[1] + except IndexError: + # TRANSLATORS: the string is used as a fallback if we cannot + # get the URI of a local repository + desc = self.transaction.gettext("local repository") + repo = "%s %s" % (host, dist) + if item.shortdesc == "InRelease": + # TRANSLATORS: repo is the name of a repository + desc = self.transaction.gettext("Structure of %s") % repo + elif item.shortdesc == "Release": + # TRANSLATORS: repo is the name of a repository + desc = self.transaction.gettext("Description of %s") % repo + elif item.shortdesc == "Release.gpg": + # TRANSLATORS: repo is the name of a repository + desc = self.transaction.gettext("Description signature " + "of %s") % repo + elif item.shortdesc.startswith("Packages"): + # TRANSLATORS: repo is the name of a repository + desc = self.transaction.gettext( + "Available packages from %s") % repo + elif item.shortdesc.startswith("Sources"): + # TRANSLATORS: repo is the name of a repository + desc = self.transaction.gettext( + "Available sources from %s") % repo + elif item.shortdesc == "TranslationIndex": + # TRANSLATORS: repo is the name of a repository + desc = self.transaction.gettext("Available translations from " + "%s") % repo + elif item.shortdesc.startswith("Translation-"): + lang_code = item.shortdesc.split("-", 1)[-1] + try: + lang_code, region_code = lang_code.split("_") + except ValueError: + region_code = None + lang = self.languages.get_localised_name(lang_code, + self.transaction.locale) + region = self.regions.get_localised_name(region_code, + self.transaction.locale) + if lang and region: + # TRANSLATORS: The first %s is the name of a language. The + # second one the name of the region/country. Th + # third %s is the name of the repository + desc = self.transaction.gettext( + "Translations for %s (%s) from %s") % (lang, region, repo) + elif lang: + # TRANSLATORS: %s is the name of a language. The second one is + # the name of the repository + desc = self.transaction.gettext("Translations for %s from " + "%s") % (lang, repo) + else: + # TRANSLATORS: %s is the code of a language, e.g. ru_RU. + # The second one is the name of the repository + desc = self.transaction.gettext("Translations (%s) from " + "%s") % (lang_code, repo) + else: + desc = item.shortdesc + self.transaction.progress_download = ( + item.uri, status, desc, total_size | item.owner.filesize, + current_size | item.owner.partialsize, msg) + + +class DaemonForkProgress(object): + + """Forks and executes a given method in the child process while + monitoring the output and return state. + + During the run() call the mainloop will be iterated. + + Furthermore a status file descriptor is available to communicate + with the child process. + """ + + def __init__(self, transaction, begin=50, end=100): + self.transaction = transaction + self.status = "" + self.progress = 0 + self.progress_begin = begin + self.progress_end = end + self._child_exit = -1 + self.last_activity = 0 + self.child_pid = 0 + self.status_parent_fd, self.status_child_fd = os.pipe() + if hasattr(os, "set_inheritable"): + os.set_inheritable(self.status_parent_fd, True) + os.set_inheritable(self.status_child_fd, True) + self.output = "" + self._line_buffer = "" + + def __enter__(self): + self.start_update() + return self + + def __exit__(self, etype, evalue, etb): + self.finish_update() + + def start_update(self): + log.debug("Start update") + self.transaction.status = enums.STATUS_COMMITTING + self.transaction.term_attached = True + self.last_activity = time.time() + self.start_time = time.time() + + def finish_update(self): + """Callback at the end of the operation""" + self.transaction.term_attached = False + + def _child(self, method, *args): + """Call the given method or function with the + corrsponding arguments in the child process. + + This method should be replace in subclasses. + """ + method(*args) + time.sleep(0.5) + os._exit(0) + + def run(self, *args, **kwargs): + """Setup monitoring, fork and call the self._child() method in the + child process with the given arguments. + """ + log.debug("Run") + terminal_fd = None + if self.transaction.terminal: + try: + # Save the settings of the transaction terminal and set to + # raw mode + terminal_fd = os.open(self.transaction.terminal, + os.O_RDWR | os.O_NOCTTY | os.O_NONBLOCK) + terminal_attr = termios.tcgetattr(terminal_fd) + tty.setraw(terminal_fd, termios.TCSANOW) + except (OSError, termios.error): + # Switch to non-interactive + self.transaction.terminal = "" + pid = self._fork() + if pid == 0: + os.close(self.status_parent_fd) + try: + self._setup_child() + self._child(*args, **kwargs) + except Exception: + traceback.print_exc() + finally: + # Give the parent process enough time to catch the output + time.sleep(1) + # Abort the subprocess immediatelly on any unhandled + # failure - otherwise the atexit methods would + # be called, e.g. the frozen status decorator + os._exit(apt_pkg.PackageManager.RESULT_FAILED) + else: + self.child_pid = pid + os.close(self.status_child_fd) + log.debug("Child pid: %s", pid) + watchers = [] + flags = GLib.IO_IN | GLib.IO_ERR | GLib.IO_HUP + if self.transaction.terminal: + # Setup copying of i/o between the controlling terminals + watchers.append(GLib.io_add_watch(terminal_fd, + GLib.PRIORITY_HIGH_IDLE, + flags, + self._copy_io)) + watchers.append(GLib.io_add_watch(self.master_fd, + GLib.PRIORITY_HIGH_IDLE, flags, + self._copy_io_master, terminal_fd)) + # Monitor the child process + watchers.append( + GLib.child_watch_add(GLib.PRIORITY_HIGH_IDLE, + pid, self._on_child_exit)) + # Watch for status updates + watchers.append(GLib.io_add_watch(self.status_parent_fd, + GLib.PRIORITY_HIGH_IDLE, + GLib.IO_IN, + self._on_status_update)) + while self._child_exit == -1: + GLib.main_context_default().iteration() + for id in watchers: + GLib.source_remove(id) + # Restore the settings of the transaction terminal + if terminal_fd: + try: + termios.tcsetattr(terminal_fd, termios.TCSADRAIN, + terminal_attr) + except termios.error: + pass + # Make sure all file descriptors are closed + for fd in [self.master_fd, self.status_parent_fd, terminal_fd]: + try: + os.close(fd) + except (OSError, TypeError): + pass + return os.WEXITSTATUS(self._child_exit) + + def _on_child_exit(self, pid, condition): + log.debug("Child exited: %s", condition) + self._child_exit = condition + return False + + def _on_status_update(self, source, condition): + """Callback for changes on the status file descriptor. + + The method has to return True to keep the monitoring alive. If + it returns False the monitoring will stop. + + Replace this method in your subclass if you use the status fd. + """ + return False + + def _fork(self): + """Fork and create a master/slave pty pair by which the forked process + can be controlled. + """ + pid, self.master_fd = os.forkpty() + return pid + + def _setup_child(self): + """Setup the environment of the child process.""" + def interrupt_handler(signum, frame): + # Exit the child immediately if we receive the interrupt + # signal or a Ctrl+C - otherwise the atexit methods would + # be called, e.g. the frozen status decorator + os._exit(apt_pkg.PackageManager.RESULT_FAILED) + signal.signal(signal.SIGINT, interrupt_handler) + # Make sure that exceptions of the child are not caught by apport + sys.excepthook = sys.__excepthook__ + + mainloop.quit() + # force terminal messages in dpkg to be untranslated, the + # status-fd or debconf prompts will not be affected + os.environ["DPKG_UNTRANSLATED_MESSAGES"] = "1" + # We also want untranslated status messages from apt + locale.setlocale(locale.LC_ALL, "C") + # Switch to the language of the user + if self.transaction.locale: + os.putenv("LANG", self.transaction.locale) + # Either connect to the controllong terminal or switch to + # non-interactive mode + if not self.transaction.terminal: + # FIXME: we should check for "mail" or "gnome" here + # and not unset in this case + os.putenv("APT_LISTCHANGES_FRONTEND", "none") + os.putenv("APT_LISTBUGS_FRONTEND", "none") + else: + os.putenv("TERM", "linux") + # Run debconf through a proxy if available + if self.transaction.debconf: + os.putenv("DEBCONF_PIPE", self.transaction.debconf) + os.putenv("DEBIAN_FRONTEND", "passthrough") + if log.level == logging.DEBUG: + os.putenv("DEBCONF_DEBUG", ".") + elif not self.transaction.terminal: + os.putenv("DEBIAN_FRONTEND", "noninteractive") + # Proxy configuration + if self.transaction.http_proxy: + apt_pkg.config.set("Acquire::http::Proxy", + self.transaction.http_proxy) + # Mark changes as being make by aptdaemon + cmd = "aptdaemon role='%s' sender='%s'" % (self.transaction.role, + self.transaction.sender) + apt_pkg.config.set("CommandLine::AsString", cmd) + + def _copy_io_master(self, source, condition, target): + if condition == GLib.IO_IN: + self.last_activity = time.time() + try: + char_byte = os.read(source, 1) + except OSError: + log.debug("Faild to read from master") + return True + # Write all the output from dpkg to a log + char = char_byte.decode("UTF-8", "ignore") + if char == "\n": + # Skip ANSI characters from the console output + line = re.sub(REGEX_ANSI_ESCAPE_CODE, "", self._line_buffer) + if line: + log_terminal.debug(line) + self.output += line + "\n" + self._line_buffer = "" + else: + self._line_buffer += char + if target: + try: + os.write(target, char_byte) + except OSError: + log.debug("Failed to write to controlling terminal") + return True + try: + os.close(source) + except OSError: + # Could already be closed by the clean up in run() + pass + return False + + def _copy_io(self, source, condition): + if condition == GLib.IO_IN: + try: + char = os.read(source, 1) + os.write(self.master_fd, char) + except OSError: + pass + else: + # Detect config file prompt answers on the console + if (self.transaction.paused and + self.transaction.config_file_conflict): + self.transaction.config_file_conflict_resolution = None + self.transaction.paused = False + return True + os.close(source) + return False + + +class DaemonInstallProgress(DaemonForkProgress): + + """Progress to execute APT package operations in a child process.""" + + def start_update(self): + DaemonForkProgress.start_update(self) + lock.status_lock.release() + + def finish_update(self): + """Callback at the end of the operation""" + DaemonForkProgress.finish_update(self) + lock.wait_for_lock(self.transaction, lock.status_lock) + + def _child(self, pm): + try: + res = pm.do_install(self.status_child_fd) + except: + os._exit(apt_pkg.PackageManager.RESULT_FAILED) + else: + os._exit(res) + + def _on_status_update(self, source, condition): + """Parse messages from APT on the status fd.""" + log.debug("UpdateInterface") + status_msg = "" + try: + while not status_msg.endswith("\n"): + self.last_activity = time.time() + status_msg += os.read(source, 1).decode("UTF-8", "ignore") + except: + return False + try: + (status, pkg, percent, message_raw) = status_msg.split(":", 3) + except ValueError: + # silently ignore lines that can't be parsed + return True + message = message_raw.strip() + # print "percent: %s %s" % (pkg, float(percent)/100.0) + if status == "pmerror": + self._error(pkg, message) + elif status == "pmconffile": + # we get a string like this: + # 'current-conffile' 'new-conffile' useredited distedited + match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", message_raw) + if match: + new, old = match.group(1), match.group(2) + self._conffile(new, old) + elif status == "pmstatus": + if message.startswith("Installing"): + status_enum = enums.PKG_INSTALLING + elif message.startswith("Installed"): + status_enum = enums.PKG_INSTALLED + elif message.startswith("Configuring"): + status_enum = enums.PKG_CONFIGURING + elif message.startswith("Preparing to configure"): + status_enum = enums.PKG_PREPARING_CONFIGURE + elif message.startswith("Preparing for removal of"): + status_enum = enums.PKG_PREPARING_REMOVE + elif message.startswith("Removing"): + status_enum = enums.PKG_REMOVING + elif message.startswith("Removed"): + status_enum = enums.PKG_REMOVED + elif message.startswith("Preparing to completely remove"): + status_enum = enums.PKG_PREPARING_PURGE + elif message.startswith("Completely removing"): + status_enum = enums.PKG_PURGING + elif message.startswith("Completely removed"): + status_enum = enums.PKG_PURGED + elif message.startswith("Unpacking"): + status_enum = enums.PKG_UNPACKING + elif message.startswith("Preparing"): + status_enum = enums.PKG_PREPARING_INSTALL + elif message.startswith("Noting disappearance of"): + status_enum = enums.PKG_DISAPPEARING + elif message.startswith("Running"): + status_enum = enums.PKG_RUNNING_TRIGGER + else: + status_enum = enums.PKG_UNKNOWN + self._status_changed(pkg, float(percent), status_enum) + # catch a time out by sending crtl+c + if (self.last_activity + INSTALL_TIMEOUT < time.time() and + self.child_pid): + log.critical("Killing child since timeout of %s s", + INSTALL_TIMEOUT) + os.kill(self.child_pid, 15) + return True + + def _status_changed(self, pkg, percent, status_enum): + """Callback to update status information""" + log.debug("APT status: %s, %s, %s", pkg, percent, status_enum) + progress = self.progress_begin + percent / 100 * (self.progress_end - + self.progress_begin) + if self.progress < progress: + self.transaction.progress = int(progress) + self.progress = progress + # We use untranslated messages from apt. + # So convert them to an enum to allow translations, see LP #641262 + # The strings are taken from apt-pkg/deb/dpkgpm.cc + desc = enums.get_package_status_from_enum(status_enum) + msg = self.transaction.gettext(desc) % pkg + self.transaction.status_details = msg + self.transaction.progress_package = (pkg, status_enum) + + def _conffile(self, current, new): + """Callback for a config file conflict""" + log.warning("Config file prompt: '%s' (%s)" % (current, new)) + self.transaction.config_file_conflict = (current, new) + self.transaction.paused = True + self.transaction.status = enums.STATUS_WAITING_CONFIG_FILE_PROMPT + while self.transaction.paused: + GLib.main_context_default().iteration() + log.debug("Sending config file answer: %s", + self.transaction.config_file_conflict_resolution) + if self.transaction.config_file_conflict_resolution == "replace": + os.write(self.master_fd, b"y\n") + elif self.transaction.config_file_conflict_resolution == "keep": + os.write(self.master_fd, b"n\n") + self.transaction.config_file_conflict_resolution = None + self.transaction.config_file_conflict = None + self.transaction.status = enums.STATUS_COMMITTING + return True + + def _error(self, pkg, msg): + """Callback for an error""" + log.critical("%s: %s" % (pkg, msg)) + + +class DaemonDpkgInstallProgress(DaemonInstallProgress): + + """Progress handler for a local Debian package installation.""" + + def __init__(self, transaction, begin=101, end=101): + DaemonInstallProgress.__init__(self, transaction, begin, end) + + def _child(self, debfile): + args = [apt_pkg.config["Dir::Bin::DPkg"], "--status-fd", + str(self.status_child_fd)] + args.extend(apt_pkg.config.value_list("DPkg::Options")) + if not self.transaction.terminal: + args.extend(["--force-confdef", "--force-confold"]) + args.extend(["-i", debfile]) + os.execlp(apt_pkg.config["Dir::Bin::DPkg"], *args) + + def _on_status_update(self, source, condition): + log.debug("UpdateInterface") + status_raw = "" + try: + while not status_raw.endswith("\n"): + status_raw += os.read(source, 1).decode("UTF-8", "ignore") + except: + return False + try: + status = [s.strip() for s in status_raw.split(":", 3)] + except ValueError: + # silently ignore lines that can't be parsed + return True + # Parse the status message. It can be of the following types: + # - "status: PACKAGE: STATUS" + # - "status: PACKAGE: error: MESSAGE" + # - "status: FILE: conffile: 'OLD' 'NEW' useredited distedited" + # - "processing: STAGE: PACKAGE" with STAGE is one of upgrade, + # install, configure, trigproc, remove, purge + if status[0] == "status": + # FIXME: Handle STATUS + if status[2] == "error": + self._error(status[1], status[3]) + elif status[2] == "conffile": + match = re.match("\s*\'(.*)\'\s*\'(.*)\'.*", status[3]) + if match: + new, old = match.group(1), match.group(2) + self._conffile(new, old) + elif status[0] == "processing": + try: + status_enum = MAP_DPKG_STAGE[status[1]] + except KeyError: + status_enum = enums.PKG_UNKONWN + self._status_changed(status[2], 101, status_enum) + return True + + +class DaemonDpkgRecoverProgress(DaemonDpkgInstallProgress): + + """Progress handler for dpkg --confiure -a call.""" + + def _child(self): + args = [apt_pkg.config["Dir::Bin::Dpkg"], "--status-fd", + str(self.status_child_fd), "--configure", "-a"] + args.extend(apt_pkg.config.value_list("Dpkg::Options")) + if not self.transaction.terminal: + args.extend(["--force-confdef", "--force-confold"]) + os.execlp(apt_pkg.config["Dir::Bin::DPkg"], *args) + + +class DaemonDpkgReconfigureProgress(DaemonDpkgInstallProgress): + + """Progress handler for dpkg-reconfigure call.""" + + def _child(self, packages, priority, ): + args = ["/usr/sbin/dpkg-reconfigure"] + if priority != "default": + args.extend(["--priority", priority]) + args.extend(packages) + os.execlp("/usr/sbin/dpkg-reconfigure", *args) + + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/test.py b/aptdaemon/test.py new file mode 100644 index 0000000..60d3587 --- /dev/null +++ b/aptdaemon/test.py @@ -0,0 +1,312 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Small helpers for the test suite.""" +# Copyright (C) 2011 Sebastian Heinlein <devel@glatzor.de> +# +# Licensed under the GNU General Public License Version 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. +# Licensed under the GNU General Public License Version 2 + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" +__all__ = ("get_tests_dir", "Chroot", "AptDaemonTestCase") + +import inspect +import os +import shutil +import subprocess +import sys +import time +import tempfile +import unittest + +if sys.version_info.major > 2: + from http.server import HTTPServer + from http.server import SimpleHTTPRequestHandler as HTTPRequestHandler +else: + from BaseHTTPServer import HTTPServer + from SimpleHTTPServer import SimpleHTTPRequestHandler as HTTPRequestHandler + +import apt_pkg +import apt.auth + +PY3K = sys.version_info.major > 2 + + +class MockQueue(object): + + """A fake TransactionQueue which only provides a limbo attribute.""" + + def __init__(self): + self.limbo = {} + + +class Chroot(object): + + """Provides a chroot which can be used by APT.""" + + def __init__(self, prefix="tmp"): + self.path = tempfile.mkdtemp(prefix) + + def setup(self): + """Setup the chroot and modify the apt configuration.""" + for subdir in ["alternatives", "info", "parts", "updates", "triggers"]: + path = os.path.join(self.path, "var", "lib", "dpkg", subdir) + os.makedirs(path) + for fname in ["status", "available"]: + with open(os.path.join(self.path, "var", "lib", "dpkg", fname), + "w"): + pass + os.makedirs(os.path.join(self.path, "var/cache/apt/archives/partial")) + os.makedirs(os.path.join(self.path, "var/lib/apt/lists")) + os.makedirs(os.path.join(self.path, "var/lib/apt/lists/partial")) + os.makedirs(os.path.join(self.path, "etc/apt/apt.conf.d")) + os.makedirs(os.path.join(self.path, "etc/apt/sources.list.d")) + os.makedirs(os.path.join(self.path, "etc/apt/preferences.d")) + os.makedirs(os.path.join(self.path, "etc/apt/trusted.gpg.d")) + os.makedirs(os.path.join(self.path, "var/log")) + os.makedirs(os.path.join(self.path, "media")) + + # Make apt use the new chroot + dpkg_wrapper = os.path.join(get_tests_dir(), "dpkg-wrapper.sh") + apt_key_wrapper = os.path.join(get_tests_dir(), "fakeroot-apt-key") + config_path = os.path.join(self.path, "etc/apt/apt.conf.d/10chroot") + with open(config_path, "w") as cnf: + cnf.write("Dir::Bin::DPkg %s;" % dpkg_wrapper) + cnf.write("Dir::Bin::Apt-Key %s;" % apt_key_wrapper) + apt_pkg.read_config_file(apt_pkg.config, config_path) + apt_pkg.init_system() + apt_pkg.config["Dir"] = self.path + + def remove(self): + """Remove the files of the chroot.""" + apt_pkg.config.clear("Dir") + apt_pkg.config.clear("Dir::State::Status") + apt_pkg.init() + shutil.rmtree(self.path) + + def add_trusted_key(self): + """Add glatzor's key to the trusted ones.""" + apt.auth.add_key_from_file(os.path.join(get_tests_dir(), + "repo/glatzor.gpg")) + + def install_debfile(self, path, force_depends=False): + """Install a package file into the chroot.""" + cmd_list = ["fakeroot", "dpkg", "--root", self.path, + "--log=%s/var/log/dpkg.log" % self.path] + if force_depends: + cmd_list.append("--force-depends") + cmd_list.extend(["--install", path]) + cmd = subprocess.Popen(cmd_list, + env={"PATH": "/sbin:/bin:/usr/bin:/usr/sbin"}) + cmd.communicate() + + def add_test_repository(self, copy_list=True, copy_sig=True): + """Add the test repository to the to the chroot.""" + return self.add_repository(os.path.join(get_tests_dir(), "repo"), + copy_list, copy_sig) + + def add_cdrom_repository(self): + """Emulate a repository on removable device.""" + # Create the content of a fake cdrom + media_path = os.path.join(self.path, "tmp/fake-cdrom") + # The cdom gets identified by the info file + os.makedirs(os.path.join(media_path, ".disk")) + with open(os.path.join(media_path, ".disk/info"), "w") as info: + info.write("This is a fake CDROM") + # Copy the test repository "on" the cdrom + shutil.copytree(os.path.join(get_tests_dir(), "repo"), + os.path.join(media_path, "repo")) + + # Call apt-cdrom add + mount_point = self.mount_cdrom() + os.system("apt-cdrom add -m -d %s " + "-o 'Debug::Acquire::cdrom'=True " + "-o 'Acquire::cdrom::AutoDetect'=False " + "-o 'Dir'=%s" % (mount_point, self.path)) + self.unmount_cdrom() + + config_path = os.path.join(self.path, "etc/apt/apt.conf.d/11cdrom") + with open(config_path, "w") as cnf: + cnf.write('Debug::Acquire::cdrom True;\n' + 'Acquire::cdrom::AutoDetect False;\n' + 'Acquire::cdrom::NoMount True;\n' + 'Acquire::cdrom::mount "%s";' % mount_point) + + def mount_cdrom(self): + """Copy the repo information to the CDROM mount point.""" + mount_point = os.path.join(self.path, "media/cdrom") + os.symlink(os.path.join(self.path, "tmp/fake-cdrom"), mount_point) + return mount_point + + def unmount_cdrom(self): + """Remove all files from the mount point.""" + os.unlink(os.path.join(self.path, "media/cdrom")) + + def add_repository(self, path, copy_list=True, copy_sig=True): + """Add a sources.list entry to the chroot.""" + # Add a sources list + lst_path = os.path.join(self.path, "etc/apt/sources.list") + with open(lst_path, "w") as lst_file: + lst_file.write("deb file://%s ./ # Test repository" % path) + if copy_list: + filename = apt_pkg.uri_to_filename("file://%s/." % path) + shutil.copy(os.path.join(path, "Packages"), + "%s/var/lib/apt/lists/%s_Packages" % (self.path, + filename)) + if os.path.exists(os.path.join(path, "Release")): + shutil.copy(os.path.join(path, "Release"), + "%s/var/lib/apt/lists/%s_Release" % (self.path, + filename)) + if copy_sig and os.path.exists(os.path.join(path, "Release.gpg")): + shutil.copy(os.path.join(path, "Release.gpg"), + "%s/var/lib/apt/lists/%s_Release.gpg" % (self.path, + filename)) + + +class AptDaemonTestCase(unittest.TestCase): + + @classmethod + def setupClass(cls): + # Start with a clean APT configuration to remove changes + # of previous tests + for key in set([key.split("::")[0] for key in apt_pkg.config.keys()]): + apt_pkg.config.clear(key) + apt_pkg.init_config() + + def start_fake_polkitd(self, options="all"): + """Start a fake PolicyKit daemon. + + :param allowed_actions: comma separated list of allowed actions. + Defaults to all + """ + try: + env = os.environ.copy() + env["DBUS_SYSTEM_BUS_ADDRESS"] = self.dbus_address + except AttributeError: + env = None + dir = get_tests_dir() + proc = subprocess.Popen([os.path.join(dir, "fake-polkitd.py"), + "--allowed-actions", options], + env=env) + self.addCleanup(self._kill_process, proc) + return proc + + def start_session_aptd(self, chroot="/", debug=True): + """Start an aptdaemon which listens on the session D-Bus. + + :param chroot: path to the chroot + """ + env = os.environ.copy() + try: + env["DBUS_SYSTEM_BUS_ADDRESS"] = self.dbus_address + except AttributeError: + pass + try: + env.pop("http_proxy") # Unset a local proxy, see LP #1050799 + except KeyError: + pass + dir = get_tests_dir() + if dir == "/usr/share/aptdaemon/tests": + path = "/usr/sbin/aptd" + else: + path = os.path.join(dir, "../aptd") + cmd = ["python3", path, "--disable-plugins", + "--chroot", chroot] + if debug: + cmd.append("--debug") + proc = subprocess.Popen(cmd, env=env) + self.addCleanup(self._kill_process, proc) + return proc + + def start_dbus_daemon(self): + """Start a private D-Bus daemon and return its process and address.""" + proc, dbus_address = start_dbus_daemon() + self.addCleanup(self._kill_process, proc) + self.dbus_address = dbus_address + + def start_keyserver(self, filename=None): + """Start a fake keyserver on hkp://localhost:19191 + + Keyword arguments: + filename -- Optional path to a GnuPG pulic key file which should + be returned by lookups. By default the key of the test repo + is used. + """ + dir = tempfile.mkdtemp(prefix="keyserver-test-") + self.addCleanup(shutil.rmtree, dir) + os.mkdir(os.path.join(dir, "pks")) + if filename is None: + filename = os.path.join(get_tests_dir(), "repo/glatzor.gpg") + shutil.copy(filename, os.path.join(dir, "pks", "lookup")) + + pid = os.fork() + if pid == 0: + # quiesce server log + os.dup2(os.open('/dev/null', os.O_WRONLY), sys.stderr.fileno()) + os.chdir(dir) + httpd = HTTPServer(('localhost', 19191), HTTPRequestHandler) + httpd.serve_forever() + os._exit(0) + else: + self.addCleanup(self._kill_pid, pid) + + # wait a bit until server is ready + time.sleep(0.5) + + def _kill_pid(self, pid): + os.kill(pid, 15) + os.wait() + + def _kill_process(self, proc): + proc.kill() + proc.wait() + + +def get_tests_dir(): + """Return the absolute path to the tests directory.""" + # Try to detect a relative tests dir if we are running from the source + # directory + try: + path = inspect.getsourcefile(sys.modules["aptdaemon.test"]) + except KeyError: + path = inspect.getsourcefile(inspect.currentframe()) + path = os.path.realpath(path) + relative_path = os.path.join(os.path.dirname(path), "../tests") + if os.path.exists(os.path.join(relative_path, "repo/Packages")): + return os.path.normpath(relative_path) + # Fallback to an absolute path + elif os.path.exists("/usr/share/aptdaemon/tests/repo/Packages"): + return "/usr/share/aptdaemon/tests" + else: + raise Exception("Could not find tests direcotry") + + +def start_dbus_daemon(): + """Start a private D-Bus daemon and return its process and address.""" + config_path = os.path.join(get_tests_dir(), "dbus.conf") + proc = subprocess.Popen(["dbus-daemon", "--nofork", + "--print-address", "--config-file", + config_path], + stdout=subprocess.PIPE) + output = proc.stdout.readline().strip() + if PY3K: + dbus_address = output.decode() + else: + dbus_address = output + return proc, dbus_address + + +# vim: ts=4 et sts=4 diff --git a/aptdaemon/utils.py b/aptdaemon/utils.py new file mode 100644 index 0000000..d7da9ce --- /dev/null +++ b/aptdaemon/utils.py @@ -0,0 +1,131 @@ +"""Module with little helper functions and classes: + +deprecated - decorator to emit a warning if a depreacted function is used +""" +# Copyright (C) 2008-2009 Sebastian Heinlein <sevel@glatzor.de> +# +# Licensed under the GNU General Public License Version 2 +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program; if not, write to the Free Software +# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("deprecated", "IsoCodes") + +import os +import sys +import contextlib +import gettext +import functools +import warnings +from xml.etree import ElementTree + +if sys.version >= '3': + _gettext_method = "gettext" +else: + _gettext_method = "ugettext" + + +def deprecated(func): + """This is a decorator which can be used to mark functions + as deprecated. It will result in a warning being emitted + when the function is used. + + Taken from http://wiki.python.org/moin/PythonDecoratorLibrary + #GeneratingDeprecationWarnings + """ + @functools.wraps(func) + def new_func(*args, **kwargs): + warnings.warn_explicit( + "Call to deprecated function %(funcname)s." % { + 'funcname': func.__name__, + }, + category=DeprecationWarning, + filename=func.__code__.co_filename, + lineno=func.__code__.co_firstlineno + 1 + ) + return func(*args, **kwargs) + return new_func + + +@contextlib.contextmanager +def set_euid_egid(uid, gid): + # no need to drop privs + if os.getuid() != 0 and os.getgid() != 0: + yield + return + # temporary drop privs + os.setegid(gid) + old_groups = os.getgroups() + os.setgroups([gid]) + os.seteuid(uid) + try: + yield + finally: + os.seteuid(os.getuid()) + os.setegid(os.getgid()) + os.setgroups(old_groups) + + +class IsoCodes(object): + + """Provides access to the iso-codes language, script and country + database. + """ + + def __init__(self, norm, tag, fallback_tag=None): + filename = "/usr/share/xml/iso-codes/%s.xml" % norm + et = ElementTree.ElementTree(file=filename) + self._dict = {} + self.norm = norm + for element in list(et.iter()): + iso_code = element.get(tag) + if not iso_code and fallback_tag: + iso_code = element.get(fallback_tag) + if iso_code: + self._dict[iso_code] = element.get("name") + + def get_localised_name(self, value, locale): + try: + name = self._dict[value] + except KeyError: + return None + trans = gettext.translation(domain=self.norm, fallback=True, + languages=[locale]) + return getattr(trans, _gettext_method)(name) + + def get_name(self, value): + try: + return self._dict[value] + except KeyError: + return None + + +def split_package_id(package): + """Return the name, the version number and the release of the + specified package.""" + if "=" in package: + name, version = package.split("=", 1) + release = None + elif "/" in package: + name, release = package.split("/", 1) + version = None + else: + name = package + version = release = None + return name, version, release + + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/worker/__init__.py b/aptdaemon/worker/__init__.py new file mode 100644 index 0000000..3cfd97f --- /dev/null +++ b/aptdaemon/worker/__init__.py @@ -0,0 +1,334 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Provides AptWorker which processes transactions.""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("BaseWorker", "DummyWorker") + +import logging +import os +import pkg_resources +import time +import traceback + +from gi.repository import GObject, GLib + +from .. import enums +from .. import errors + +log = logging.getLogger("AptDaemon.Worker") + +# Just required to detect translatable strings. The translation is done by +# core.Transaction.gettext +_ = lambda s: s + + +class BaseWorker(GObject.GObject): + + """Worker which processes transactions from the queue.""" + + __gsignals__ = {"transaction-done": (GObject.SignalFlags.RUN_FIRST, + None, + (GObject.TYPE_PYOBJECT,)), + "transaction-simulated": (GObject.SignalFlags.RUN_FIRST, + None, + (GObject.TYPE_PYOBJECT,))} + NATIVE_ARCH = None + + def __init__(self, chroot=None, load_plugins=True): + """Initialize a new AptWorker instance.""" + GObject.GObject.__init__(self) + self.trans = None + self.last_action_timestamp = time.time() + self.chroot = chroot + # Store the the tid of the transaction whose changes are currently + # marked in the cache + self.marked_tid = None + self.plugins = {} + + @staticmethod + def _split_package_id(package): + """Return the name, the version number and the release of the + specified package.""" + if "=" in package: + name, version = package.split("=", 1) + release = None + elif "/" in package: + name, release = package.split("/", 1) + version = None + else: + name = package + version = release = None + return name, version, release + + def run(self, transaction): + """Process the given transaction in the background. + + Keyword argument: + transaction -- core.Transcation instance to run + """ + log.info("Processing transaction %s", transaction.tid) + if self.trans: + raise Exception("There is already a running transaction") + self.trans = transaction + GLib.idle_add(self._run_transaction_idle, transaction) + + def simulate(self, trans): + """Return the dependencies which will be installed by the transaction, + the content of the dpkg status file after the transaction would have + been applied, the download size and the required disk space. + + Keyword arguments: + trans -- the transaction which should be simulated + """ + log.info("Simulating trans: %s" % trans.tid) + trans.status = enums.STATUS_RESOLVING_DEP + GLib.idle_add(self._simulate_transaction_idle, trans) + + def _emit_transaction_simulated(self, trans): + """Emit the transaction-simulated signal. + + Keyword argument: + trans -- the simulated transaction + """ + log.debug("Emitting transaction-simulated: %s", trans.tid) + self.emit("transaction-simulated", trans) + + def _emit_transaction_done(self, trans): + """Emit the transaction-done signal. + + Keyword argument: + trans -- the finished transaction + """ + log.debug("Emitting transaction-done: %s", trans.tid) + self.emit("transaction-done", trans) + + def _run_transaction_idle(self, trans): + """Run the transaction""" + self.last_action_timestamp = time.time() + trans.status = enums.STATUS_RUNNING + trans.progress = 11 + try: + self._run_transaction(trans) + except errors.TransactionCancelled: + trans.exit = enums.EXIT_CANCELLED + except errors.TransactionFailed as excep: + trans.error = excep + trans.exit = enums.EXIT_FAILED + except (KeyboardInterrupt, SystemExit): + trans.exit = enums.EXIT_CANCELLED + except Exception as excep: + tbk = traceback.format_exc() + trans.error = errors.TransactionFailed(enums.ERROR_UNKNOWN, tbk) + trans.exit = enums.EXIT_FAILED + try: + from . import crash + except ImportError: + pass + else: + crash.create_report("%s: %s" % (type(excep), str(excep)), + tbk, trans) + else: + trans.exit = enums.EXIT_SUCCESS + finally: + trans.progress = 100 + self.last_action_timestamp = time.time() + tid = trans.tid[:] + self.trans = None + self.marked_tid = None + self._emit_transaction_done(trans) + log.info("Finished transaction %s", tid) + return False + + def _simulate_transaction_idle(self, trans): + try: + (trans.depends, trans.download, trans.space, + trans.unauthenticated, + trans.high_trust_packages) = self._simulate_transaction(trans) + except errors.TransactionFailed as excep: + trans.error = excep + trans.exit = enums.EXIT_FAILED + except Exception as excep: + tbk = traceback.format_exc() + trans.error = errors.TransactionFailed(enums.ERROR_UNKNOWN, tbk) + try: + from . import crash + except ImportError: + pass + else: + crash.create_report("%s: %s" % (type(excep), str(excep)), + tbk, trans) + trans.exit = enums.EXIT_FAILED + else: + trans.status = enums.STATUS_SETTING_UP + trans.simulated = time.time() + self.marked_tid = trans.tid + finally: + self._emit_transaction_simulated(trans) + self.last_action_timestamp = time.time() + return False + + def _load_plugins(self, plugins, entry_point="aptdaemon.plugins"): + """Load the plugins from setuptools' entry points.""" + plugin_dirs = [os.path.join(os.path.dirname(__file__), "plugins")] + env = pkg_resources.Environment(plugin_dirs) + dists, errors = pkg_resources.working_set.find_plugins(env) + for dist in dists: + pkg_resources.working_set.add(dist) + for name in plugins: + for ept in pkg_resources.iter_entry_points(entry_point, + name): + try: + self.plugins.setdefault(name, []).append(ept.load()) + except: + log.critical("Failed to load %s plugin: " + "%s" % (name, ept.dist)) + else: + log.debug("Loaded %s plugin: %s", name, ept.dist) + + def _simulate_transaction(self, trans): + """This method needs to be implemented by the backends.""" + depends = [[], [], [], [], [], [], []] + unauthenticated = [] + high_trust_packages = [] + skip_pkgs = [] + size = 0 + installs = reinstalls = removals = purges = upgrades = upgradables = \ + downgrades = [] + + return depends, 0, 0, [], [] + + def _run_transaction(self, trans): + """This method needs to be implemented by the backends.""" + raise errors.TransactionFailed(enums.ERROR_NOT_SUPPORTED) + + def set_config(self, option, value, filename): + """Set a configuration option. + + This method needs to be implemented by the backends.""" + raise NotImplementedError + + def get_config(self, option): + """Get a configuration option. + + This method needs to be implemented by the backends.""" + raise NotImplementedError + + def get_trusted_vendor_keys(self): + """This method needs to be implemented by the backends.""" + return [] + + def is_reboot_required(self): + """This method needs to be implemented by the backends.""" + return False + + +class DummyWorker(BaseWorker): + + """Allows to test the daemon without making any changes to the system.""" + + def run(self, transaction): + """Process the given transaction in the background. + + Keyword argument: + transaction -- core.Transcation instance to run + """ + log.info("Processing transaction %s", transaction.tid) + if self.trans: + raise Exception("There is already a running transaction") + self.trans = transaction + self.last_action_timestamp = time.time() + self.trans.status = enums.STATUS_RUNNING + self.trans.progress = 0 + self.trans.cancellable = True + GLib.timeout_add(200, self._run_transaction_idle, transaction) + + def _run_transaction_idle(self, trans): + """Run the worker""" + if trans.cancelled: + trans.exit = enums.EXIT_CANCELLED + elif trans.progress == 100: + trans.exit = enums.EXIT_SUCCESS + elif trans.role == enums.ROLE_UPDATE_CACHE: + trans.exit = enums.EXIT_FAILED + elif trans.role == enums.ROLE_UPGRADE_PACKAGES: + trans.exit = enums.EXIT_SUCCESS + elif trans.role == enums.ROLE_UPGRADE_SYSTEM: + trans.exit = enums.EXIT_CANCELLED + else: + if trans.role == enums.ROLE_INSTALL_PACKAGES: + if trans.progress == 1: + trans.status = enums.STATUS_RESOLVING_DEP + elif trans.progress == 5: + trans.status = enums.STATUS_DOWNLOADING + elif trans.progress == 50: + trans.status = enums.STATUS_COMMITTING + trans.status_details = "Heyas!" + elif trans.progress == 55: + trans.paused = True + trans.status = enums.STATUS_WAITING_CONFIG_FILE_PROMPT + trans.config_file_conflict = "/etc/fstab", "/etc/mtab" + while trans.paused: + GLib.main_context_default().iteration() + trans.config_file_conflict_resolution = None + trans.config_file_conflict = None + trans.status = enums.STATUS_COMMITTING + elif trans.progress == 60: + trans.required_medium = ("Debian Lenny 5.0 CD 1", + "USB CD-ROM") + trans.paused = True + trans.status = enums.STATUS_WAITING_MEDIUM + while trans.paused: + GLib.main_context_default().iteration() + trans.status = enums.STATUS_DOWNLOADING + elif trans.progress == 70: + trans.status_details = "Servus!" + elif trans.progress == 90: + trans.status_deatils = "" + trans.status = enums.STATUS_CLEANING_UP + elif trans.role == enums.ROLE_REMOVE_PACKAGES: + if trans.progress == 1: + trans.status = enums.STATUS_RESOLVING_DEP + elif trans.progress == 5: + trans.status = enums.STATUS_COMMITTING + trans.status_details = "Heyas!" + elif trans.progress == 50: + trans.status_details = "Hola!" + elif trans.progress == 70: + trans.status_details = "Servus!" + elif trans.progress == 90: + trans.status_deatils = "" + trans.status = enums.STATUS_CLEANING_UP + trans.progress += 1 + return True + trans.status = enums.STATUS_FINISHED + self.last_action_timestamp = time.time() + tid = self.trans.tid[:] + trans = self.trans + self.trans = None + self._emit_transaction_done(trans) + log.info("Finished transaction %s", tid) + return False + + def simulate(self, trans): + depends = [[], [], [], [], [], [], []] + return depends, 0, 0, [], [] + + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/worker/aptworker.py b/aptdaemon/worker/aptworker.py new file mode 100644 index 0000000..447534f --- /dev/null +++ b/aptdaemon/worker/aptworker.py @@ -0,0 +1,1537 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +"""Provides AptWorker which processes transactions.""" +# Copyright (C) 2008-2009 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +__all__ = ("AptWorker") + +import contextlib +import errno +import glob +import logging +import netrc +import os +import re +import shutil +import stat +import sys +import tempfile +import time +import traceback +try: + from urllib.parse import urlsplit, urlunsplit +except ImportError: + from urlparse import urlsplit, urlunsplit + +try: + from configparser import ConfigParser +except ImportError: + from ConfigParser import ConfigParser + +import apt +import apt.auth +import apt.cache +import apt.debfile +import apt_pkg +import aptsources +import aptsources.distro +from aptsources.sourceslist import SourcesList +from gi.repository import GObject, GLib + +from . import BaseWorker +from ..enums import * +from ..errors import * +from .. import lock +from ..utils import set_euid_egid +from ..progress import ( + DaemonOpenProgress, + DaemonInstallProgress, + DaemonAcquireProgress, + DaemonAcquireRepoProgress, + DaemonDpkgInstallProgress, + DaemonDpkgReconfigureProgress, + DaemonDpkgRecoverProgress, + DaemonForkProgress) + +log = logging.getLogger("AptDaemon.Worker") + +# Just required to detect translatable strings. The translation is done by +# core.Transaction.gettext +_ = lambda s: s + +_POPCON_PATH = "/etc/popularity-contest.conf" +_POPCON_DEFAULT = """# Config file for Debian's popularity-contest package. +# +# To change this file, use: +# dpkg-reconfigure popularity-contest +# +# You can also edit it by hand, if you so choose. +# +# See /usr/share/popularity-contest/default.conf for more info +# on the options. +MY_HOSTID="%(host_id)s" +PARTICIPATE="%(participate)s" +USE_HTTP="yes" +""" + + +def trans_only_installs_pkgs_from_high_trust_repos(trans, + whitelist=set()): + """Return True if this transaction only touches packages in the + aptdaemon repoisotry high trust repository whitelist + """ + # the transaction *must* be simulated before + if not trans.simulated: + return False + # we never allow unauthenticated ones + if trans.unauthenticated: + return False + # paranoia: wrong role + if trans.role not in (ROLE_INSTALL_PACKAGES, ROLE_COMMIT_PACKAGES): + return False + # if there is anything touched that is not a install bail out + for enum in (PKGS_REINSTALL, PKGS_REMOVE, PKGS_PURGE, PKGS_DOWNGRADE, + PKGS_UPGRADE): + if trans.packages[enum]: + return False + # paranoia(2): we must want to install something + if not trans.packages[PKGS_INSTALL]: + return False + # we only care about the name, not the version + pkgs = [pkg.split("=")[0] for pkg in trans.packages[PKGS_INSTALL]] + # if the install packages matches the whitelisted set we are good + return set(pkgs) == set(trans.high_trust_packages) + + +def read_high_trust_repository_dir(whitelist_cfg_d): + """Return a set of (origin, component, pkgname-regexp) from a + high-trust-repository-whitelist.d directory + """ + whitelist = set() + for path in glob.glob(os.path.join(whitelist_cfg_d, "*.cfg")): + whitelist |= _read_high_trust_repository_whitelist_file(path) + return whitelist + + +def _read_high_trust_repository_whitelist_file(path): + """Read a individual high-trust-repository whitelist file and return + a set of tuples (origin, component, pkgname-regexp) + """ + parser = ConfigParser() + whitelist = set() + try: + parser.read(path) + except Exception as e: + log.error("Failed to read repository whitelist '%s' (%s)" % (path, e)) + return whitelist + for section in parser.sections(): + origin = parser.get(section, "origin") + component = parser.get(section, "component") + pkgnames = parser.get(section, "pkgnames") + whitelist.add((origin, component, pkgnames)) + return whitelist + + +class AptWorker(BaseWorker): + + """Worker which processes transactions from the queue.""" + + NATIVE_ARCH = apt_pkg.get_architectures()[0] + + # the basedir under which license keys can be stored + LICENSE_KEY_ROOTDIR = "/opt/" + + def __init__(self, chroot=None, load_plugins=True): + """Initialize a new AptWorker instance.""" + BaseWorker.__init__(self, chroot, load_plugins) + self._cache = None + + # Change to a given chroot + if self.chroot: + apt_conf_file = os.path.join(chroot, "etc/apt/apt.conf") + if os.path.exists(apt_conf_file): + apt_pkg.read_config_file(apt_pkg.config, apt_conf_file) + apt_conf_dir = os.path.join(chroot, "etc/apt/apt.conf.d") + if os.path.isdir(apt_conf_dir): + apt_pkg.read_config_dir(apt_pkg.config, apt_conf_dir) + apt_pkg.config["Dir"] = chroot + apt_pkg.config["Dir::State::Status"] = os.path.join( + chroot, "var/lib/dpkg/status") + apt_pkg.config.clear("DPkg::Post-Invoke") + apt_pkg.config.clear("DPkg::Options") + apt_pkg.config["DPkg::Options::"] = "--root=%s" % chroot + apt_pkg.config["DPkg::Options::"] = ("--log=%s/var/log/dpkg.log" % + chroot) + status_file = apt_pkg.config.find_file("Dir::State::status") + lock.frontend_lock.path = os.path.join(os.path.dirname(status_file), + "lock-frontend") + lock.status_lock.path = os.path.join(os.path.dirname(status_file), + "lock") + archives_dir = apt_pkg.config.find_dir("Dir::Cache::Archives") + lock.archive_lock.path = os.path.join(archives_dir, "lock") + lists_dir = apt_pkg.config.find_dir("Dir::State::lists") + lock.lists_lock.path = os.path.join(lists_dir, "lock") + apt_pkg.init_system() + + # a set of tuples of the type (origin, component, pkgname-regexp) + # that on install will trigger a different kind of polkit + # authentication request (see LP: #1035207), useful for e.g. + # webapps/company repos + self._high_trust_repositories = read_high_trust_repository_dir( + os.path.join(apt_pkg.config.find_dir("Dir"), + "etc/aptdaemon/high-trust-repository-whitelist.d")) + log.debug( + "using high-trust whitelist: '%s'" % self._high_trust_repositories) + + self._status_orig = apt_pkg.config.find_file("Dir::State::status") + self._status_frozen = None + if load_plugins: + self._load_plugins(["modify_cache_after", "modify_cache_before", + "get_license_key"]) + + def _call_plugins(self, name, resolver=None): + """Call all plugins of a given type.""" + if not resolver: + # If the resolver of the original task isn't available we create + # a new one and protect the already marked changes + resolver = apt.cache.ProblemResolver(self._cache) + for pkg in self._cache.get_changes(): + resolver.clear(pkg) + resolver.protect(pkg) + if pkg.marked_delete: + resolver.remove(pkg) + if name not in self.plugins: + log.debug("There isn't any registered %s plugin" % name) + return False + for plugin in self.plugins[name]: + log.debug("Calling %s plugin: %s", name, plugin) + try: + plugin(resolver, self._cache) + except Exception as error: + log.critical("Failed to call %s plugin:\n%s" % (plugin, error)) + return True + + def _run_transaction(self, trans): + """Run the worker""" + try: + lock.wait_for_lock(trans) + # Prepare the package cache + if (trans.role == ROLE_FIX_INCOMPLETE_INSTALL or + not self.is_dpkg_journal_clean()): + self.fix_incomplete_install(trans) + # Process transaction which don't require a cache + if trans.role == ROLE_ADD_VENDOR_KEY_FILE: + self.add_vendor_key_from_file(trans, **trans.kwargs) + elif trans.role == ROLE_ADD_VENDOR_KEY_FROM_KEYSERVER: + self.add_vendor_key_from_keyserver(trans, **trans.kwargs) + elif trans.role == ROLE_REMOVE_VENDOR_KEY: + self.remove_vendor_key(trans, **trans.kwargs) + elif trans.role == ROLE_ADD_REPOSITORY: + self.add_repository(trans, **trans.kwargs) + elif trans.role == ROLE_ENABLE_DISTRO_COMP: + self.enable_distro_comp(trans, **trans.kwargs) + elif trans.role == ROLE_RECONFIGURE: + self.reconfigure(trans, trans.packages[PKGS_REINSTALL], + **trans.kwargs) + elif trans.role == ROLE_CLEAN: + self.clean(trans) + # Check if the transaction has been just simulated. So we could + # skip marking the changes a second time. + elif (trans.role in (ROLE_REMOVE_PACKAGES, ROLE_INSTALL_PACKAGES, + ROLE_UPGRADE_PACKAGES, ROLE_COMMIT_PACKAGES, + ROLE_UPGRADE_SYSTEM, + ROLE_FIX_BROKEN_DEPENDS) and + self.marked_tid == trans.tid): + self._apply_changes(trans) + trans.exit = EXIT_SUCCESS + return False + else: + self._open_cache(trans) + # Process transaction which can handle a broken dep cache + if trans.role == ROLE_FIX_BROKEN_DEPENDS: + self.fix_broken_depends(trans) + elif trans.role == ROLE_UPDATE_CACHE: + self.update_cache(trans, **trans.kwargs) + # Process the transactions which require a consistent cache + elif trans.role == ROLE_ADD_LICENSE_KEY: + self.add_license_key(trans, **trans.kwargs) + elif self._cache and self._cache.broken_count: + raise TransactionFailed(ERROR_CACHE_BROKEN, + self._get_broken_details(trans)) + if trans.role == ROLE_PK_QUERY: + self.query(trans) + elif trans.role == ROLE_INSTALL_FILE: + self.install_file(trans, **trans.kwargs) + elif trans.role in [ROLE_REMOVE_PACKAGES, ROLE_INSTALL_PACKAGES, + ROLE_UPGRADE_PACKAGES, ROLE_COMMIT_PACKAGES]: + self.commit_packages(trans, *trans.packages) + elif trans.role == ROLE_UPGRADE_SYSTEM: + self.upgrade_system(trans, **trans.kwargs) + finally: + lock.release() + + def commit_packages(self, trans, install, reinstall, remove, purge, + upgrade, downgrade, simulate=False): + """Perform a complex package operation. + + Keyword arguments: + trans - the transaction + install - list of package names to install + reinstall - list of package names to reinstall + remove - list of package names to remove + purge - list of package names to purge including configuration files + upgrade - list of package names to upgrade + downgrade - list of package names to upgrade + simulate -- if True the changes won't be applied + """ + log.info("Committing packages: %s, %s, %s, %s, %s, %s", + install, reinstall, remove, purge, upgrade, downgrade) + with self._cache.actiongroup(): + resolver = apt.cache.ProblemResolver(self._cache) + self._mark_packages_for_installation(install, resolver) + self._mark_packages_for_installation(reinstall, resolver, + reinstall=True) + self._mark_packages_for_removal(remove, resolver) + self._mark_packages_for_removal(purge, resolver, purge=True) + self._mark_packages_for_upgrade(upgrade, resolver) + self._mark_packages_for_downgrade(downgrade, resolver) + self._resolve_depends(trans, resolver) + self._check_obsoleted_dependencies(trans, resolver) + if not simulate: + self._apply_changes(trans) + + def _resolve_depends(self, trans, resolver): + """Resolve the dependencies using the given ProblemResolver.""" + self._call_plugins("modify_cache_before", resolver) + try: + resolver.resolve() + except SystemError: + raise TransactionFailed(ERROR_DEP_RESOLUTION_FAILED, + self._get_broken_details(trans, now=False)) + if self._call_plugins("modify_cache_after", resolver): + try: + resolver.resolve() + except SystemError: + details = self._get_broken_details(trans, now=False) + raise TransactionFailed(ERROR_DEP_RESOLUTION_FAILED, details) + + def _get_high_trust_packages(self): + """ Return a list of packages that come from a high-trust repo """ + def _in_high_trust_repository(pkgname, pkgorigin): + for origin, component, regexp in self._high_trust_repositories: + if (origin == pkgorigin.origin and + component == pkgorigin.component and + re.match(regexp, pkgname)): + return True + return False + # loop + from_high_trust_repo = [] + for pkg in self._cache.get_changes(): + if pkg.marked_install: + for origin in pkg.candidate.origins: + if _in_high_trust_repository(pkg.name, origin): + from_high_trust_repo.append(pkg.name) + break + return from_high_trust_repo + + def _get_unauthenticated(self): + """Return a list of unauthenticated package names """ + unauthenticated = [] + for pkg in self._cache.get_changes(): + if (pkg.marked_install or + pkg.marked_downgrade or + pkg.marked_upgrade or + pkg.marked_reinstall): + trusted = False + for origin in pkg.candidate.origins: + trusted |= origin.trusted + if not trusted: + unauthenticated.append(pkg.name) + return unauthenticated + + def _mark_packages_for_installation(self, packages, resolver, + reinstall=False): + """Mark packages for installation.""" + for pkg_name, pkg_ver, pkg_rel in [self._split_package_id(pkg) + for pkg in packages]: + pkg_name, sep, auto_marker = pkg_name.partition("#") + from_user = (auto_marker != "auto") + try: + pkg = self._cache[pkg_name] + except KeyError: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("Package %s isn't available"), + pkg_name) + if reinstall: + if not pkg.is_installed: + raise TransactionFailed(ERROR_PACKAGE_NOT_INSTALLED, + _("Package %s isn't installed"), + pkg.name) + if pkg_ver and pkg.installed.version != pkg_ver: + raise TransactionFailed(ERROR_PACKAGE_NOT_INSTALLED, + _("The version %s of %s isn't " + "installed"), + pkg_ver, pkg_name) + else: + # Fail if the user requests to install the same version + # of an already installed package. + if (pkg.is_installed and + # Compare version numbers + pkg_ver and pkg_ver == pkg.installed.version and + # Optionally compare the origin if specified + (not pkg_rel or + pkg_rel in [origin.archive for + origin in pkg.installed.origins])): + raise TransactionFailed( + ERROR_PACKAGE_ALREADY_INSTALLED, + _("Package %s is already installed"), pkg_name) + + if pkg_ver: + try: + pkg.candidate = pkg.versions[pkg_ver] + except KeyError: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("The version %s of %s isn't " + "available."), pkg_ver, pkg_name) + elif pkg_rel: + self._set_candidate_release(pkg, pkg_rel) + + pkg.mark_install(False, False, from_user) + resolver.clear(pkg) + resolver.protect(pkg) + + def enable_distro_comp(self, trans, component): + """Enable given component in the sources list. + + Keyword arguments: + trans -- the corresponding transaction + component -- a component, e.g. main or universe + """ + trans.progress = 101 + trans.status = STATUS_COMMITTING + old_umask = os.umask(0o022) + try: + sourceslist = SourcesList() + distro = aptsources.distro.get_distro() + distro.get_sources(sourceslist) + distro.enable_component(component) + sourceslist.save() + finally: + os.umask(old_umask) + + def add_repository(self, trans, src_type, uri, dist, comps, comment, + sourcesfile): + """Add given repository to the sources list. + + Keyword arguments: + trans -- the corresponding transaction + src_type -- the type of the entry (deb, deb-src) + uri -- the main repository uri (e.g. http://archive.ubuntu.com/ubuntu) + dist -- the distribution to use (e.g. karmic, "/") + comps -- a (possible empty) list of components (main, restricted) + comment -- an (optional) comment + sourcesfile -- an (optinal) filename in sources.list.d + """ + trans.progress = 101 + trans.status = STATUS_COMMITTING + + if sourcesfile: + if not sourcesfile.endswith(".list"): + sourcesfile += ".list" + dir = apt_pkg.config.find_dir("Dir::Etc::sourceparts") + sourcesfile = os.path.join(dir, os.path.basename(sourcesfile)) + else: + sourcesfile = None + # Store any private login information in a separate auth.conf file + if re.match("(http|https|ftp)://\S+?:\S+?@\S+", uri): + uri = self._store_and_strip_password_from_uri(uri) + auth_conf_path = apt_pkg.config.find_file("Dir::etc::netrc") + if not comment: + comment = "credentials stored in %s" % auth_conf_path + else: + comment += "; credentials stored in %s" % auth_conf_path + try: + sources = SourcesList() + entry = sources.add(src_type, uri, dist, comps, comment, + file=sourcesfile) + if entry.invalid: + # FIXME: Introduce new error codes + raise RepositoryInvalidError() + except: + log.exception("adding repository") + raise + else: + sources.save() + + def _store_and_strip_password_from_uri(self, uri, auth_conf_path=None): + """Extract the credentials from an URI. Store the password in + auth.conf and return the URI without any password information. + """ + try: + res = urlsplit(uri) + except ValueError as error: + log.warning("Failed to urlsplit '%s'", error) + return uri + netloc_public = res.netloc.replace("%s:%s@" % (res.username, + res.password), + "") + machine = netloc_public + res.path + # find auth.conf + if auth_conf_path is None: + auth_conf_path = apt_pkg.config.find_file("Dir::etc::netrc") + + # read all "machine"s from the auth.conf "netrc" file + netrc_hosts = {} + netrc_hosts_as_text = "" + if os.path.exists(auth_conf_path): + netrc_hosts = netrc.netrc(auth_conf_path).hosts + with open(auth_conf_path, "rb") as f: + netrc_hosts_as_text = f.read().decode("UTF-8") + + # the new entry + new_netrc_entry = "\nmachine %s login %s password %s\n" % ( + machine, res.username, res.password) + # if there is the same machine already defined, update it + # using a regexp this will ensure order/comments remain + if machine in netrc_hosts: + sub_regexp = r'machine\s+%s\s+login\s+%s\s+password\s+%s' % ( + re.escape(machine), + re.escape(netrc_hosts[machine][0]), + re.escape(netrc_hosts[machine][2])) + replacement = 'machine %s login %s password %s' % ( + machine, res.username, res.password) + # this may happen if e.g. the order is unexpected + if not re.search(sub_regexp, netrc_hosts_as_text): + log.warning("can not replace existing netrc entry for '%s' " + "prepending it instead" % machine) + netrc_hosts_as_text = new_netrc_entry + netrc_hosts_as_text + else: + netrc_hosts_as_text = re.sub( + sub_regexp, replacement, netrc_hosts_as_text) + else: + netrc_hosts_as_text += new_netrc_entry + + # keep permssion bits of the file + mode = 0o640 + try: + mode = os.stat(auth_conf_path)[stat.ST_MODE] + except OSError as e: + if e.errno != errno.ENOENT: + raise + # write out, tmp file first plus rename to be atomic + try: + auth_conf_tmp = tempfile.NamedTemporaryFile( + dir=os.path.dirname(auth_conf_path), + prefix=os.path.basename(auth_conf_path), + delete=False) + auth_conf_tmp.write(netrc_hosts_as_text.encode('UTF-8')) + auth_conf_tmp.close() + os.rename(auth_conf_tmp.name, auth_conf_path) + # and restore permissions (or set default ones) + os.chmod(auth_conf_path, mode) + except OSError as error: + log.warning("Failed to write auth.conf: '%s'" % error) + + # Return URI without user/pass + return urlunsplit((res.scheme, netloc_public, res.path, res.query, + res.fragment)) + + def add_vendor_key_from_keyserver(self, trans, keyid, keyserver): + """Add the signing key from the given (keyid, keyserver) to the + trusted vendors. + + Keyword argument: + trans -- the corresponding transaction + keyid - the keyid of the key (e.g. 0x0EB12F05) + keyserver - the keyserver (e.g. keyserver.ubuntu.com) + """ + log.info("Adding vendor key from keyserver: %s %s", keyid, keyserver) + # Perform some sanity checks + try: + res = urlsplit(keyserver) + except ValueError: + raise TransactionFailed(ERROR_KEY_NOT_INSTALLED, + # TRANSLATORS: %s is the URL of GnuPG + # keyserver + _("The keyserver URL is invalid: %s"), + keyserver) + if res.scheme not in ["hkp", "ldap", "ldaps", "http", "https"]: + raise TransactionFailed(ERROR_KEY_NOT_INSTALLED, + # TRANSLATORS: %s is the URL of GnuPG + # keyserver + _("Invalid protocol of the server: %s"), + keyserver) + try: + int(keyid, 16) + except ValueError: + raise TransactionFailed(ERROR_KEY_NOT_INSTALLED, + # TRANSLATORS: %s is the id of a GnuPG key + # e.g. E08ADE95 + _("Invalid key id: %s"), keyid) + trans.status = STATUS_DOWNLOADING + trans.progress = 101 + with DaemonForkProgress(trans) as progress: + progress.run(apt.auth.add_key_from_keyserver, keyid, keyserver) + if progress._child_exit != 0: + # TRANSLATORS: The first %s is the key id and the second the server + raise TransactionFailed(ERROR_KEY_NOT_INSTALLED, + _("Failed to download and install the key " + "%s from %s:\n%s"), + keyid, keyserver, progress.output) + + def add_vendor_key_from_file(self, trans, path): + """Add the signing key from the given file to the trusted vendors. + + Keyword argument: + path -- absolute path to the key file + """ + log.info("Adding vendor key from file: %s", path) + trans.progress = 101 + trans.status = STATUS_COMMITTING + with DaemonForkProgress(trans) as progress: + progress.run(apt.auth.add_key_from_file, path) + if progress._child_exit != 0: + raise TransactionFailed(ERROR_KEY_NOT_INSTALLED, + _("Key file %s couldn't be installed: %s"), + path, progress.output) + + def remove_vendor_key(self, trans, fingerprint): + """Remove repository key. + + Keyword argument: + trans -- the corresponding transaction + fingerprint -- fingerprint of the key to remove + """ + log.info("Removing vendor key: %s", fingerprint) + trans.progress = 101 + trans.status = STATUS_COMMITTING + try: + int(fingerprint, 16) + except ValueError: + raise TransactionFailed(ERROR_KEY_NOT_REMOVED, + # TRANSLATORS: %s is the id of a GnuPG key + # e.g. E08ADE95 + _("Invalid key id: %s"), fingerprint) + with DaemonForkProgress(trans) as progress: + progress.run(apt.auth.remove_key, fingerprint) + if progress._child_exit != 0: + raise TransactionFailed(ERROR_KEY_NOT_REMOVED, + _("Key with fingerprint %s couldn't be " + "removed: %s"), + fingerprint, progress.output) + + def install_file(self, trans, path, force, simulate=False): + """Install local package file. + + Keyword argument: + trans -- the corresponding transaction + path -- absolute path to the package file + force -- if installing an invalid package is allowed + simulate -- if True the changes won't be committed but the debfile + instance will be returned + """ + log.info("Installing local package file: %s", path) + # Check if the dpkg can be installed at all + trans.status = STATUS_RESOLVING_DEP + deb = self._check_deb_file(trans, path, force) + # Check for required changes and apply them before + (install, remove, unauth) = deb.required_changes + self._call_plugins("modify_cache_after") + if simulate: + return deb + with self._frozen_status(): + if len(install) > 0 or len(remove) > 0: + self._apply_changes(trans, fetch_range=(15, 33), + install_range=(34, 63)) + # Install the dpkg file + deb_progress = DaemonDpkgInstallProgress(trans, begin=64, end=95) + res = deb.install(deb_progress) + trans.output += deb_progress.output + if res: + raise TransactionFailed(ERROR_PACKAGE_MANAGER_FAILED, + trans.output) + + def _mark_packages_for_removal(self, packages, resolver, purge=False): + """Mark packages for installation.""" + for pkg_name, pkg_ver, pkg_rel in [self._split_package_id(pkg) + for pkg in packages]: + try: + pkg = self._cache[pkg_name] + except KeyError: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("Package %s isn't available"), + pkg_name) + if not pkg.is_installed and not pkg.installed_files: + raise TransactionFailed(ERROR_PACKAGE_NOT_INSTALLED, + _("Package %s isn't installed"), + pkg_name) + if not self.is_deletable(pkg): + raise TransactionFailed(ERROR_NOT_REMOVE_ESSENTIAL_PACKAGE, + _("Package %s cannot be removed."), + pkg_name) + if pkg_ver and pkg.installed != pkg_ver: + raise TransactionFailed(ERROR_PACKAGE_NOT_INSTALLED, + _("The version %s of %s is not " + "installed"), pkg_ver, pkg_name) + pkg.mark_delete(False, purge) + resolver.clear(pkg) + resolver.protect(pkg) + resolver.remove(pkg) + + def is_deletable(self, pkg): + if pkg.name == "aptdaemon": + return False + + if pkg.essential == True or (pkg.installed and pkg.installed.priority == "required"): + # Package is essential or required + is_orphan = False + if pkg.candidate == None or (not pkg.candidate.downloadable): + is_orphan = True + for version in pkg.versions: + if version.downloadable: + is_orphan = False + break + # only allow to delete it if it's an orphan + return is_orphan + + return True + + def _check_obsoleted_dependencies(self, trans, resolver=None): + """Mark obsoleted dependencies of to be removed packages + for removal. + """ + if not trans.remove_obsoleted_depends: + return + if not resolver: + resolver = apt.cache.ProblemResolver(self._cache) + installed_deps = set() + with self._cache.actiongroup(): + for pkg in self._cache.get_changes(): + if pkg.marked_delete: + installed_deps = self._installed_dependencies( + pkg.name, installed_deps) + for dep_name in installed_deps: + if dep_name in self._cache: + pkg = self._cache[dep_name] + if pkg.is_installed and pkg.is_auto_removable: + pkg.mark_delete(False) + # do an additional resolver run to ensure that the autoremove + # never leaves the cache in an inconsistent state, see bug + # LP: #659111 for the rational, essentially this may happen + # if a package is marked install during problem resolving but + # is later no longer required. the resolver deals with that + self._resolve_depends(trans, resolver) + + def _installed_dependencies(self, pkg_name, all_deps=None): + """Recursively return all installed dependencies of a given package.""" + # FIXME: Should be part of python-apt, since it makes use of non-public + # API. Perhaps by adding a recursive argument to + # apt.package.Version.get_dependencies() + if not all_deps: + all_deps = set() + if pkg_name not in self._cache: + return all_deps + cur = self._cache[pkg_name]._pkg.current_ver + if not cur: + return all_deps + for sec in ("PreDepends", "Depends", "Recommends"): + try: + for dep in cur.depends_list[sec]: + dep_name = dep[0].target_pkg.name + if dep_name not in all_deps: + all_deps.add(dep_name) + all_deps |= self._installed_dependencies(dep_name, + all_deps) + except KeyError: + pass + return all_deps + + def _mark_packages_for_downgrade(self, packages, resolver): + """Mark packages for downgrade.""" + for pkg_name, pkg_ver, pkg_rel in [self._split_package_id(pkg) + for pkg in packages]: + try: + pkg = self._cache[pkg_name] + except KeyError: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("Package %s isn't available"), + pkg_name) + if not pkg.is_installed: + raise TransactionFailed(ERROR_PACKAGE_NOT_INSTALLED, + _("Package %s isn't installed"), + pkg_name) + auto = pkg.is_auto_installed + + if pkg_ver: + if pkg.installed and pkg.installed.version < pkg_ver: + # FIXME: We need a new error enum + raise TransactionFailed(ERROR_NO_PACKAGE, + _("The former version %s of %s " + "is already installed"), + pkg.installed.version, pkg.name) + elif pkg.installed and pkg.installed.version == pkg_ver: + raise TransactionFailed(ERROR_PACKAGE_ALREADY_INSTALLED, + _("The version %s of %s " + "is already installed"), + pkg.installed.version, pkg.name) + try: + pkg.candidate = pkg.versions[pkg_ver] + except KeyError: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("The version %s of %s isn't " + "available"), pkg_ver, pkg_name) + else: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("You need to specify a version to " + "downgrade %s to"), + pkg_name) + + pkg.mark_install(False, False, True) + pkg.mark_auto(auto) + resolver.clear(pkg) + resolver.protect(pkg) + + def _mark_packages_for_upgrade(self, packages, resolver): + """Mark packages for upgrade.""" + for pkg_name, pkg_ver, pkg_rel in [self._split_package_id(pkg) + for pkg in packages]: + try: + pkg = self._cache[pkg_name] + except KeyError: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("Package %s isn't available"), + pkg_name) + if not pkg.is_installed: + raise TransactionFailed(ERROR_PACKAGE_NOT_INSTALLED, + _("Package %s isn't installed"), + pkg_name) + auto = pkg.is_auto_installed + + if pkg_ver: + if (pkg.installed and + apt_pkg.version_compare(pkg.installed.version, + pkg_ver) == 1): + raise TransactionFailed(ERROR_PACKAGE_UPTODATE, + _("The later version %s of %s " + "is already installed"), + pkg.installed.version, pkg.name) + elif (pkg.installed and + apt_pkg.version_compare(pkg.installed.version, + pkg_ver) == 0): + raise TransactionFailed(ERROR_PACKAGE_UPTODATE, + _("The version %s of %s " + "is already installed"), + pkg.installed.version, pkg.name) + try: + pkg.candidate = pkg.versions[pkg_ver] + except KeyError: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("The version %s of %s isn't " + "available."), pkg_ver, pkg_name) + + elif pkg_rel: + self._set_candidate_release(pkg, pkg_rel) + + pkg.mark_install(False, False, True) + pkg.mark_auto(auto) + resolver.clear(pkg) + resolver.protect(pkg) + + @staticmethod + def _set_candidate_release(pkg, release): + """Set the candidate of a package to the one from the given release.""" + # FIXME: Should be moved to python-apt + # Check if the package is provided in the release + for version in pkg.versions: + if [origin for origin in version.origins + if origin.archive == release]: + break + else: + raise TransactionFailed(ERROR_NO_PACKAGE, + _("The package %s isn't available in " + "the %s release."), pkg.name, release) + pkg._pcache.cache_pre_change() + pkg._pcache._depcache.set_candidate_release(pkg._pkg, version._cand, + release) + pkg._pcache.cache_post_change() + + def update_cache(self, trans, sources_list): + """Update the cache. + + Keyword arguments: + trans -- the corresponding transaction + sources_list -- only update the repositories found in the sources.list + snippet by the given file name. + """ + + def compare_pathes(first, second): + """Small helper to compare two pathes.""" + return os.path.normpath(first) == os.path.normpath(second) + + log.info("Updating cache") + + progress = DaemonAcquireRepoProgress(trans, begin=10, end=90) + if sources_list and not sources_list.startswith("/"): + dir = apt_pkg.config.find_dir("Dir::Etc::sourceparts") + sources_list = os.path.join(dir, sources_list) + if sources_list: + # For security reasons (LP #722228) we only allow files inside + # sources.list.d as basedir + basedir = apt_pkg.config.find_dir("Dir::Etc::sourceparts") + system_sources = apt_pkg.config.find_file("Dir::Etc::sourcelist") + if "/" in sources_list: + sources_list = os.path.abspath(sources_list) + # Check if the sources_list snippet is in the sourceparts + # directory + common_prefix = os.path.commonprefix([sources_list, basedir]) + if not (compare_pathes(common_prefix, basedir) or + compare_pathes(sources_list, system_sources)): + raise AptDaemonError("Only alternative sources.list files " + "inside '%s' are allowed (not '%s')" % + (basedir, sources_list)) + else: + sources_list = os.path.join(basedir, sources_list) + try: + self._cache.update(progress, sources_list=sources_list) + except apt.cache.FetchFailedException as error: + # ListUpdate() method of apt handles a cancelled operation + # as a failed one, see LP #162441 + if trans.cancelled: + raise TransactionCancelled() + else: + raise TransactionFailed(ERROR_REPO_DOWNLOAD_FAILED, + str(error)) + except apt.cache.FetchCancelledException: + raise TransactionCancelled() + except apt.cache.LockFailedException: + raise TransactionFailed(ERROR_NO_LOCK) + self._open_cache(trans, begin=91, end=95) + + def upgrade_system(self, trans, safe_mode=True, simulate=False): + """Upgrade the system. + + Keyword argument: + trans -- the corresponding transaction + safe_mode -- if additional software should be installed or removed to + satisfy the dependencies the an updates + simulate -- if the changes should not be applied + """ + log.info("Upgrade system with safe mode: %s" % safe_mode) + trans.status = STATUS_RESOLVING_DEP + # FIXME: What to do if already uptotdate? Add error code? + self._call_plugins("modify_cache_before") + try: + self._cache.upgrade(dist_upgrade=not safe_mode) + except SystemError as excep: + raise TransactionFailed(ERROR_DEP_RESOLUTION_FAILED, str(excep)) + self._call_plugins("modify_cache_after") + self._check_obsoleted_dependencies(trans) + if not simulate: + self._apply_changes(trans) + + def fix_incomplete_install(self, trans): + """Run dpkg --configure -a to recover from a failed installation. + + Keyword arguments: + trans -- the corresponding transaction + """ + log.info("Fixing incomplete installs") + trans.status = STATUS_CLEANING_UP + with self._frozen_status(): + with DaemonDpkgRecoverProgress(trans) as progress: + progress.run() + trans.output += progress.output + if progress._child_exit != 0: + raise TransactionFailed(ERROR_PACKAGE_MANAGER_FAILED, + trans.output) + + def reconfigure(self, trans, packages, priority): + """Run dpkg-reconfigure to reconfigure installed packages. + + Keyword arguments: + trans -- the corresponding transaction + packages -- list of packages to reconfigure + priority -- the lowest priority of question which should be asked + """ + log.info("Reconfiguring packages") + with self._frozen_status(): + with DaemonDpkgReconfigureProgress(trans) as progress: + progress.run(packages, priority) + trans.output += progress.output + if progress._child_exit != 0: + raise TransactionFailed(ERROR_PACKAGE_MANAGER_FAILED, + trans.output) + + def fix_broken_depends(self, trans, simulate=False): + """Try to fix broken dependencies. + + Keyword arguments: + trans -- the corresponding transaction + simualte -- if the changes should not be applied + """ + log.info("Fixing broken depends") + trans.status = STATUS_RESOLVING_DEP + try: + self._cache._depcache.fix_broken() + except SystemError: + raise TransactionFailed(ERROR_DEP_RESOLUTION_FAILED, + self._get_broken_details(trans)) + if not simulate: + self._apply_changes(trans) + + def _open_cache(self, trans, begin=1, end=5, quiet=False, status=None): + """Open the APT cache. + + Keyword arguments: + trans -- the corresponding transaction + start -- the begin of the progress range + end -- the end of the the progress range + quiet -- if True do no report any progress + status -- an alternative dpkg status file + """ + self.marked_tid = None + trans.status = STATUS_LOADING_CACHE + if not status: + status = self._status_orig + apt_pkg.config.set("Dir::State::status", status) + apt_pkg.init_system() + progress = DaemonOpenProgress(trans, begin=begin, end=end, + quiet=quiet) + try: + if not isinstance(self._cache, apt.cache.Cache): + self._cache = apt.cache.Cache(progress) + else: + self._cache.open(progress) + except SystemError as excep: + raise TransactionFailed(ERROR_NO_CACHE, str(excep)) + + def is_dpkg_journal_clean(self): + """Return False if there are traces of incomplete dpkg status + updates.""" + status_updates = os.path.join(os.path.dirname(self._status_orig), + "updates/") + for dentry in os.listdir(status_updates): + if dentry.isdigit(): + return False + return True + + def _apply_changes(self, trans, fetch_range=(15, 50), + install_range=(50, 90)): + """Apply previously marked changes to the system. + + Keyword arguments: + trans -- the corresponding transaction + fetch_range -- tuple containing the start and end point of the + download progress + install_range -- tuple containing the start and end point of the + install progress + """ + changes = self._cache.get_changes() + if not changes: + return + # Do not allow to remove essential packages + for pkg in changes: + if pkg.marked_delete and not self.is_deletable(pkg): + raise TransactionFailed(ERROR_NOT_REMOVE_ESSENTIAL_PACKAGE, + _("Package %s cannot be removed"), + pkg.name) + # Check if any of the cache changes get installed from an + # unauthenticated repository"" + if not trans.allow_unauthenticated and trans.unauthenticated: + raise TransactionFailed(ERROR_PACKAGE_UNAUTHENTICATED, + " ".join(sorted(trans.unauthenticated))) + if trans.cancelled: + raise TransactionCancelled() + trans.cancellable = False + fetch_progress = DaemonAcquireProgress(trans, begin=fetch_range[0], + end=fetch_range[1]) + inst_progress = DaemonInstallProgress(trans, begin=install_range[0], + end=install_range[1]) + with self._frozen_status(): + try: + # This was backported as + if "allow_unauthenticated" in apt.Cache.commit.__doc__: + self._cache.commit(fetch_progress, inst_progress, + allow_unauthenticated=trans.allow_unauthenticated) + else: + self._cache.commit(fetch_progress, inst_progress) + except apt.cache.FetchFailedException as error: + raise TransactionFailed(ERROR_PACKAGE_DOWNLOAD_FAILED, + str(error)) + except apt.cache.FetchCancelledException: + raise TransactionCancelled() + except SystemError as excep: + # Run dpkg --configure -a to recover from a failed transaction + trans.status = STATUS_CLEANING_UP + with DaemonDpkgRecoverProgress(trans, begin=90, end=95) as pro: + pro.run() + output = inst_progress.output + pro.output + trans.output += output + raise TransactionFailed(ERROR_PACKAGE_MANAGER_FAILED, + "%s: %s" % (excep, trans.output)) + else: + trans.output += inst_progress.output + + @contextlib.contextmanager + def _frozen_status(self): + """Freeze the status file to allow simulate operations during + a dpkg call.""" + frozen_dir = tempfile.mkdtemp(prefix="aptdaemon-frozen-status") + shutil.copy(self._status_orig, frozen_dir) + self._status_frozen = os.path.join(frozen_dir, "status") + try: + yield + finally: + shutil.rmtree(frozen_dir) + self._status_frozen = None + + def query(self, trans): + """Process a PackageKit query transaction.""" + raise NotImplementedError + + def _simulate_transaction(self, trans): + depends = [[], [], [], [], [], [], []] + unauthenticated = [] + high_trust_packages = [] + skip_pkgs = [] + size = 0 + installs = reinstalls = removals = purges = upgrades = upgradables = \ + downgrades = [] + + # Only handle transaction which change packages + # FIXME: Add support for ROLE_FIX_INCOMPLETE_INSTALL + if trans.role not in [ROLE_INSTALL_PACKAGES, ROLE_UPGRADE_PACKAGES, + ROLE_UPGRADE_SYSTEM, ROLE_REMOVE_PACKAGES, + ROLE_COMMIT_PACKAGES, ROLE_INSTALL_FILE, + ROLE_FIX_BROKEN_DEPENDS]: + return depends, 0, 0, [], [] + + # If a transaction is currently running use the former status file + if self._status_frozen: + status_path = self._status_frozen + else: + status_path = self._status_orig + self._open_cache(trans, quiet=True, status=status_path) + if trans.role == ROLE_FIX_BROKEN_DEPENDS: + self.fix_broken_depends(trans, simulate=True) + elif self._cache.broken_count: + raise TransactionFailed(ERROR_CACHE_BROKEN, + self._get_broken_details(trans)) + elif trans.role == ROLE_UPGRADE_SYSTEM: + for pkg in self._iterate_packages(): + if pkg.is_upgradable: + upgradables.append(pkg) + self.upgrade_system(trans, simulate=True, **trans.kwargs) + elif trans.role == ROLE_INSTALL_FILE: + deb = self.install_file(trans, simulate=True, **trans.kwargs) + skip_pkgs.append(deb.pkgname) + try: + # Sometimes a thousands comma is used in packages + # See LP #656633 + size = int(deb["Installed-Size"].replace(",", "")) * 1024 + # Some packages ship really large install sizes e.g. + # openvpn access server, see LP #758837 + if size > sys.maxsize: + raise OverflowError("Size is too large: %s Bytes" % size) + except (KeyError, AttributeError, ValueError, OverflowError): + if not trans.kwargs["force"]: + msg = trans.gettext("The package doesn't provide a " + "valid Installed-Size control " + "field. See Debian Policy 5.6.20.") + raise TransactionFailed(ERROR_INVALID_PACKAGE_FILE, msg) + try: + pkg = self._cache[deb.pkgname] + except KeyError: + trans.packages = [[deb.pkgname], [], [], [], [], []] + else: + if pkg.is_installed: + # if we failed to get the size from the deb file do nor + # try to get the delta + if size != 0: + size -= pkg.installed.installed_size + trans.packages = [[], [deb.pkgname], [], [], [], []] + else: + trans.packages = [[deb.pkgname], [], [], [], [], []] + else: + # FIXME: ugly code to get the names of the packages + (installs, reinstalls, removals, purges, + upgrades, downgrades) = [[re.split("(=|/)", entry, 1)[0] + for entry in lst] + for lst in trans.packages] + self.commit_packages(trans, *trans.packages, simulate=True) + + changes = self._cache.get_changes() + changes_names = [] + # get the additional dependencies + for pkg in changes: + if (pkg.marked_upgrade and pkg.is_installed and + pkg.name not in upgrades): + pkg_str = "%s=%s" % (pkg.name, pkg.candidate.version) + depends[PKGS_UPGRADE].append(pkg_str) + elif pkg.marked_reinstall and pkg.name not in reinstalls: + pkg_str = "%s=%s" % (pkg.name, pkg.candidate.version) + depends[PKGS_REINSTALL].append(pkg_str) + elif pkg.marked_downgrade and pkg.name not in downgrades: + pkg_str = "%s=%s" % (pkg.name, pkg.candidate.version) + depends[PKGS_DOWNGRADE].append(pkg_str) + elif pkg.marked_install and pkg.name not in installs: + pkg_str = "%s=%s" % (pkg.name, pkg.candidate.version) + depends[PKGS_INSTALL].append(pkg_str) + elif pkg.marked_delete and pkg.name not in removals: + pkg_str = "%s=%s" % (pkg.name, pkg.installed.version) + depends[PKGS_REMOVE].append(pkg_str) + # FIXME: add support for purges + changes_names.append(pkg.name) + # get the unauthenticated packages + unauthenticated = self._get_unauthenticated() + high_trust_packages = self._get_high_trust_packages() + # Check for skipped upgrades + for pkg in upgradables: + if pkg.marked_keep: + pkg_str = "%s=%s" % (pkg.name, pkg.candidate.version) + depends[PKGS_KEEP].append(pkg_str) + + # apt.cache.Cache.required_download requires a clean cache. Under some + # strange circumstances it can fail (most likely an interrupted + # debconf question), see LP#659438 + # Running dpkg --configure -a fixes the situation + try: + required_download = self._cache.required_download + except SystemError as error: + raise TransactionFailed(ERROR_INCOMPLETE_INSTALL, str(error)) + + required_space = size + self._cache.required_space + + return (depends, required_download, required_space, unauthenticated, + high_trust_packages) + + def _check_deb_file(self, trans, path, force): + """Perform some basic checks for the Debian package. + + :param trans: The transaction instance. + + :returns: An apt.debfile.Debfile instance. + """ + # This code runs as root for simulate and simulate requires no + # authentication - so we need to ensure we do not leak information + # about files here (LP: #1449587, CVE-2015-1323) + with set_euid_egid(trans.uid, trans.gid): + if not os.path.isfile(path): + raise TransactionFailed(ERROR_UNREADABLE_PACKAGE_FILE, path) + + try: + deb = apt.debfile.DebPackage(path, self._cache) + except IOError: + raise TransactionFailed(ERROR_UNREADABLE_PACKAGE_FILE, path) + except Exception as error: + raise TransactionFailed(ERROR_INVALID_PACKAGE_FILE, str(error)) + try: + ret = deb.check() + except Exception as error: + raise TransactionFailed(ERROR_DEP_RESOLUTION_FAILED, str(error)) + if not ret: + raise TransactionFailed(ERROR_DEP_RESOLUTION_FAILED, + deb._failure_string) + return deb + + def clean(self, trans): + """Clean the download directories. + + Keyword arguments: + trans -- the corresponding transaction + """ + # FIXME: Use pkgAcquire.Clean(). Currently not part of python-apt. + trans.status = STATUS_CLEANING_UP + archive_path = apt_pkg.config.find_dir("Dir::Cache::archives") + for dir in [archive_path, os.path.join(archive_path, "partial")]: + for filename in os.listdir(dir): + if filename == "lock": + continue + path = os.path.join(dir, filename) + if os.path.isfile(path): + log.debug("Removing file %s", path) + os.remove(path) + + def add_license_key(self, trans, pkg_name, json_token, server_name): + """Add a license key data to the given package. + + Keyword arguemnts: + trans -- the coresponding transaction + pkg_name -- the name of the corresponding package + json_token -- the oauth token as json + server_name -- the server to use (ubuntu-production, ubuntu-staging) + """ + # set transaction state to downloading + trans.status = STATUS_DOWNLOADING + try: + license_key, license_key_path = ( + self.plugins["get_license_key"][0](trans.uid, pkg_name, + json_token, server_name)) + except Exception as error: + logging.exception("get_license_key plugin failed") + raise TransactionFailed(ERROR_LICENSE_KEY_DOWNLOAD_FAILED, + str(error)) + # ensure stuff is good + if not license_key_path or not license_key: + raise TransactionFailed(ERROR_LICENSE_KEY_DOWNLOAD_FAILED, + _("The license key is empty")) + + # add license key if we have one + self._add_license_key_to_system(pkg_name, license_key, + license_key_path) + + def _add_license_key_to_system(self, pkg_name, license_key, + license_key_path): + # fixup path + license_key_path = os.path.join(apt_pkg.config.find_dir("Dir"), + license_key_path.lstrip("/")) + + # Check content of the key + if (license_key.strip().startswith("#!") or + license_key.startswith("\x7fELF")): + raise TransactionFailed(ERROR_LICENSE_KEY_INSTALL_FAILED, + _("The license key is not allowed to " + "contain executable code.")) + # Check the path of the license + license_key_path = os.path.normpath(license_key_path) + license_key_path_rootdir = os.path.join( + apt_pkg.config["Dir"], self.LICENSE_KEY_ROOTDIR.lstrip("/"), + pkg_name) + if not license_key_path.startswith(license_key_path_rootdir): + raise TransactionFailed(ERROR_LICENSE_KEY_INSTALL_FAILED, + _("The license key path %s is invalid"), + license_key_path) + if os.path.lexists(license_key_path): + raise TransactionFailed(ERROR_LICENSE_KEY_INSTALL_FAILED, + _("The license key already exists: %s"), + license_key_path) + # Symlink attacks! + if os.path.realpath(license_key_path) != license_key_path: + raise TransactionFailed(ERROR_LICENSE_KEY_INSTALL_FAILED, + _("The location of the license key is " + "unsecure since it contains symbolic " + "links. The path %s maps to %s"), + license_key_path, + os.path.realpath(license_key_path)) + # Check if the directory already exists + if not os.path.isdir(os.path.dirname(license_key_path)): + raise TransactionFailed(ERROR_LICENSE_KEY_INSTALL_FAILED, + _("The directory where to install the key " + "to doesn't exist yet: %s"), + license_key_path) + # write it + log.info("Writing license key to '%s'" % license_key_path) + old_umask = os.umask(18) + try: + with open(license_key_path, "w") as license_file: + license_file.write(license_key) + except IOError: + raise TransactionFailed(ERROR_LICENSE_KEY_INSTALL_FAILED, + _("Failed to write key file to: %s"), + license_key_path) + finally: + os.umask(old_umask) + + def _iterate_mainloop(self): + """Process pending actions on the main loop.""" + while GLib.main_context_default().pending(): + GLib.main_context_default().iteration() + + def _iterate_packages(self, interval=1000): + """Itarte von the packages of the cache and iterate on the + GObject main loop time for more responsiveness. + + Keyword arguments: + interval - the number of packages after which we iterate on the + mainloop + """ + for enum, pkg in enumerate(self._cache): + if not enum % interval: + self._iterate_mainloop() + yield pkg + + def _get_broken_details(self, trans, now=True): + """Return a message which provides debugging information about + broken packages. + + This method is basically a Python implementation of apt-get.cc's + ShowBroken. + + Keyword arguments: + trans -- the corresponding transaction + now -- if we check currently broken dependecies or the installation + candidate + """ + msg = trans.gettext("The following packages have unmet dependencies:") + msg += "\n\n" + for pkg in self._cache: + if not ((now and pkg.is_now_broken) or + (not now and pkg.is_inst_broken)): + continue + msg += "%s: " % pkg.name + if now: + version = pkg.installed + else: + version = pkg.candidate + indent = " " * (len(pkg.name) + 2) + dep_msg = "" + for dep in version.dependencies: + or_msg = "" + for base_dep in dep.or_dependencies: + if or_msg: + or_msg += "or\n" + or_msg += indent + # Check if it's an important dependency + # See apt-pkg/depcache.cc IsImportantDep + # See apt-pkg/pkgcache.cc IsCritical() + if not (base_dep.rawtype in ["Depends", "PreDepends", + "Obsoletes", "DpkgBreaks", + "Conflicts"] or + (apt_pkg.config.find_b("APT::Install-Recommends", + False) and + base_dep.rawtype == "Recommends") or + (apt_pkg.config.find_b("APT::Install-Suggests", + False) and + base_dep.rawtype == "Suggests")): + continue + # Get the version of the target package + try: + pkg_dep = self._cache[base_dep.name] + except KeyError: + dep_version = None + else: + if now: + dep_version = pkg_dep.installed + else: + dep_version = pkg_dep.candidate + # We only want to display dependencies which cannot + # be satisfied + if dep_version and not apt_pkg.check_dep(base_dep.version, + base_dep.relation, + version.version): + break + or_msg = "%s: %s " % (base_dep.rawtype, base_dep.name) + if base_dep.version: + or_msg += "(%s %s) " % (base_dep.relation, + base_dep.version) + if self._cache.is_virtual_package(base_dep.name): + or_msg += trans.gettext("but it is a virtual package") + elif not dep_version: + if now: + or_msg += trans.gettext("but it is not installed") + else: + or_msg += trans.gettext("but it is not going to " + "be installed") + elif now: + # TRANSLATORS: %s is a version number + or_msg += (trans.gettext("but %s is installed") % + dep_version.version) + else: + # TRANSLATORS: %s is a version number + or_msg += (trans.gettext("but %s is to be installed") % + dep_version.version) + else: + # Only append an or-group if at least one of the + # dependencies cannot be satisfied + if dep_msg: + dep_msg += indent + dep_msg += or_msg + dep_msg += "\n" + msg += dep_msg + return msg + + def is_reboot_required(self): + """If a reboot is required to get all changes into effect.""" + return os.path.exists(os.path.join(apt_pkg.config.find_dir("Dir"), + "var/run/reboot-required")) + + def set_config(self, option, value, filename=None): + """Write a configuration value to file.""" + if option in ["AutoUpdateInterval", "AutoDownload", + "AutoCleanInterval", "UnattendedUpgrade"]: + self._set_apt_config(option, value, filename) + elif option == "PopConParticipation": + self._set_popcon_pariticipation(value) + + def _set_apt_config(self, option, value, filename): + config_writer = ConfigWriter() + cw.set_value(option, value, filename) + apt_pkg.init_config() + + def _set_popcon_participation(self, participate): + if participate in [True, 1, "yes"]: + value = "yes" + else: + value = "no" + if os.path.exists(_POPCON_PATH): + # read the current config and replace the corresponding settings + # FIXME: Check if the config file is a valid bash script and + # contains the host_id + with open(_POPCON_PATH) as conf_file: + old_config = conf_file.read() + config = re.sub(r'(PARTICIPATE=*)(".+?")', + '\\1"%s"' % value, + old_config) + else: + # create a new popcon config file + m = md5() + m.update(open("/dev/urandom", "r").read(1024)) + config = _POPCON_DEFAULT % {"host_id": m.hexdigest(), + "participate": value} + + with open(_POPCON_PATH, "w") as conf_file: + conf_file.write(config) + + def get_config(self, option): + """Return a configuration value.""" + if option == "AutoUpdateInterval": + key = "APT::Periodic::Update-Package-Lists" + return apt_pkg.config.find_i(key, 0) + elif option == "AutoDownload": + key = "APT::Periodic::Download-Upgradeable-Packages" + return apt_pkg.config.find_b(key, False) + elif option == "AutoCleanInterval": + key = "APT::Periodic::AutocleanInterval" + return apt_pkg.config.find_i(key, 0) + elif option == "UnattendedUpgrade": + key = "APT::Periodic::Unattended-Upgrade" + return apt_pkg.config.find_b(key, False) + elif option == "GetPopconParticipation": + return self._get_popcon_pariticipation() + + def _get_popcon_participation(self): + # FIXME: Use a script to evaluate the configuration: + # #!/bin/sh + # . /etc/popularitiy-contest.conf + # . /usr/share/popularitiy-contest/default.conf + # echo $PARTICIAPTE $HOST_ID + if os.path.exists(_POPCON_PATH): + with open(_POPCON_PATH) as conf_file: + config = conf_file.read() + match = re.match("\nPARTICIPATE=\"(yes|no)\"", config) + if match and match[0] == "yes": + return True + return False + + def get_trusted_vendor_keys(self): + """Return a list of trusted GPG keys.""" + return [key.keyid for key in apt.auth.list_keys()] + + +# vim:ts=4:sw=4:et diff --git a/aptdaemon/worker/pkworker.py b/aptdaemon/worker/pkworker.py new file mode 100644 index 0000000..addfe9a --- /dev/null +++ b/aptdaemon/worker/pkworker.py @@ -0,0 +1,1353 @@ +# !/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Provides a compatibility layer to PackageKit + +Copyright (C) 2007 Ali Sabil <ali.sabil@gmail.com> +Copyright (C) 2007 Tom Parker <palfrey@tevp.net> +Copyright (C) 2008-2011 Sebastian Heinlein <glatzor@ubuntu.com> + +Licensed under the GNU General Public License Version 2 + +This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" + +import datetime +import glob +import gzip +import locale +import logging +import os +import platform +import re +import subprocess +import tempfile +import time + +import apt_pkg + + +import gi +gi.require_version('PackageKitGlib', '1.0') + +from gi.repository import GObject +from gi.repository import PackageKitGlib as pk + +# for optional plugin support +try: + import pkg_resources +except ImportError: + pkg_resources = None + +from ..pkutils import (bitfield_add, bitfield_remove, bitfield_summarize, + bitfield_contains) +from . import enums as aptd_enums +from ..errors import TransactionFailed +from ..progress import DaemonAcquireProgress +from . import aptworker + + +pklog = logging.getLogger("AptDaemon.PackageKitWorker") + +# Check if update-manager-core is installed to get aware of the +# latest distro releases +try: + from UpdateManager.Core.MetaRelease import MetaReleaseCore +except ImportError: + META_RELEASE_SUPPORT = False +else: + META_RELEASE_SUPPORT = True + +# Xapian database is optionally used to speed up package description search +XAPIAN_DB_PATH = os.environ.get("AXI_DB_PATH", "/var/lib/apt-xapian-index") +XAPIAN_DB = XAPIAN_DB_PATH + "/index" +XAPIAN_DB_VALUES = XAPIAN_DB_PATH + "/values" +XAPIAN_SUPPORT = False +try: + import xapian +except ImportError: + pass +else: + if os.access(XAPIAN_DB, os.R_OK): + pklog.debug("Use XAPIAN for the search") + XAPIAN_SUPPORT = True + +# Regular expressions to detect bug numbers in changelogs according to the +# Debian Policy Chapter 4.4. For details see the footnote 16: +# http://www.debian.org/doc/debian-policy/footnotes.html#f16 +MATCH_BUG_CLOSES_DEBIAN = ( + r"closes:\s*(?:bug)?\#?\s?\d+(?:,\s*(?:bug)?\#?\s?\d+)*") +MATCH_BUG_NUMBERS = r"\#?\s?(\d+)" +# URL pointing to a bug in the Debian bug tracker +HREF_BUG_DEBIAN = "http://bugs.debian.org/cgi-bin/bugreport.cgi?bug=%s" + +MATCH_BUG_CLOSES_UBUNTU = r"lp:\s+\#\d+(?:,\s*\#\d+)*" +HREF_BUG_UBUNTU = "https://bugs.launchpad.net/bugs/%s" + +# Regular expression to find cve references +MATCH_CVE = "CVE-\d{4}-\d{4}" +HREF_CVE = "http://web.nvd.nist.gov/view/vuln/detail?vulnId=%s" + +# Map Debian sections to the PackageKit group name space +SECTION_GROUP_MAP = { + "admin": pk.GroupEnum.ADMIN_TOOLS, + "base": pk.GroupEnum.SYSTEM, + "cli-mono": pk.GroupEnum.PROGRAMMING, + "comm": pk.GroupEnum.COMMUNICATION, + "database": pk.GroupEnum.SERVERS, + "debian-installer": pk.GroupEnum.SYSTEM, + "debug": pk.GroupEnum.PROGRAMMING, + "devel": pk.GroupEnum.PROGRAMMING, + "doc": pk.GroupEnum.DOCUMENTATION, + "editors": pk.GroupEnum.PUBLISHING, + "education": pk.GroupEnum.EDUCATION, + "electronics": pk.GroupEnum.ELECTRONICS, + "embedded": pk.GroupEnum.SYSTEM, + "fonts": pk.GroupEnum.FONTS, + "games": pk.GroupEnum.GAMES, + "gnome": pk.GroupEnum.DESKTOP_GNOME, + "gnu-r": pk.GroupEnum.PROGRAMMING, + "gnustep": pk.GroupEnum.DESKTOP_OTHER, + "graphics": pk.GroupEnum.GRAPHICS, + "hamradio": pk.GroupEnum.COMMUNICATION, + "haskell": pk.GroupEnum.PROGRAMMING, + "httpd": pk.GroupEnum.SERVERS, + "interpreters": pk.GroupEnum.PROGRAMMING, + "introspection": pk.GroupEnum.PROGRAMMING, + "java": pk.GroupEnum.PROGRAMMING, + "kde": pk.GroupEnum.DESKTOP_KDE, + "kernel": pk.GroupEnum.SYSTEM, + "libdevel": pk.GroupEnum.PROGRAMMING, + "libs": pk.GroupEnum.SYSTEM, + "lisp": pk.GroupEnum.PROGRAMMING, + "localization": pk.GroupEnum.LOCALIZATION, + "mail": pk.GroupEnum.INTERNET, + "math": pk.GroupEnum.SCIENCE, + "misc": pk.GroupEnum.OTHER, + "net": pk.GroupEnum.NETWORK, + "news": pk.GroupEnum.INTERNET, + "ocaml": pk.GroupEnum.PROGRAMMING, + "oldlibs": pk.GroupEnum.LEGACY, + "otherosfs": pk.GroupEnum.SYSTEM, + "perl": pk.GroupEnum.PROGRAMMING, + "php": pk.GroupEnum.PROGRAMMING, + "python": pk.GroupEnum.PROGRAMMING, + "ruby": pk.GroupEnum.PROGRAMMING, + "science": pk.GroupEnum.SCIENCE, + "shells": pk.GroupEnum.ADMIN_TOOLS, + "sound": pk.GroupEnum.MULTIMEDIA, + "tex": pk.GroupEnum.PUBLISHING, + "text": pk.GroupEnum.PUBLISHING, + "utils": pk.GroupEnum.ACCESSORIES, + "vcs": pk.GroupEnum.PROGRAMMING, + "video": pk.GroupEnum.MULTIMEDIA, + "virtual": pk.GroupEnum.COLLECTIONS, + "web": pk.GroupEnum.INTERNET, + "xfce": pk.GroupEnum.DESKTOP_OTHER, + "x11": pk.GroupEnum.DESKTOP_OTHER, + "zope": pk.GroupEnum.PROGRAMMING, + "unknown": pk.GroupEnum.UNKNOWN, + "alien": pk.GroupEnum.UNKNOWN, + "translations": pk.GroupEnum.LOCALIZATION, + "metapackages": pk.GroupEnum.COLLECTIONS, + "tasks": pk.GroupEnum.COLLECTIONS} + + +class AptPackageKitWorker(aptworker.AptWorker): + + _plugins = None + + """Process PackageKit Query transactions.""" + + def __init__(self, chroot=None, load_plugins=True): + aptworker.AptWorker.__init__(self, chroot, load_plugins) + + self.roles = bitfield_summarize(pk.RoleEnum.REFRESH_CACHE, + pk.RoleEnum.UPDATE_PACKAGES, + pk.RoleEnum.INSTALL_PACKAGES, + pk.RoleEnum.INSTALL_FILES, + pk.RoleEnum.REMOVE_PACKAGES, + pk.RoleEnum.GET_UPDATES, + pk.RoleEnum.GET_UPDATE_DETAIL, + pk.RoleEnum.GET_PACKAGES, + pk.RoleEnum.GET_DETAILS, + pk.RoleEnum.SEARCH_NAME, + pk.RoleEnum.SEARCH_DETAILS, + pk.RoleEnum.SEARCH_GROUP, + pk.RoleEnum.SEARCH_FILE, + pk.RoleEnum.WHAT_PROVIDES, + pk.RoleEnum.REPO_ENABLE, + pk.RoleEnum.INSTALL_SIGNATURE, + pk.RoleEnum.REPAIR_SYSTEM, + pk.RoleEnum.CANCEL, + pk.RoleEnum.DOWNLOAD_PACKAGES) + if META_RELEASE_SUPPORT: + self.roles = bitfield_add(self.roles, + pk.RoleEnum.GET_DISTRO_UPGRADES) + self.filters = bitfield_summarize(pk.FilterEnum.INSTALLED, + pk.FilterEnum.NOT_INSTALLED, + pk.FilterEnum.FREE, + pk.FilterEnum.NOT_FREE, + pk.FilterEnum.GUI, + pk.FilterEnum.NOT_GUI, + pk.FilterEnum.COLLECTIONS, + pk.FilterEnum.NOT_COLLECTIONS, + pk.FilterEnum.SUPPORTED, + pk.FilterEnum.NOT_SUPPORTED, + pk.FilterEnum.ARCH, + pk.FilterEnum.NOT_ARCH, + pk.FilterEnum.NEWEST) + self.groups = bitfield_summarize(*SECTION_GROUP_MAP.values()) + # FIXME: Add support for Plugins + self.provides = (pk.ProvidesEnum.ANY) + self.mime_types = ["application/x-deb"] + + def _run_transaction(self, trans): + if (hasattr(trans, "pktrans") and + bitfield_contains(trans.pktrans.flags, + pk.TransactionFlagEnum.SIMULATE)): + self._simulate_and_emit_packages(trans) + return False + else: + return aptworker.AptWorker._run_transaction(self, trans) + + def _simulate_and_emit_packages(self, trans): + trans.status = aptd_enums.STATUS_RUNNING + + self._simulate_transaction_idle(trans) + + # The simulate method lets the transaction fail in the case of an + # error + if trans.exit == aptd_enums.EXIT_UNFINISHED: + # It is a little bit complicated to get the packages but avoids + # a larger refactoring of apt.AptWorker._simulate() + for pkg in trans.depends[aptd_enums.PKGS_INSTALL]: + self._emit_package(trans, + self._cache[self._split_package_id(pkg)[0]], + pk.InfoEnum.INSTALLING) + for pkg in trans.depends[aptd_enums.PKGS_REINSTALL]: + self._emit_package(trans, + self._cache[self._split_package_id(pkg)[0]], + pk.InfoEnum.REINSTALLING) + for pkg in trans.depends[aptd_enums.PKGS_REMOVE]: + self._emit_package(trans, + self._cache[self._split_package_id(pkg)[0]], + pk.InfoEnum.REMOVING) + for pkg in trans.depends[aptd_enums.PKGS_PURGE]: + self._emit_package(trans, + self._cache[self._split_package_id(pkg)[0]], + pk.InfoEnum.REMOVING) + for pkg in trans.depends[aptd_enums.PKGS_UPGRADE]: + self._emit_package(trans, + self._cache[self._split_package_id(pkg)[0]], + pk.InfoEnum.UPDATING, force_candidate=True) + for pkg in trans.depends[aptd_enums.PKGS_DOWNGRADE]: + self._emit_package(trans, + self._cache[self._split_package_id(pkg)[0]], + pk.InfoEnum.DOWNGRADING, + force_candidate=True) + for pkg in trans.depends[aptd_enums.PKGS_KEEP]: + self._emit_package(trans, + self._cache[self._split_package_id(pkg)[0]], + pk.InfoEnum.BLOCKED, force_candidate=True) + for pkg in trans.unauthenticated: + self._emit_package(trans, self._cache[pkg], + pk.InfoEnum.UNTRUSTED, force_candidate=True) + trans.status = aptd_enums.STATUS_FINISHED + trans.progress = 100 + trans.exit = aptd_enums.EXIT_SUCCESS + tid = trans.tid[:] + self.trans = None + self.marked_tid = None + self._emit_transaction_done(trans) + pklog.info("Finished transaction %s", tid) + + def query(self, trans): + """Run the worker""" + if trans.role != aptd_enums.ROLE_PK_QUERY: + raise TransactionFailed(aptd_enums.ERROR_UNKNOWN, + "The transaction doesn't seem to be " + "a query") + if trans.pktrans.role == pk.RoleEnum.RESOLVE: + self.resolve(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.GET_UPDATES: + self.get_updates(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.GET_UPDATE_DETAIL: + self.get_update_detail(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.GET_PACKAGES: + self.get_packages(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.GET_FILES: + self.get_files(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.SEARCH_NAME: + self.search_names(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.SEARCH_GROUP: + self.search_groups(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.SEARCH_DETAILS: + self.search_details(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.SEARCH_FILE: + self.search_files(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.GET_DETAILS: + self.get_details(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.DOWNLOAD_PACKAGES: + self.download_packages(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.WHAT_PROVIDES: + self.what_provides(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.REPO_ENABLE: + self.repo_enable(trans, **trans.kwargs) + elif trans.pktrans.role == pk.RoleEnum.INSTALL_SIGNATURE: + self.install_signature(trans, **trans.kwargs) + else: + raise TransactionFailed(aptd_enums.ERROR_UNKNOWN, + "Role %s isn't supported", + trans.pktrans.role) + + def search_files(self, trans, filters, values): + """Implement org.freedesktop.PackageKit.Transaction.SearchFiles() + + Works only for installed file if apt-file isn't installed. + """ + trans.progress = 101 + + result_names = set() + # Optionally make use of apt-file's Contents cache to search for not + # installed files. But still search for installed files additionally + # to make sure that we provide up-to-date results + if (os.path.exists("/usr/bin/apt-file") and + not bitfield_contains(filters, pk.FilterEnum.INSTALLED)): + # FIXME: use rapt-file on Debian if the network is available + # FIXME: Show a warning to the user if the apt-file cache is + # several weeks old + pklog.debug("Using apt-file") + filenames_regex = [] + for filename in values: + if filename.startswith("/"): + pattern = "^%s$" % filename[1:].replace("/", "\/") + else: + pattern = "\/%s$" % filename + filenames_regex.append(pattern) + cmd = ["/usr/bin/apt-file", "--regexp", "--non-interactive", + "--package-only", "find", "|".join(filenames_regex)] + pklog.debug("Calling: %s" % cmd) + apt_file = subprocess.Popen(cmd, stdout=subprocess.PIPE, + stderr=subprocess.PIPE) + stdout, stderr = apt_file.communicate() + if apt_file.returncode == 0: + # FIXME: Actually we should check if the file is part of the + # candidate, e.g. if unstable and experimental are + # enabled and a file would only be part of the + # experimental version + result_names.update(stdout.split()) + self._emit_visible_packages_by_name(trans, filters, + result_names) + else: + raise TransactionFailed(aptd_enums.ERROR_INTERNAL_ERROR, + "%s %s" % (stdout, stderr)) + # Search for installed files + filenames_regex = [] + for filename in values: + if filename.startswith("/"): + pattern = "^%s$" % filename.replace("/", "\/") + else: + pattern = ".*\/%s$" % filename + filenames_regex.append(pattern) + files_pattern = re.compile("|".join(filenames_regex)) + for pkg in self._iterate_packages(): + if pkg.name in result_names: + continue + for installed_file in self._get_installed_files(pkg): + if files_pattern.match(installed_file): + self._emit_visible_package(trans, filters, pkg) + break + + def search_groups(self, trans, filters, values): + """Implement org.freedesktop.PackageKit.Transaction.SearchGroups()""" + # FIXME: Handle repo and category search + trans.progress = 101 + + for pkg in self._iterate_packages(): + group_str = pk.group_enum_to_string(self._get_package_group(pkg)) + if group_str in values: + self._emit_visible_package(trans, filters, pkg) + + def search_names(self, trans, filters, values): + """Implement org.freedesktop.PackageKit.Transaction.SearchNames()""" + def matches(searches, text): + for search in searches: + if search not in text: + return False + return True + trans.progress = 101 + + for pkg_name in list(self._cache.keys()): + if matches(values, pkg_name): + self._emit_all_visible_pkg_versions(trans, filters, + self._cache[pkg_name]) + + def search_details(self, trans, filters, values): + """Implement org.freedesktop.PackageKit.Transaction.SearchDetails()""" + trans.progress = 101 + + if XAPIAN_SUPPORT is True: + search_flags = (xapian.QueryParser.FLAG_BOOLEAN | + xapian.QueryParser.FLAG_PHRASE | + xapian.QueryParser.FLAG_LOVEHATE | + xapian.QueryParser.FLAG_BOOLEAN_ANY_CASE) + pklog.debug("Performing xapian db based search") + db = xapian.Database(XAPIAN_DB) + parser = xapian.QueryParser() + parser.set_default_op(xapian.Query.OP_AND) + query = parser.parse_query(" ".join(values), search_flags) + enquire = xapian.Enquire(db) + enquire.set_query(query) + matches = enquire.get_mset(0, 1000) + for pkg_name in (match.document.get_data() + for match in enquire.get_mset(0, 1000)): + if pkg_name in self._cache: + self._emit_visible_package(trans, filters, + self._cache[pkg_name]) + else: + def matches(searches, text): + for search in searches: + if search not in text: + return False + return True + pklog.debug("Performing apt cache based search") + values = [val.lower() for val in values] + for pkg in self._iterate_packages(): + txt = pkg.name + try: + txt += pkg.candidate.raw_description.lower() + txt += pkg.candidate._translated_records.long_desc.lower() + except AttributeError: + pass + if matches(values, txt): + self._emit_visible_package(trans, filters, pkg) + + def get_updates(self, trans, filters): + """Only report updates which can be installed safely: Which can depend + on the installation of additional packages but which don't require + the removal of already installed packages or block any other update. + """ + def succeeds_security_update(pkg): + """ + Return True if an update succeeds a previous security update + + An example would be a package with version 1.1 in the security + archive and 1.1.1 in the archive of proposed updates or the + same version in both archives. + """ + for version in pkg.versions: + # Only check versions between the installed and the candidate + if (pkg.installed and + apt_pkg.version_compare(version.version, + pkg.installed.version) <= 0 and + apt_pkg.version_compare(version.version, + pkg.candidate.version) > 0): + continue + for origin in version.origins: + if (origin.origin in ["Debian", "Ubuntu"] and + (origin.archive.endswith("-security") or + origin.label == "Debian-Security") and + origin.trusted): + return True + return False + # FIXME: Implment the basename filter + self.cancellable = False + self.progress = 101 + # Start with a safe upgrade + try: + self._cache.upgrade(dist_upgrade=True) + except SystemError: + pass + for pkg in self._iterate_packages(): + if not pkg.is_upgradable: + continue + # This may occur on pinned packages which have been updated to + # later version than the pinned one + if not pkg.candidate.origins: + continue + if not pkg.marked_upgrade: + # FIXME: Would be nice to all show why + self._emit_package(trans, pkg, pk.InfoEnum.BLOCKED, + force_candidate=True) + continue + # The update can be safely installed + info = pk.InfoEnum.NORMAL + # Detect the nature of the upgrade (e.g. security, enhancement) + candidate_origin = pkg.candidate.origins[0] + archive = candidate_origin.archive + origin = candidate_origin.origin + trusted = candidate_origin.trusted + label = candidate_origin.label + if origin in ["Debian", "Ubuntu"] and trusted is True: + if archive.endswith("-security") or label == "Debian-Security": + info = pk.InfoEnum.SECURITY + elif succeeds_security_update(pkg): + pklog.debug("Update of %s succeeds a security update. " + "Raising its priority." % pkg.name) + info = pk.InfoEnum.SECURITY + elif archive.endswith("-backports"): + info = pk.InfoEnum.ENHANCEMENT + elif archive.endswith("-updates"): + info = pk.InfoEnum.BUGFIX + if origin in ["Backports.org archive"] and trusted is True: + info = pk.InfoEnum.ENHANCEMENT + self._emit_package(trans, pkg, info, force_candidate=True) + self._emit_require_restart(trans) + + def _emit_require_restart(self, trans): + """Emit RequireRestart if required.""" + # Check for a system restart + if self.is_reboot_required(): + trans.pktrans.RequireRestart(pk.RestartEnum.SYSTEM, "") + + def get_update_detail(self, trans, package_ids): + """ + Implement the {backend}-get-update-details functionality + """ + def get_bug_urls(changelog): + """ + Create a list of urls pointing to closed bugs in the changelog + """ + urls = [] + for r in re.findall(MATCH_BUG_CLOSES_DEBIAN, changelog, + re.IGNORECASE | re.MULTILINE): + urls.extend([HREF_BUG_DEBIAN % bug for bug in + re.findall(MATCH_BUG_NUMBERS, r)]) + for r in re.findall(MATCH_BUG_CLOSES_UBUNTU, changelog, + re.IGNORECASE | re.MULTILINE): + urls.extend([HREF_BUG_UBUNTU % bug for bug in + re.findall(MATCH_BUG_NUMBERS, r)]) + return urls + + def get_cve_urls(changelog): + """ + Create a list of urls pointing to cves referred in the changelog + """ + return [HREF_CVE % c for c in re.findall(MATCH_CVE, changelog, + re.MULTILINE)] + + trans.progress = 0 + trans.cancellable = False + trans.pktrans.status = pk.StatusEnum.DOWNLOAD_CHANGELOG + total = len(package_ids) + count = 1 + old_locale = locale.getlocale(locale.LC_TIME) + locale.setlocale(locale.LC_TIME, "C") + for pkg_id in package_ids: + self._iterate_mainloop() + trans.progress = count * 100 / total + count += 1 + pkg = self._get_package_by_id(pkg_id) + # FIXME add some real data + if pkg.installed.origins: + installed_origin = pkg.installed.origins[0].label + # APT returns a str with Python 2 + if isinstance(installed_origin, bytes): + installed_origin = installed_origin.decode("UTF-8") + else: + installed_origin = "" + updates = ["%s;%s;%s;%s" % (pkg.name, pkg.installed.version, + pkg.installed.architecture, + installed_origin)] + # Get the packages which will be replaced by the update + obsoletes = set() + if pkg.candidate: + for dep_group in pkg.candidate.get_dependencies("Replaces"): + for dep in dep_group: + try: + obs = self._cache[dep.name] + except KeyError: + continue + if not obs.installed: + continue + if dep.relation: + cmp = apt_pkg.version_compare( + obs.candidate.version, + dep.version) + # Same version + if cmp == 0 and dep.relation in [">", "<"]: + continue + # Installed version higer + elif cmp < 0 and dep.relation in ["<"]: + continue + # Installed version lower + elif cmp > 0 and dep.relation in [">"]: + continue + obs_id = self._get_id_from_version(obs.installed) + obsoletes.add(obs_id) + vendor_urls = [] + restart = pk.RestartEnum.NONE + update_text = "" + state = pk.UpdateStateEnum.UNKNOWN + # FIXME: Add support for Ubuntu and a better one for Debian + if (pkg.candidate and pkg.candidate.origins[0].trusted and + pkg.candidate.origins[0].label == "Debian"): + archive = pkg.candidate.origins[0].archive + if archive == "stable": + state = pk.UpdateStateEnum.STABLE + elif archive == "testing": + state = pk.UpdateStateEnum.TESTING + elif archive == "unstable": + state = pk.UpdateStateEnum.UNSTABLE + issued = "" + updated = "" + # FIXME: make this more configurable. E.g. a dbus update requires + # a reboot on Ubuntu but not on Debian + if (pkg.name.startswith("linux-image-") or + pkg.name in ["libc6", "dbus"]): + restart == pk.RestartEnum.SYSTEM + changelog_dir = apt_pkg.config.find_dir("Dir::Cache::Changelogs") + if changelog_dir == "/": + changelog_dir = os.path.join(apt_pkg.config.find_dir("Dir::" + "Cache"), + "Changelogs") + filename = os.path.join(changelog_dir, + "%s_%s.gz" % (pkg.name, + pkg.candidate.version)) + changelog_raw = "" + if os.path.exists(filename): + pklog.debug("Reading changelog from cache") + changelog_file = gzip.open(filename, "rb") + try: + changelog_raw = changelog_file.read().decode("UTF-8") + except: + pass + finally: + changelog_file.close() + if not changelog_raw: + pklog.debug("Downloading changelog") + changelog_raw = pkg.get_changelog() + # The internal download error string of python-apt ist not + # provided as unicode object + if not isinstance(changelog_raw, str): + changelog_raw = changelog_raw.decode("UTF-8") + # Cache the fetched changelog + if not os.path.exists(changelog_dir): + os.makedirs(changelog_dir) + # Remove old cached changelogs + pattern = os.path.join(changelog_dir, "%s_*" % pkg.name) + for old_changelog in glob.glob(pattern): + os.remove(os.path.join(changelog_dir, old_changelog)) + changelog_file = gzip.open(filename, mode="wb") + try: + changelog_file.write(changelog_raw.encode("UTF-8")) + finally: + changelog_file.close() + # Convert the changelog to markdown syntax + changelog = "" + for line in changelog_raw.split("\n"): + if line == "": + changelog += " \n" + else: + changelog += " %s \n" % line + if line.startswith(pkg.candidate.source_name): + match = re.match(r"(?P<source>.+) \((?P<version>.*)\) " + "(?P<dist>.+); urgency=(?P<urgency>.+)", + line) + update_text += "%s\n%s\n\n" % (match.group("version"), + "=" * + len(match.group("version"))) + elif line.startswith(" "): + # FIXME: The GNOME PackageKit markup parser doesn't support + # monospaced yet + # update_text += " %s \n" % line + update_text += "%s\n\n" % line + elif line.startswith(" --"): + # FIXME: Add %z for the time zone - requires Python 2.6 + update_text += " \n" + match = re.match("^ -- (?P<maintainer>.+) (?P<mail><.+>) " + "(?P<date>.+) (?P<offset>[-\+][0-9]+)$", + line) + if not match: + continue + try: + date = datetime.datetime.strptime(match.group("date"), + "%a, %d %b %Y " + "%H:%M:%S") + except ValueError: + continue + issued = date.isoformat() + if not updated: + updated = date.isoformat() + if issued == updated: + updated = "" + bugzilla_urls = get_bug_urls(changelog) + cve_urls = get_cve_urls(changelog) + trans.emit_update_detail(pkg_id, updates, obsoletes, vendor_urls, + bugzilla_urls, cve_urls, restart, + update_text, changelog, + state, issued, updated) + locale.setlocale(locale.LC_TIME, old_locale) + + def get_details(self, trans, package_ids): + """Implement org.freedesktop.PackageKit.Transaction.GetDetails()""" + trans.progress = 101 + + for pkg_id in package_ids: + version = self._get_version_by_id(pkg_id) + # FIXME: We need more fine grained license information! + origins = version.origins + if (origins and + origins[0].component in ["main", "universe"] and + origins[0].origin in ["Debian", "Ubuntu"]): + license = "free" + else: + license = "unknown" + group = self._get_package_group(version.package) + trans.emit_details(pkg_id, license, group, version.description, + version.homepage, version.size) + + def get_packages(self, trans, filters): + """Implement org.freedesktop.PackageKit.Transaction.GetPackages()""" + self.progress = 101 + + for pkg in self._iterate_packages(): + if self._is_package_visible(pkg, filters): + self._emit_package(trans, pkg) + + def resolve(self, trans, filters, packages): + """Implement org.freedesktop.PackageKit.Transaction.Resolve()""" + trans.status = aptd_enums.STATUS_QUERY + trans.progress = 101 + self.cancellable = False + + for name in packages: + try: + # Check if the name is a valid package id + version = self._get_version_by_id(name) + except ValueError: + pass + else: + if self._is_version_visible(version, filters): + self._emit_pkg_version(trans, version) + continue + # The name seems to be a normal name + try: + pkg = self._cache[name] + except KeyError: + raise TransactionFailed(aptd_enums.ERROR_NO_PACKAGE, + "Package name %s could not be " + "resolved.", name) + else: + self._emit_all_visible_pkg_versions(trans, filters, pkg) + + def get_depends(self, trans, filters, package_ids, recursive): + """Emit all dependencies of the given package ids. + + Doesn't support recursive dependency resolution. + """ + def emit_blocked_dependency(base_dependency, pkg=None, + filters=pk.FilterEnum.NONE): + """Send a blocked package signal for the given + apt.package.BaseDependency. + """ + if pk.FilterEnum.INSTALLED in filters: + return + if pkg: + summary = pkg.candidate.summary + filters = bitfield_remove(filters, pk.FilterEnum.NOT_INSTALLED) + if not self._is_package_visible(pkg, filters): + return + else: + summary = "" + if base_dependency.relation: + version = "%s%s" % (base_dependency.relation, + base_dependency.version) + else: + version = base_dependency.version + trans.emit_package("%s;%s;;" % (base_dependency.name, version), + pk.InfoEnum.BLOCKED, summary) + + def check_dependency(pkg, base_dep): + """Check if the given apt.package.Package can satisfy the + BaseDepenendcy and emit the corresponding package signals. + """ + if not self._is_package_visible(pkg, filters): + return + if base_dep.version: + satisfied = False + # Sort the version list to check the installed + # and candidate before the other ones + ver_list = list(pkg.versions) + if pkg.installed: + ver_list.remove(pkg.installed) + ver_list.insert(0, pkg.installed) + if pkg.candidate: + ver_list.remove(pkg.candidate) + ver_list.insert(0, pkg.candidate) + for dep_ver in ver_list: + if apt_pkg.check_dep(dep_ver.version, + base_dep.relation, + base_dep.version): + self._emit_pkg_version(trans, dep_ver) + satisfied = True + break + if not satisfied: + emit_blocked_dependency(base_dep, pkg, filters) + else: + self._emit_package(trans, pkg) + + # Setup the transaction + self.status = aptd_enums.STATUS_RESOLVING_DEP + trans.progress = 101 + self.cancellable = True + + dependency_types = ["PreDepends", "Depends"] + if apt_pkg.config["APT::Install-Recommends"]: + dependency_types.append("Recommends") + for id in package_ids: + version = self._get_version_by_id(id) + for dependency in version.get_dependencies(*dependency_types): + # Walk through all or_dependencies + for base_dep in dependency.or_dependencies: + if self._cache.is_virtual_package(base_dep.name): + # Check each proivider of a virtual package + for provider in self._cache.get_providing_packages( + base_dep.name): + check_dependency(provider, base_dep) + elif base_dep.name in self._cache: + check_dependency(self._cache[base_dep.name], base_dep) + else: + # The dependency does not exist + emit_blocked_dependency(trans, base_dep, + filters=filters) + + def get_requires(self, trans, filters, package_ids, recursive): + """Emit all packages which depend on the given ids. + + Recursive searching is not supported. + """ + self.status = aptd_enums.STATUS_RESOLVING_DEP + self.progress = 101 + self.cancellable = True + for id in package_ids: + version = self._get_version_by_id(id) + for pkg in self._iterate_packages(): + if not self._is_package_visible(pkg, filters): + continue + if pkg.is_installed: + pkg_ver = pkg.installed + elif pkg.candidate: + pkg_ver = pkg.candidate + for dependency in pkg_ver.dependencies: + satisfied = False + for base_dep in dependency.or_dependencies: + if (version.package.name == base_dep.name or + base_dep.name in version.provides): + satisfied = True + break + if satisfied: + self._emit_package(trans, pkg) + break + + def download_packages(self, trans, store_in_cache, package_ids): + """Implement the DownloadPackages functionality. + + The store_in_cache parameter gets ignored. + """ + def get_download_details(ids): + """Calculate the start and end point of a package download + progress. + """ + total = 0 + downloaded = 0 + versions = [] + # Check if all ids are vaild and calculate the total download size + for id in ids: + pkg_ver = self._get_version_by_id(id) + if not pkg_ver.downloadable: + raise TransactionFailed( + aptd_enums.ERROR_PACKAGE_DOWNLOAD_FAILED, + "package %s isn't downloadable" % id) + total += pkg_ver.size + versions.append((id, pkg_ver)) + for id, ver in versions: + start = downloaded * 100 / total + end = start + ver.size * 100 / total + yield id, ver, start, end + downloaded += ver.size + trans.status = aptd_enums.STATUS_DOWNLOADING + trans.cancellable = True + trans.progress = 10 + # Check the destination directory + if store_in_cache: + dest = apt_pkg.config.find_dir("Dir::Cache::archives") + else: + dest = tempfile.mkdtemp(prefix="aptdaemon-download") + if not os.path.isdir(dest) or not os.access(dest, os.W_OK): + raise TransactionFailed(aptd_enums.ERROR_INTERNAL_ERROR, + "The directory '%s' is not writable" % + dest) + # Start the download + for id, ver, start, end in get_download_details(package_ids): + progress = DaemonAcquireProgress(trans, start, end) + self._emit_pkg_version(trans, ver, pk.InfoEnum.DOWNLOADING) + try: + ver.fetch_binary(dest, progress) + except Exception as error: + raise TransactionFailed( + aptd_enums.ERROR_PACKAGE_DOWNLOAD_FAILED, str(error)) + else: + path = os.path.join(dest, os.path.basename(ver.filename)) + trans.emit_files(id, [path]) + self._emit_pkg_version(trans, ver, pk.InfoEnum.FINISHED) + + def get_files(self, trans, package_ids): + """Emit the Files signal which includes the files included in a package + Apt only supports this for installed packages + """ + for id in package_ids: + pkg = self._get_package_by_id(id) + trans.emit_files(id, self._get_installed_files(pkg)) + + def what_provides(self, trans, filters, provides_type, values): + """Emit all packages which provide the given type and search value.""" + self._init_plugins() + + supported_type = False + provides_type_str = pk.provides_enum_to_string(provides_type) + + # run plugins + for plugin in self._plugins.get("what_provides", []): + pklog.debug("calling what_provides plugin %s %s", + str(plugin), str(filters)) + for search_item in values: + try: + for package in plugin(self._cache, provides_type_str, + search_item): + self._emit_visible_package(trans, filters, package) + supported_type = True + except NotImplementedError: + # keep supported_type as False + pass + + if not supported_type and provides_type != pk.ProvidesEnum.ANY: + # none of the plugins felt responsible for this type + raise TransactionFailed(aptd_enums.ERROR_NOT_SUPPORTED, + "Query type '%s' is not supported" % + pk.provides_enum_to_string(provides_type)) + + def repo_enable(self, trans, repo_id, enabled): + """Enable or disable a repository.""" + if not enabled: + raise TransactionFailed(aptd_enums.ERROR_NOT_SUPPORTED, + "Disabling repositories is not " + "implemented") + + fields = repo_id.split() + if len(fields) < 3 or fields[0] not in ('deb', 'deb-src'): + raise TransactionFailed( + aptd_enums.ERROR_NOT_SUPPORTED, + "Unknown repository ID format: %s" % repo_id) + + self.add_repository(trans, fields[0], fields[1], fields[2], + fields[3:], '', None) + + def install_signature(self, trans, sig_type, key_id, package_id): + """Install an archive key.""" + if sig_type != pk.SigTypeEnum.GPG: + raise TransactionFailed(aptd_enums.ERROR_NOT_SUPPORTED, + "Type %s is not supported" % sig_type) + try: + keyserver = os.environ["APTDAEMON_KEYSERVER"] + except KeyError: + if platform.dist()[0] == "Ubuntu": + keyserver = "hkp://keyserver.ubuntu.com:80" + else: + keyserver = "hkp://keys.gnupg.net" + self.add_vendor_key_from_keyserver(trans, key_id, keyserver) + + # Helpers + + def _get_id_from_version(self, version): + """Return the package id of an apt.package.Version instance.""" + if version.origins: + origin = version.origins[0].label + # APT returns a str with Python 2 + if isinstance(origin, bytes): + origin = origin.decode("UTF-8") + else: + origin = "" + if version.architecture in [self.NATIVE_ARCH, "all"]: + name = version.package.name + else: + name = version.package.name.split(":")[0] + id = "%s;%s;%s;%s" % (name, version.version, + version.architecture, origin) + return id + + def _emit_package(self, trans, pkg, info=None, force_candidate=False): + """ + Send the Package signal for a given apt package + """ + if (not pkg.is_installed or force_candidate) and pkg.candidate: + self._emit_pkg_version(trans, pkg.candidate, info) + elif pkg.is_installed: + self._emit_pkg_version(trans, pkg.installed, info) + else: + pklog.debug("Package %s hasn't got any version." % pkg.name) + + def _emit_pkg_version(self, trans, version, info=None): + """Emit the Package signal of the given apt.package.Version.""" + id = self._get_id_from_version(version) + section = version.section.split("/")[-1] + if not info: + if version == version.package.installed: + if section == "metapackages": + info = pk.InfoEnum.COLLECTION_INSTALLED + else: + info = pk.InfoEnum.INSTALLED + else: + if section == "metapackages": + info = pk.InfoEnum.COLLECTION_AVAILABLE + else: + info = pk.InfoEnum.AVAILABLE + trans.emit_package(info, id, version.summary) + + def _emit_all_visible_pkg_versions(self, trans, filters, pkg): + """Emit all available versions of a package.""" + for version in pkg.versions: + if self._is_version_visible(version, filters): + self._emit_pkg_version(trans, version) + + def _emit_visible_package(self, trans, filters, pkg, info=None): + """ + Filter and emit a package + """ + if self._is_package_visible(pkg, filters): + self._emit_package(trans, pkg, info) + + def _emit_visible_packages(self, trans, filters, pkgs, info=None): + """ + Filter and emit packages + """ + for pkg in pkgs: + if self._is_package_visible(pkg, filters): + self._emit_package(trans, pkg, info) + + def _emit_visible_packages_by_name(self, trans, filters, pkgs, info=None): + """ + Find the packages with the given namens. Afterwards filter and emit + them + """ + for name in pkgs: + if (name in self._cache and + self._is_package_visible(self._cache[name], filters)): + self._emit_package(trans, self._cache[name], info) + + def _is_version_visible(self, version, filters): + """Return True if the package version is matched by the given + filters. + """ + if filters == pk.FilterEnum.NONE: + return True + if (bitfield_contains(filters, pk.FilterEnum.NEWEST) and + version.package.candidate != version): + return False + if (bitfield_contains(filters, pk.FilterEnum.INSTALLED) and + version.package.installed != version): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_INSTALLED) and + version.package.installed == version): + return False + if (bitfield_contains(filters, pk.FilterEnum.SUPPORTED) and + not self._is_package_supported(version.package)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_SUPPORTED) and + self._is_package_supported(version.package)): + return False + if (bitfield_contains(filters, pk.FilterEnum.FREE) and + not self._is_version_free(version)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_FREE) and + self._is_version_free(version)): + return False + if (bitfield_contains(filters, pk.FilterEnum.GUI) and + not self._has_package_gui(version.package)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_GUI) and + self._has_package_gui(version.package)): + return False + if (bitfield_contains(filters, pk.FilterEnum.COLLECTIONS) and + not self._is_package_collection(version.package)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_COLLECTIONS) and + self._is_package_collection(version.package)): + return False + if (bitfield_contains(filters, pk.FilterEnum.DEVELOPMENT) and + not self._is_package_devel(version.package)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_DEVELOPMENT) and + self._is_package_devel(version.package)): + return False + if (bitfield_contains(filters, pk.FilterEnum.ARCH) and + version.architecture not in [self.NATIVE_ARCH, "all"]): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_ARCH) and + version.architecture in [self.NATIVE_ARCH, "all"]): + return False + return True + + def _is_package_visible(self, pkg, filters): + """Return True if the package is matched by the given filters.""" + if filters == pk.FilterEnum.NONE: + return True + if (bitfield_contains(filters, pk.FilterEnum.INSTALLED) and + not pkg.is_installed): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_INSTALLED) and + pkg.is_installed): + return False + if (bitfield_contains(filters, pk.FilterEnum.SUPPORTED) and + not self._is_package_supported(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_SUPPORTED) and + self._is_package_supported(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.FREE) and + not self._is_package_free(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_FREE) and + self._is_package_free(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.GUI) and + not self._has_package_gui(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_GUI) and + self._has_package_gui(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.COLLECTIONS) and + not self._is_package_collection(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_COLLECTIONS) and + self._is_package_collection(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.DEVELOPMENT) and + not self._is_package_devel(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_DEVELOPMENT) and + self._is_package_devel(pkg)): + return False + if (bitfield_contains(filters, pk.FilterEnum.ARCH) and + ":" in pkg.name): + return False + if (bitfield_contains(filters, pk.FilterEnum.NOT_ARCH) and + ":" not in pkg.name): + return False + return True + + def _is_package_free(self, pkg): + """Return True if we can be sure that the package's license is a + free one + """ + if not pkg.candidate: + return False + return self._is_version_free(pkg.candidate) + + def _is_version_free(self, version): + """Return True if we can be sure that the package version has got + a free license. + """ + origins = version.origins + return (origins and + ((origins[0].origin == "Ubuntu" and + origins[0].component in ["main", "universe"]) or + (origins[0].origin == "Debian" and + origins[0].component == "main")) and + origins[0].trusted) + + def _is_package_collection(self, pkg): + """Return True if the package is a metapackge + """ + section = pkg.section.split("/")[-1] + return section == "metapackages" + + def _has_package_gui(self, pkg): + # FIXME: take application data into account. perhaps checking for + # property in the xapian database + return pkg.section.split('/')[-1].lower() in ['x11', 'gnome', 'kde'] + + def _is_package_devel(self, pkg): + return (pkg.name.endswith("-dev") or pkg.name.endswith("-dbg") or + pkg.section.split('/')[-1].lower() in ['devel', 'libdevel']) + + def _is_package_supported(self, pkg): + if not pkg.candidate: + return False + origins = pkg.candidate.origins + return (origins and + ((origins[0].origin == "Ubuntu" and + origins[0].component in ["main", "restricted"]) or + (origins[0].origin == "Debian" and + origins[0].component == "main")) and + origins[0].trusted) + + def _get_package_by_id(self, id): + """Return the apt.package.Package corresponding to the given + package id. + + If the package isn't available error out. + """ + version = self._get_version_by_id(id) + return version.package + + def _get_version_by_id(self, id): + """Return the apt.package.Version corresponding to the given + package id. + + If the version isn't available error out. + """ + name, version_string, arch, data = id.split(";", 4) + if arch and arch not in [self.NATIVE_ARCH, "all"]: + name += ":%s" % arch + try: + pkg = self._cache[name] + except KeyError: + raise TransactionFailed(aptd_enums.ERROR_NO_PACKAGE, + "There isn't any package named %s", + name) + try: + version = pkg.versions[version_string] + except: + raise TransactionFailed(aptd_enums.ERROR_NO_PACKAGE, + "Verion %s doesn't exist", + version_string) + if version.architecture != arch: + raise TransactionFailed(aptd_enums.ERROR_NO_PACKAGE, + "Version %s of %s isn't available " + "for architecture %s", + pkg.name, version.version, arch) + return version + + def _get_installed_files(self, pkg): + """ + Return the list of unicode names of the files which have + been installed by the package + + This method should be obsolete by the + apt.package.Package.installedFiles attribute as soon as the + consolidate branch of python-apt gets merged + """ + path = os.path.join(apt_pkg.config["Dir"], + "var/lib/dpkg/info/%s.list" % pkg.name) + try: + with open(path, 'rb') as f: + files = f.read().decode('UTF-8').split("\n") + except IOError: + return [] + return files + + def _get_package_group(self, pkg): + """ + Return the packagekit group corresponding to the package's section + """ + section = pkg.section.split("/")[-1] + if section in SECTION_GROUP_MAP: + return SECTION_GROUP_MAP[section] + else: + pklog.warning("Unkown package section %s of %s" % (pkg.section, + pkg.name)) + return pk.GroupEnum.UNKNOWN + + def _init_plugins(self): + """Initialize PackageKit apt backend plugins. + Do nothing if plugins are already initialized. + """ + if self._plugins is not None: + return + + if not pkg_resources: + return + + self._plugins = {} # plugin_name -> [plugin_fn1, ...] + + # just look in standard Python paths for now + dists, errors = pkg_resources.working_set.find_plugins( + pkg_resources.Environment()) + for dist in dists: + pkg_resources.working_set.add(dist) + for plugin_name in ["what_provides"]: + for entry_point in pkg_resources.iter_entry_points( + "packagekit.apt.plugins", plugin_name): + try: + plugin = entry_point.load() + except Exception as e: + pklog.warning("Failed to load %s from plugin %s: %s" % ( + plugin_name, str(entry_point.dist), str(e))) + continue + pklog.debug("Loaded %s from plugin %s" % ( + plugin_name, str(entry_point.dist))) + self._plugins.setdefault(plugin_name, []).append(plugin) + + def _apply_changes(self, trans, fetch_range=(15, 50), + install_range=(50, 90)): + """Apply changes and emit RequireRestart accordingly.""" + if hasattr(trans, "pktrans"): + # Cache the ids of the to be changed packages, since we will + # only get the package name during download/install time + for pkg in self._cache.get_changes(): + if pkg.marked_delete or pkg.marked_reinstall: + pkg_id = self._get_id_from_version(pkg.installed) + else: + pkg_id = self._get_id_from_version(pkg.candidate) + trans.pktrans.pkg_id_cache[pkg.name] = pkg_id + + aptworker.AptWorker._apply_changes(self, trans, fetch_range, + install_range) + + if (hasattr(trans, "pktrans") and + (trans.role == aptd_enums.ROLE_UPGRADE_SYSTEM or + trans.packages[aptd_enums.PKGS_UPGRADE] or + trans.depends[aptd_enums.PKGS_UPGRADE])): + self._emit_require_restart(trans) + + +if META_RELEASE_SUPPORT: + + class GMetaRelease(GObject.GObject, MetaReleaseCore): + + __gsignals__ = {"download-done": (GObject.SignalFlags.RUN_FIRST, + None, + ())} + + def __init__(self): + GObject.GObject.__init__(self) + MetaReleaseCore.__init__(self, False, False) + + def download(self): + MetaReleaseCore.download(self) + self.emit("download-done") + + +def bitfield_summarize(*enums): + """Return the bitfield with the given PackageKit enums.""" + field = 0 + for enum in enums: + field |= 2 ** int(enum) + return field + + +def bitfield_add(field, enum): + """Add a PackageKit enum to a given field""" + field |= 2 ** int(enum) + return field + + +def bitfield_remove(field, enum): + """Remove a PackageKit enum to a given field""" + field = field ^ 2 ** int(enum) + return field + + +def bitfield_contains(field, enum): + """Return True if a bitfield contains the given PackageKit enum""" + return field & 2 ** int(enum) + + +# vim: ts=4 et sts=4 @@ -0,0 +1,30 @@ +#!/usr/bin/python3 +# -*- coding: utf-8 -*- +""" +aptdcon - command line interface client to aptdaemon +""" +# Copyright (C) 2008 Sebastian Heinlein <devel@glatzor.de> +# +# This program is free software; you can redistribute it and/or modify +# it under the terms of the GNU General Public License as published by +# the Free Software Foundation; either version 2 of the License, or +# any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU General Public License along +# with this program; if not, write to the Free Software Foundation, Inc., +# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. + +__author__ = "Sebastian Heinlein <devel@glatzor.de>" +__state__ = "experimental" + +import aptdaemon.console + +if __name__ == "__main__": + aptdaemon.console.main() + +# vim:ts=4:sw=4:et |