[Date Prev][Date Next] [Thread Prev][Thread Next] [Date Index] [Thread Index]

Bug#758116: Patch to install packages using PackageKit



Hello,

Here is an experimental patch to perform package installation using
PackageKit.  I have tested it for success and error conditions but there
are corner cases such as inserting devices during package installation
and parallel package installation that I haven't tested.  I expect them
to work fine by the way.

One thing to consider about PackageKit is that it does not show progress
dialogs like aptdaemon.  But I have left some code in there to collect
progress information that can be used to show progress dialogs if necessary.

-- 
Sunil
From 7ee652f3fc6b09e1fdf9cba706d562128de2c247 Mon Sep 17 00:00:00 2001
From: Sunil Mohan Adapa <sunil@medhas.org>
Date: Wed, 25 May 2016 11:29:58 +0530
Subject: [PATCH] Implement package installation using PackageKit

- Based on code from FreedomBox UI - Plinth.  I am the original author
  of the code and I here by license it under the same license as
  isenkram package: GNU GPL v2 or later.

- When trying to install packages, a dialog is shown to the user asking
  for administrator password to allow for package installation.

- PackageKit does not provide package installation progress bars like
  aptdaemon.  So, use libnotify to show a message at the start and end
  of installation.
---
 isenkramd | 181 +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
 1 file changed, 179 insertions(+), 2 deletions(-)

diff --git a/isenkramd b/isenkramd
index 7eee84d..ac5028d 100755
--- a/isenkramd
+++ b/isenkramd
@@ -40,6 +40,8 @@ gi.require_version('GUdev', '1.0')
 from gi.repository import GUdev
 gi.require_version('Notify', '0.7')
 from gi.repository import Notify
+gi.require_version('PackageKitGlib', '1.0')
+from gi.repository import PackageKitGlib as packagekit
 
 import isenkram.lookup
 
@@ -49,6 +51,10 @@ from aptdaemon.gtk3widgets import AptErrorDialog, \
                                  AptProgressDialog
 import aptdaemon.errors
 
+
+use_apt_daemon = False
+
+
 class AptDaemonGUIClient(object):
 
     """Provides a graphical interface to aptdaemon."""
@@ -102,6 +108,173 @@ class AptDaemonGUIClient(object):
         self.loop.run()
 
 
