aboutsummaryrefslogtreecommitdiff
path: root/aptd
diff options
context:
space:
mode:
Diffstat (limited to '')
-rwxr-xr-xaptd41
-rw-r--r--aptdaemon/__init__.py28
-rw-r--r--aptdaemon/client.py1714
-rw-r--r--aptdaemon/config.py243
-rw-r--r--aptdaemon/console.py691
-rw-r--r--aptdaemon/core.py2204
-rw-r--r--aptdaemon/crash.py79
-rw-r--r--aptdaemon/debconf.py180
-rw-r--r--aptdaemon/enums.py718
-rw-r--r--aptdaemon/errors.py235
-rw-r--r--aptdaemon/gtk3widgets.py1206
-rw-r--r--aptdaemon/lock.py198
-rw-r--r--aptdaemon/logger.py77
-rw-r--r--aptdaemon/loop.py33
-rw-r--r--aptdaemon/networking.py267
-rw-r--r--aptdaemon/pkenums.py969
-rw-r--r--aptdaemon/pkutils.py46
-rw-r--r--aptdaemon/policykit1.py175
-rw-r--r--aptdaemon/progress.py829
-rw-r--r--aptdaemon/test.py312
-rw-r--r--aptdaemon/utils.py131
-rw-r--r--aptdaemon/worker/__init__.py334
-rw-r--r--aptdaemon/worker/aptworker.py1537
-rw-r--r--aptdaemon/worker/pkworker.py1353
-rwxr-xr-xaptdcon30
25 files changed, 13630 insertions, 0 deletions
diff --git a/aptd b/aptd
new file mode 100755
index 0000000..f26f335
--- /dev/null
+++ b/aptd
@@ -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
diff --git a/aptdcon b/aptdcon
new file mode 100755
index 0000000..c24ebd4
--- /dev/null
+++ b/aptdcon
@@ -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