<commit_before>#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

"""
Framework for installing and updating distribution packages
"""

from django.contrib import messages
import functools
from gettext import gettext as _
from gi.repository import GLib as glib
from gi.repository import PackageKitGlib as packagekit
import logging
import threading

import plinth


logger = logging.getLogger(__name__)
transactions = {}
packages_resolved = {}


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


class Transaction(object):
    """Information about an ongoing transaction."""

    def __init__(self, package_names, on_install=None):
        """Initialize transaction object.

        Set most values to None until they are sent as progress update.
        """
        self.package_names = package_names
        # XXX: This is hack, remove after implementing proper setup mechanism.
        self.on_install = on_install

        # 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

        # Completion
        self.is_finished = False
        self.exception = 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 start_install(self):
        """Start a PackageKit transaction to install given list of packages.

        This operation is non-blocking at it spawns a new thread.
        """
        thread = threading.Thread(target=self._install)
        thread.start()

    def _install(self):
        """Run a PackageKit transaction to install given packages."""
        try:
            self._do_install()
        except PackageException as exception:
            self.finish(exception)
            return
        except glib.Error as exception:
            self.finish(PackageException(exception.message))
            return

        try:
            if self.on_install:
                self.on_install()
        except Exception as exception:
            logger.exception('Error during setup - %s', exception)
            self.finish(exception)
            return

        self.finish()

    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)

        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."""
        # XXX: Untested code
        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 = error_code.to_string() 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"""
        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
        else:
            logger.info('Unhandle packagekit progress callback - %s, %s',
                        progress, progress_type)

    def finish(self, exception=None):
        """Mark transaction as complected and store exception if any."""
        self.is_finished = True
        self.exception = exception<commit_msg>packages: handle speed updates during installation<commit_after>#
# This file is part of Plinth.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as
# published by the Free Software Foundation, either version 3 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program.  If not, see <http://www.gnu.org/licenses/>.
#

"""
Framework for installing and updating distribution packages
"""

from django.contrib import messages
import functools
from gettext import gettext as _
from gi.repository import GLib as glib
from gi.repository import PackageKitGlib as packagekit
import logging
import threading

import plinth


logger = logging.getLogger(__name__)
transactions = {}
packages_resolved = {}


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


class Transaction(object):
    """Information about an ongoing transaction."""

    def __init__(self, package_names, on_install=None):
        """Initialize transaction object.

        Set most values to None until they are sent as progress update.
        """
        self.package_names = package_names
        # XXX: This is hack, remove after implementing proper setup mechanism.
        self.on_install = on_install

        # 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

        # Completion
        self.is_finished = False
        self.exception = 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 start_install(self):
        """Start a PackageKit transaction to install given list of packages.

        This operation is non-blocking at it spawns a new thread.
        """
        thread = threading.Thread(target=self._install)
        thread.start()

    def _install(self):
        """Run a PackageKit transaction to install given packages."""
        try:
            self._do_install()
        except PackageException as exception:
            self.finish(exception)
            return
        except glib.Error as exception:
            self.finish(PackageException(exception.message))
            return

        try:
            if self.on_install:
                self.on_install()
        except Exception as exception:
            logger.exception('Error during setup - %s', exception)
            self.finish(exception)
            return

        self.finish()

    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)

        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."""
        # XXX: Untested code
        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 = error_code.to_string() 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"""
        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:
            logger.info('Unhandle packagekit progress callback - %s, %s',
                        progress, progress_type)

    def finish(self, exception=None):
        """Mark transaction as complected and store exception if any."""
        self.is_finished = True
        self.exception = exception
{'fraction_non_alphanumeric': 0.07700408055811504, 'fraction_numerical': 0.0009214163485586416, 'mean_word_length': 2.53865859338612, 'pattern_counts': {'":': 0, '<': 5, '<?xml version=': 0, '>': 5, 'https://': 0, 'lorem ipsum': 0, 'www.': 2, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 10, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '5855059', 'n_tokens_mistral': 3797, 'n_tokens_neox': 3716, 'n_words': 1147}