mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
setup: Add method to run app repair
- Repair is run within an operation. - Diagnostics are run for the app first. - Call app.repair, then re-run setup if needed. - Add helper functions for apps or components to store error messages in thread local storage. These error messages are shown at the end. Signed-off-by: James Valleroy <jvalleroy@mailbox.org> [sunil: Undo minor reformatting, due to automatic tool] [sunil: Fix passing incorrect Exception argument to operation.on_update] [sunil: Add full stop at the end of the success message to match install message] Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Sunil Mohan Adapa <sunil@medhas.org>
This commit is contained in:
parent
f487565b2c
commit
35c2326261
113
plinth/setup.py
113
plinth/setup.py
@ -13,6 +13,7 @@ from django.utils.translation import gettext_noop
|
||||
|
||||
import plinth
|
||||
from plinth import app as app_module
|
||||
from plinth.diagnostic_check import Result
|
||||
from plinth.package import Packages
|
||||
from plinth.signals import post_setup
|
||||
|
||||
@ -26,6 +27,8 @@ _is_first_setup = False
|
||||
is_first_setup_running = False
|
||||
_is_shutting_down = False
|
||||
|
||||
thread_local_storage = threading.local()
|
||||
|
||||
|
||||
def run_setup_on_app(app_id, allow_install=True, rerun=False):
|
||||
"""Execute the setup process in a thread."""
|
||||
@ -50,10 +53,10 @@ def run_setup_on_app(app_id, allow_install=True, rerun=False):
|
||||
thread_data={'allow_install': allow_install})
|
||||
|
||||
|
||||
def _run_setup_on_app(app, current_version):
|
||||
def _run_setup_on_app(app, current_version, repair: bool = False):
|
||||
"""Execute the setup process."""
|
||||
logger.info('Setup run: %s', app.app_id)
|
||||
exception_to_update = None
|
||||
exception_to_update: Exception | None = None
|
||||
message = None
|
||||
try:
|
||||
current_version = app.get_setup_version()
|
||||
@ -74,12 +77,17 @@ def _run_setup_on_app(app, current_version):
|
||||
if not current_version:
|
||||
message = gettext_noop('Error installing app: {error}').format(
|
||||
error=exception)
|
||||
elif repair:
|
||||
message = gettext_noop('Error repairing 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.')
|
||||
elif repair:
|
||||
return
|
||||
else:
|
||||
message = gettext_noop('App updated')
|
||||
|
||||
@ -89,6 +97,86 @@ def _run_setup_on_app(app, current_version):
|
||||
operation.on_update(message, exception_to_update)
|
||||
|
||||
|
||||
def run_repair_on_app(app_id):
|
||||
"""Execute the repair process in a thread."""
|
||||
app = app_module.App.get(app_id)
|
||||
current_version = app.get_setup_version()
|
||||
if not current_version:
|
||||
logger.warning('App %s is not installed, cannot repair', app_id)
|
||||
return
|
||||
|
||||
logger.debug('Creating operation to repair app: %s', app_id)
|
||||
return operation_module.manager.new(f'{app_id}-repair', app_id,
|
||||
gettext_noop('Repairing app'),
|
||||
_run_repair_on_app, [app],
|
||||
show_message=True,
|
||||
show_notification=True)
|
||||
|
||||
|
||||
def _run_repair_on_app(app: app_module.App):
|
||||
"""Execute the repair process."""
|
||||
logger.info('Repair run: %s', app.app_id)
|
||||
message = None
|
||||
operation = operation_module.Operation.get_operation()
|
||||
|
||||
# Always re-run diagnostics first for this app, to ensure results are
|
||||
# current.
|
||||
checks = []
|
||||
try:
|
||||
checks = app.diagnose()
|
||||
except Exception as exception:
|
||||
logger.error('Error running %s diagnostics - %s', app.app_id,
|
||||
exception)
|
||||
message = gettext_noop('Error running diagnostics: {error}').format(
|
||||
error=exception)
|
||||
operation.on_update(message, exception)
|
||||
return
|
||||
|
||||
# Filter for checks that have failed.
|
||||
failed_checks = []
|
||||
for check in checks:
|
||||
if check.result in [Result.FAILED, Result.WARNING]:
|
||||
failed_checks.append(check)
|
||||
|
||||
if not failed_checks:
|
||||
logger.warning('Skipping repair for %s: no failed checks', app.app_id)
|
||||
message = gettext_noop('Skipping repair, no failed checks')
|
||||
operation.on_update(message, None)
|
||||
return
|
||||
|
||||
try:
|
||||
should_rerun_setup = app.repair(failed_checks)
|
||||
except Exception as exception:
|
||||
logger.error('Repair error: %s: %s %s', app.app_id, message, exception)
|
||||
message = gettext_noop('Error repairing app: {error}').format(
|
||||
error=exception)
|
||||
operation.on_update(message, exception)
|
||||
return
|
||||
|
||||
if should_rerun_setup:
|
||||
message = gettext_noop('Re-running setup to complete repairs')
|
||||
operation.on_update(message, None)
|
||||
current_version = app.get_setup_version()
|
||||
_run_setup_on_app(app, current_version, True)
|
||||
|
||||
logger.info('Repair completed: %s', app.app_id)
|
||||
|
||||
# Check for errors in thread local storage
|
||||
message = gettext_noop('App repaired.')
|
||||
errors = retrieve_error_messages()
|
||||
exceptions = None
|
||||
if errors:
|
||||
message = gettext_noop('App repair completed with errors:\n')
|
||||
error_message = ''
|
||||
for error in errors:
|
||||
message += str(error) + '\n'
|
||||
error_message += str(error) + '\n'
|
||||
|
||||
exceptions = Exception(error_message)
|
||||
|
||||
operation.on_update(message, exceptions)
|
||||
|
||||
|
||||
def run_uninstall_on_app(app_id):
|
||||
"""Execute the uninstall process in a thread."""
|
||||
# App is already uninstalled
|
||||
@ -565,3 +653,24 @@ def on_package_cache_updated():
|
||||
"""Called by D-Bus service when apt package cache is updated."""
|
||||
force_upgrader = ForceUpgrader.get_instance()
|
||||
force_upgrader.on_package_cache_updated()
|
||||
|
||||
|
||||
def store_error_message(error_message: str):
|
||||
"""Add an error message to thread local storage."""
|
||||
try:
|
||||
thread_local_storage.errors.append(error_message)
|
||||
except AttributeError:
|
||||
thread_local_storage.errors = [error_message]
|
||||
|
||||
|
||||
def retrieve_error_messages() -> list[str]:
|
||||
"""Retrieve the error messages from thread local storage.
|
||||
|
||||
Errors are cleared after retrieval."""
|
||||
try:
|
||||
errors = thread_local_storage.errors
|
||||
thread_local_storage.errors = []
|
||||
except AttributeError:
|
||||
errors = []
|
||||
|
||||
return errors
|
||||
|
||||
19
plinth/tests/test_setup.py
Normal file
19
plinth/tests/test_setup.py
Normal file
@ -0,0 +1,19 @@
|
||||
# SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
"""
|
||||
Test module for setup module.
|
||||
"""
|
||||
|
||||
from plinth.setup import store_error_message, retrieve_error_messages
|
||||
|
||||
|
||||
def test_store_retrieve_error_message():
|
||||
"""Test storing and retrieving error messages."""
|
||||
store_error_message('error 1')
|
||||
assert retrieve_error_messages() == ['error 1']
|
||||
|
||||
store_error_message('error 1')
|
||||
store_error_message('error 2')
|
||||
assert retrieve_error_messages() == ['error 1', 'error 2']
|
||||
|
||||
# errors are cleared after retrieving
|
||||
assert retrieve_error_messages() == []
|
||||
Loading…
x
Reference in New Issue
Block a user