diff --git a/plinth/app.py b/plinth/app.py index 727db85db..a3d1d7781 100644 --- a/plinth/app.py +++ b/plinth/app.py @@ -7,7 +7,6 @@ import collections import enum import inspect import logging -import sys from plinth import cfg from plinth.signals import post_app_loading @@ -542,10 +541,6 @@ def _insert_apps(app_id, app, remaining_apps, ordered_apps): def _initialize_module(module_name, module): """Perform initialization on all apps in a module.""" - # Perform setup related initialization on the module - from . import setup # noqa # Avoid circular import - setup.init(module_name, module) - try: module_classes = inspect.getmembers(module, inspect.isclass) app_classes = [ diff --git a/plinth/middleware.py b/plinth/middleware.py index b68db7a89..0dec396ba 100644 --- a/plinth/middleware.py +++ b/plinth/middleware.py @@ -4,7 +4,6 @@ Common Django middleware. """ import logging -import sys from django import urls from django.conf import settings @@ -13,41 +12,26 @@ from django.contrib.auth.decorators import login_required from django.core.exceptions import PermissionDenied from django.shortcuts import render from django.utils.deprecation import MiddlewareMixin -from django.utils.translation import gettext_lazy as _ from stronghold.utils import is_view_func_public from plinth import app as app_module from plinth import setup -from plinth.package import PackageException from plinth.utils import is_user_admin +from . import operation as operation_module from . import views logger = logging.getLogger(__name__) -def _collect_setup_result(request, app): - """Show success/fail message from previous install operation.""" - setup_helper = sys.modules[app.__module__].setup_helper - if not setup_helper.is_finished: - return - - exception = setup_helper.collect_result() - if not exception: - if not app.info.is_essential: - messages.success(request, _('Application installed.')) - else: - if isinstance(exception, PackageException): - error_string = getattr(exception, 'error_string', str(exception)) - error_details = getattr(exception, 'error_details', '') - message = _('Error installing application: {string} ' - '{details}').format(string=error_string, - details=error_details) +def _collect_operations_results(request, app): + """Show success/fail messages from previous operations.""" + operations = operation_module.manager.collect_results(app.app_id) + for operation in operations: + if operation.exception: + messages.error(request, operation.message) else: - message = _('Error installing application: {error}') \ - .format(error=exception) - - messages.error(request, message) + messages.success(request, operation.message) class SetupMiddleware(MiddlewareMixin): @@ -77,9 +61,9 @@ class SetupMiddleware(MiddlewareMixin): app = app_module.App.get(app_id) is_admin = is_user_admin(request) - # Collect and show setup operation result to admins + # Collect and show operations' results to admins if is_admin: - _collect_setup_result(request, app) + _collect_operations_results(request, app) # Check if application is up-to-date if app.get_setup_state() == \ diff --git a/plinth/modules/backups/tests/test_api.py b/plinth/modules/backups/tests/test_api.py index 395e2f271..8ea170b7a 100644 --- a/plinth/modules/backups/tests/test_api.py +++ b/plinth/modules/backups/tests/test_api.py @@ -15,8 +15,6 @@ from ..components import BackupRestore # pylint: disable=protected-access -setup_helper = MagicMock() - def _get_test_manifest(name): return { diff --git a/plinth/package.py b/plinth/package.py index 0fea8449d..a7dbcb734 100644 --- a/plinth/package.py +++ b/plinth/package.py @@ -8,7 +8,6 @@ import json import logging import pathlib import subprocess -import sys import threading from typing import Optional, Union @@ -20,6 +19,9 @@ from plinth import actions, app from plinth.errors import ActionError, MissingPackageError from plinth.utils import format_lazy +from . import operation as operation_module +from .errors import PackageNotInstalledError + logger = logging.getLogger(__name__) @@ -170,12 +172,8 @@ class Packages(app.FollowerComponent): def setup(self, old_version): """Install the packages.""" - # TODO: Drop the need for setup helper. - module_name = self.app.__module__ - module = sys.modules[module_name] - helper = module.setup_helper - helper.install(self.get_actual_packages(), - skip_recommends=self.skip_recommends) + install(self.get_actual_packages(), + skip_recommends=self.skip_recommends) def diagnose(self): """Run diagnostics and return results.""" @@ -387,6 +385,34 @@ class Transaction: self.percentage = int(float(parts[2])) +def install(package_names, skip_recommends=False, force_configuration=None, + reinstall=False, force_missing_configuration=False): + """Install a set of packages marking progress.""" + try: + operation = operation_module.Operation.get_operation() + except AttributeError: + raise RuntimeError( + 'install() must be called from within an operation.') + + if not operation.thread_data.get('allow_install', True): + # Raise error if packages are not already installed. + cache = apt.Cache() + for package_name in package_names: + if not cache[package_name].is_installed: + raise PackageNotInstalledError(package_name) + + return + + logger.info('Running install for app - %s, packages - %s', + operation.app_id, package_names) + + from . import package + transaction = package.Transaction(operation.app_id, package_names) + operation.thread_data['transaction'] = transaction + transaction.install(skip_recommends, force_configuration, reinstall, + force_missing_configuration) + + def is_package_manager_busy(): """Return whether a package manager is running.""" try: diff --git a/plinth/setup.py b/plinth/setup.py index fd5dc4d72..718127465 100644 --- a/plinth/setup.py +++ b/plinth/setup.py @@ -4,20 +4,20 @@ Utilities for performing application setup operations. """ import logging -import sys import threading import time from collections import defaultdict import apt +from django.utils.translation import gettext_noop import plinth from plinth import app as app_module -from plinth.package import Packages +from plinth.package import PackageException, Packages from plinth.signals import post_setup +from . import operation as operation_module from . import package -from .errors import PackageNotInstalledError logger = logging.getLogger(__name__) @@ -28,106 +28,72 @@ _is_shutting_down = False _force_upgrader = None -class Helper: - """Helper routines for modules to show progress.""" +def run_setup_on_app(app_id, allow_install=True): + """Execute the setup process in a thread.""" + # Setup for the module is already running + if operation_module.manager.filter(app_id): + return - def __init__(self, module_name, module): - """Initialize the object.""" - self.module_name = module_name - self.module = module - self.current_operation = None - self.is_finished = None - self.exception = None - self.allow_install = True + # App is already up-to-date + app = app_module.App.get(app_id) + current_version = app.get_setup_version() + if current_version >= app.info.version: + return - def run_in_thread(self): - """Execute the setup process in a thread.""" - thread = threading.Thread(target=self._run) - thread.start() + if not current_version: + name = gettext_noop('Installing app') + else: + name = gettext_noop('Updating app') - def _run(self): - """Collect exceptions when running in a thread.""" - try: - self.run() - except Exception as exception: - self.exception = exception + logger.debug('Creating operation to setup app: %s', app_id) + show_notification = show_message = (current_version + or not app.info.is_essential) + return operation_module.manager.new( + app_id, name, _run_setup_on_app, [app, current_version], + show_message=show_message, show_notification=show_notification, + thread_data={'allow_install': allow_install}) - def collect_result(self): - """Return the exception if any.""" - exception = self.exception - self.exception = None - self.is_finished = None - return exception - def run(self, allow_install=True): - """Execute the setup process.""" - # Setup for the module is already running - if self.current_operation: - return - - app = self.module.app +def _run_setup_on_app(app, current_version): + """Execute the setup process.""" + logger.info('Setup run: %s', app.app_id) + exception_to_update = None + message = None + try: current_version = app.get_setup_version() - if current_version >= app.info.version: - return - - self.allow_install = allow_install - self.exception = None - self.current_operation = None - self.is_finished = False - try: - if hasattr(self.module, 'setup'): - logger.info('Running module setup - %s', self.module_name) - self.module.setup(self, old_version=current_version) - else: - logger.info('Module does not require setup - %s', - self.module_name) - except Exception as exception: - logger.exception('Error running setup - %s', exception) - raise exception + app.setup(old_version=current_version) + app.set_setup_version(app.info.version) + post_setup.send_robust(sender=app.__class__, module_name=app.app_id) + except PackageException as exception: + exception_to_update = exception + error_string = getattr(exception, 'error_string', str(exception)) + error_details = getattr(exception, 'error_details', '') + if not current_version: + message = gettext_noop('Error installing app: {string} ' + '{details}').format(string=error_string, + details=error_details) else: - app.set_setup_version(app.info.version) - post_setup.send_robust(sender=self.__class__, - module_name=self.module_name) - finally: - self.is_finished = True - self.current_operation = None + message = gettext_noop('Error updating app: {string} ' + '{details}').format(string=error_string, + details=error_details) + except Exception as exception: + exception_to_update = exception + if not current_version: + message = gettext_noop('Error installing app: {error}').format( + error=exception) + else: + message = gettext_noop('Error updating app: {error}').format( + error=exception) + else: + if not current_version: + message = gettext_noop('App installed.') + else: + message = gettext_noop('App updated') - def install(self, package_names, skip_recommends=False, - force_configuration=None, reinstall=False, - force_missing_configuration=False): - """Install a set of packages marking progress.""" - if self.allow_install is False: - # Raise error if packages are not already installed. - cache = apt.Cache() - for package_name in package_names: - if not cache[package_name].is_installed: - raise PackageNotInstalledError(package_name) - - return - - logger.info('Running install for module - %s, packages - %s', - self.module_name, package_names) - - transaction = package.Transaction(self.module_name, package_names) - self.current_operation = { - 'step': 'install', - 'transaction': transaction, - } - transaction.install(skip_recommends, force_configuration, reinstall, - force_missing_configuration) - - def call(self, step, method, *args, **kwargs): - """Call an arbitrary method during setup and note down its stage.""" - logger.info('Running step for module - %s, step - %s', - self.module_name, step) - self.current_operation = {'step': step} - return method(*args, **kwargs) - - -def init(module_name, module): - """Create a setup helper for a module for later use.""" - if not hasattr(module, 'setup_helper'): - module.setup_helper = Helper(module_name, module) + logger.info('Setup completed: %s: %s %s', app.app_id, message, + exception_to_update) + operation = operation_module.Operation.get_operation() + operation.on_update(message, exception_to_update) def stop(): @@ -151,8 +117,9 @@ def setup_apps(app_ids=None, essential=False, allow_install=True): if app_ids and app.app_id not in app_ids: continue - module = sys.modules[app.__module__] - module.setup_helper.run(allow_install=allow_install) + operation = run_setup_on_app(app.app_id, allow_install=allow_install) + if operation: + operation.join() def list_dependencies(app_ids=None, essential=False): @@ -173,10 +140,10 @@ def list_dependencies(app_ids=None, essential=False): def run_setup_in_background(): """Run setup in a background thread.""" _set_is_first_setup() - threading.Thread(target=_run_setup).start() + threading.Thread(target=_run_setup_on_startup).start() -def _run_setup(): +def _run_setup_on_startup(): """Run setup with retry till it succeeds.""" sleep_time = 10 while True: @@ -203,7 +170,6 @@ def _run_first_setup(): """Run setup on essential apps on first setup.""" global is_first_setup_running is_first_setup_running = True - # TODO When it errors out, show error in the UI run_setup_on_apps(None, allow_install=False) is_first_setup_running = False diff --git a/plinth/templates/setup.html b/plinth/templates/setup.html index 37cb5ea22..6895ddfa6 100644 --- a/plinth/templates/setup.html +++ b/plinth/templates/setup.html @@ -17,7 +17,7 @@ {% trans "Application installed." %} - {% elif not setup_current_operation %} + {% elif not operations %}
{% if setup_state.value == 'needs-setup' %} @@ -77,38 +77,12 @@ {% else %} - {% if setup_current_operation.step == 'pre' %} -