+class PackageException(Exception):
+    """A package operation has failed."""
+
+    def __init__(self, error_string=None, error_details=None, *args, **kwargs):
+        """Store packagekit error string and details."""
+        super(PackageException, self).__init__(*args, **kwargs)
+
+        self.error_string = error_string
+        self.error_details = error_details
+
+    def __str__(self):
+        """Return the strin representation of the exception."""
+        return 'PackageException(error_string="{0}", error_details="{1}")' \
+            .format(self.error_string, self.error_details)
+
+
+class PackageKitInstaller(object):
+    """Helper to install packages using PackageKit."""
+
+    def __init__(self, package_names):
+        """Initialize transaction object.
+
+        Set most values to None until they are sent as progress update.
+        """
+        self.package_names = package_names
+
+        # Progress
+        self.allow_cancel = None
+        self.percentage = None
+        self.status = None
+        self.status_string = None
+        self.flags = None
+        self.package = None
+        self.package_id = None
+        self.item_progress = None
+        self.role = None
+        self.caller_active = None
+        self.download_size_remaining = None
+        self.speed = None
+
+    def get_id(self):
+        """Return a identifier to use as a key in a map of transactions."""
+        return frozenset(self.package_names)
+
+    def __str__(self):
+        """Return the string representation of the object"""
+        return ('Transaction(packages={0}, allow_cancel={1}, status={2}, '
+                ' percentage={3}, package={4}, item_progress={5})').format(
+                    self.package_names, self.allow_cancel, self.status_string,
+                    self.percentage, self.package, self.item_progress)
+
+    def notify_and_install(self):
+        """Notify, start installation and then notify success/error."""
+        start_notification = Notify.Notification(
+            summary='Installing packages',
+            body='Instaling packages - {packages}'.format(
+                packages=', '.join(self.package_names)))
+        start_notification.set_timeout(10000)
+        start_notification.show()
+
+        error = None
+        try:
+            self.install()
+        except PackageException as exception:
+            error = exception
+
+        start_notification.close()
+        if not error:
+            final_notification = Notify.Notification(
+                summary='Installation successful',
+                body='Packages have been successfully installed - {packages}'.
+                format(packages=', '.join(self.package_names)))
+        else:
+            final_notification = Notify.Notification(
+                summary='Installation failed',
+                body='Error installing packages: {string}, {details}'.format(
+                    string=error.error_string, details=error.error_details))
+        final_notification.set_timeout(10000)
+        final_notification.show()
+
+    def install(self):
+        """Run a PackageKit transaction to install given packages."""
+        try:
+            self._do_install()
+        except GLib.Error as exception:
+            raise PackageException(exception.message)
+
+    def _do_install(self):
+        """Run a PackageKit transaction to install given packages.
+
+        Raise exception in case of error.
+        """
+        client = packagekit.Client()
+        client.set_interactive(False)
+
+        # Refresh package cache from all enabled repositories
+        results = client.refresh_cache(
+            False, None, self.progress_callback, self)
+        self._assert_success(results)
+
+        # Resolve packages again to get the latest versions after refresh
+        results = client.resolve(packagekit.FilterEnum.INSTALLED,
+                                 tuple(self.package_names) + (None, ),
+                                 None, self.progress_callback, self)
+        self._assert_success(results)
+
+        packages_resolved = {}
+        for package in results.get_package_array():
+            packages_resolved[package.get_name()] = package
+
+        package_ids = []
+        for package_name in self.package_names:
+            if package_name not in packages_resolved or \
+               not packages_resolved[package_name]:
+                raise PackageException('packages not found')
+
+            package_ids.append(packages_resolved[package_name].get_id())
+
+        # Start package installation
+        results = client.install_packages(
+            packagekit.TransactionFlagEnum.ONLY_TRUSTED, package_ids + [None],
+            None, self.progress_callback, self)
+        self._assert_success(results)
+
+    def _assert_success(self, results):
+        """Check that the most recent operation was a success."""
+        if results and results.get_error_code() is not None:
+            error = results.get_error_code()
+            error_code = error.get_code() if error else None
+            error_string = packagekit.ErrorEnum.to_string(error_code) \
+                if error_code else None
+            error_details = error.get_details() if error else None
+            raise PackageException(error_string, error_details)
+
+    def progress_callback(self, progress, progress_type, user_data):
+        """Process progress updates on package resolve operation"""
+        return
+        if progress_type == packagekit.ProgressType.PERCENTAGE:
+            self.percentage = progress.props.percentage
+        elif progress_type == packagekit.ProgressType.PACKAGE:
+            self.package = progress.props.package
+        elif progress_type == packagekit.ProgressType.ALLOW_CANCEL:
+            self.allow_cancel = progress.props.allow_cancel
+        elif progress_type == packagekit.ProgressType.PACKAGE_ID:
+            self.package_id = progress.props.package_id
+        elif progress_type == packagekit.ProgressType.ITEM_PROGRESS:
+            self.item_progress = progress.props.item_progress
+        elif progress_type == packagekit.ProgressType.STATUS:
+            self.status = progress.props.status
+            self.status_string = \
+                packagekit.StatusEnum.to_string(progress.props.status)
+        elif progress_type == packagekit.ProgressType.TRANSACTION_FLAGS:
+            self.flags = progress.props.transaction_flags
+        elif progress_type == packagekit.ProgressType.ROLE:
+            self.role = progress.props.role
+        elif progress_type == packagekit.ProgressType.CALLER_ACTIVE:
+            self.caller_active = progress.props.caller_active
+        elif progress_type == packagekit.ProgressType.DOWNLOAD_SIZE_REMAINING:
+            self.download_size_remaining = \
+                progress.props.download_size_remaining
+        elif progress_type == packagekit.ProgressType.SPEED:
+            self.speed = progress.props.speed
+        else:
+            print('Unhandled packagekit progress callback - %s, %s',
+                  progress, progress_type)
+
+
 # Keep refs needed for callback to work
 n = None
 npkgs = None
@@ -111,8 +284,12 @@ def notify_pleaseinstall(notification=None, action=None, data=None):
     pkgsstr = string.join(pkgs, " ")
 #    print pkgs
     print "info: button clicked, installing %s" % pkgsstr
-    demo = AptDaemonGUIClient(pkgs[0])
-    demo.request_installation()
+    if use_apt_daemon:
+        installer = AptDaemonGUIClient(pkgs[0])
+        installer.request_installation()
+    else:
+        installer = PackageKitInstaller(pkgs)
+        installer.notify_and_install()
 
 def notify(bus, vendor, device, pkgs):
     pkgstr = string.join(pkgs, " ")
-- 
2.8.1

Attachment: signature.asc
Description: OpenPGP digital signature


Reply to: