mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
323 lines
12 KiB
Python
323 lines
12 KiB
Python
#
|
|
# 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
|
|
from django.utils.translation import ugettext as _
|
|
import functools
|
|
import logging
|
|
import threading
|
|
|
|
import plinth
|
|
from plinth.utils import import_from_gi
|
|
glib = import_from_gi('GLib', '2.0')
|
|
packagekit = import_from_gi('PackageKitGlib', '1.0')
|
|
|
|
|
|
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
|
|
|
|
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 Transaction(object):
|
|
"""Information about an ongoing transaction."""
|
|
|
|
def __init__(self, package_names, before_install=None, 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.before_install = before_install
|
|
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_in_thread(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:
|
|
if self.before_install:
|
|
self.before_install()
|
|
except Exception as exception:
|
|
logger.exception('Error during setup before install - %s',
|
|
exception)
|
|
self.finish(exception)
|
|
return
|
|
|
|
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."""
|
|
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"""
|
|
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
|
|
|
|
def collect_result(self):
|
|
"""Retrieve the result of this transaction.
|
|
|
|
Also remove self from global transactions list.
|
|
"""
|
|
assert self.is_finished
|
|
|
|
del transactions[self.get_id()]
|
|
return self.exception
|
|
|
|
|
|
def required(package_names, before_install=None, on_install=None):
|
|
"""Decorate a view to check and install required packages."""
|
|
|
|
def wrapper2(func):
|
|
"""Return a function to check and install packages."""
|
|
|
|
@functools.wraps(func)
|
|
def wrapper(request, *args, **kwargs):
|
|
"""Check and install packages required by a view."""
|
|
if not _should_show_install_view(request, package_names):
|
|
return func(request, *args, **kwargs)
|
|
|
|
view = plinth.views.PackageInstallView.as_view()
|
|
return view(request, package_names=package_names,
|
|
before_install=before_install, on_install=on_install,
|
|
*args, **kwargs)
|
|
|
|
return wrapper
|
|
|
|
return wrapper2
|
|
|
|
|
|
def _should_show_install_view(request, package_names):
|
|
"""Return whether the installation view should be shown."""
|
|
transaction_id = frozenset(package_names)
|
|
|
|
# No transaction in progress
|
|
if transaction_id not in transactions:
|
|
is_installed = check_installed(package_names)
|
|
return not is_installed
|
|
|
|
# Installing
|
|
transaction = transactions[transaction_id]
|
|
if not transaction.is_finished:
|
|
return True
|
|
|
|
# Transaction finished, waiting to show the result
|
|
exception = transaction.collect_result()
|
|
if not exception:
|
|
messages.success(request,
|
|
_('Installed and configured packages successfully.'))
|
|
return False
|
|
else:
|
|
error_string = getattr(exception, 'error_string', str(exception))
|
|
error_details = getattr(exception, 'error_details', '')
|
|
messages.error(request, _('Error installing packages: {string} {details}')
|
|
.format(string=error_string, details=error_details))
|
|
return True
|
|
|
|
|
|
def check_installed(package_names):
|
|
"""Return a boolean installed status of package.
|
|
|
|
This operation is blocking and waits until the check is finished.
|
|
"""
|
|
def _callback(progress, progress_type, user_data):
|
|
"""Process progress updates on package resolve operation."""
|
|
pass
|
|
|
|
client = packagekit.Client()
|
|
response = client.resolve(packagekit.FilterEnum.INSTALLED,
|
|
tuple(package_names) + (None, ), None,
|
|
_callback, None)
|
|
|
|
installed_package_names = []
|
|
for package in response.get_package_array():
|
|
if package.get_info() == packagekit.InfoEnum.INSTALLED:
|
|
installed_package_names.append(package.get_name())
|
|
|
|
packages_resolved[package.get_name()] = package
|
|
|
|
# When package names could not be resolved
|
|
for package_name in package_names:
|
|
if package_name not in packages_resolved:
|
|
packages_resolved[package_name] = None
|
|
|
|
return set(installed_package_names) == set(package_names)
|
|
|
|
|
|
def is_installing(package_names):
|
|
"""Return whether a set of packages are currently being installed."""
|
|
return frozenset(package_names) in transactions
|
|
|
|
|
|
def start_install(package_names, before_install=None, on_install=None):
|
|
"""Start a PackageKit transaction to install given list of packages.
|
|
|
|
This operation is non-blocking at it spawns a new thread.
|
|
"""
|
|
transaction = Transaction(package_names,
|
|
before_install=before_install,
|
|
on_install=on_install)
|
|
transactions[frozenset(package_names)] = transaction
|
|
|
|
transaction.start_install_in_thread()
|