package: Implement low-level methods for uninstalling

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2022-08-18 22:02:24 -07:00 committed by James Valleroy
parent 145b3ecc65
commit 1908bd5366
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
4 changed files with 57 additions and 33 deletions

View File

@ -51,6 +51,8 @@ def parse_arguments():
help='list of packages to install') help='list of packages to install')
subparser = subparsers.add_parser('remove', help='remove the package(s)') subparser = subparsers.add_parser('remove', help='remove the package(s)')
subparser.add_argument(
'app_id', help='ID of app for which package is being uninstalled')
subparser.add_argument('--packages', required=True, subparser.add_argument('--packages', required=True,
help='List of packages to remove', nargs='+') help='List of packages to remove', nargs='+')
@ -107,8 +109,19 @@ def subcommand_install(arguments):
def subcommand_remove(arguments): def subcommand_remove(arguments):
"""Remove apt package(s).""" """Remove packages using apt-get."""
sys.exit(run_apt_command(['remove'] + arguments.packages)) try:
_assert_managed_packages(arguments.app_id, arguments.packages)
except Exception as exception:
print('Access check failed:', exception, file=sys.stderr)
sys.exit(99)
subprocess.run(['dpkg', '--configure', '-a'], check=False)
with apt_hold_freedombox():
run_apt_command(['--fix-broken', 'install'])
returncode = run_apt_command(['remove'] + arguments.packages)
sys.exit(returncode)
def _assert_managed_packages(app_id, packages): def _assert_managed_packages(app_id, packages):
@ -118,7 +131,7 @@ def _assert_managed_packages(app_id, packages):
app = app_module.App.get(app_id) app = app_module.App.get(app_id)
managed_packages = [] managed_packages = []
for component in app.get_components_of_type(Packages): for component in app.get_components_of_type(Packages):
managed_packages += component.possible_packages managed_packages += component.possible_packages + component.conflicts
for package in packages: for package in packages:
assert package in managed_packages assert package in managed_packages

View File

@ -16,7 +16,7 @@ from plinth.modules.backups.components import BackupRestore
from plinth.modules.config import get_domainname from plinth.modules.config import get_domainname
from plinth.modules.firewall.components import Firewall from plinth.modules.firewall.components import Firewall
from plinth.modules.letsencrypt.components import LetsEncrypt from plinth.modules.letsencrypt.components import LetsEncrypt
from plinth.package import Packages, remove from plinth.package import Packages, uninstall
from plinth.signals import domain_added, domain_removed from plinth.signals import domain_added, domain_removed
from plinth.utils import format_lazy from plinth.utils import format_lazy
@ -173,7 +173,7 @@ class EmailApp(plinth.app.App):
if packages_to_remove: if packages_to_remove:
logger.info('Removing conflicting packages: %s', logger.info('Removing conflicting packages: %s',
packages_to_remove) packages_to_remove)
remove(packages_to_remove) uninstall(packages_to_remove)
# Install # Install
_clear_conflicts() _clear_conflicts()

View File

@ -17,7 +17,7 @@ from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
from plinth import actions, app from plinth import actions, app
from plinth.errors import ActionError, MissingPackageError from plinth.errors import MissingPackageError
from plinth.utils import format_lazy from plinth.utils import format_lazy
from . import operation as operation_module from . import operation as operation_module
@ -144,7 +144,7 @@ class Packages(app.FollowerComponent):
self._packages.append(package) self._packages.append(package)
self.skip_recommends = skip_recommends self.skip_recommends = skip_recommends
self.conflicts = conflicts self.conflicts = conflicts or []
self.conflicts_action = conflicts_action self.conflicts_action = conflicts_action
@property @property
@ -324,6 +324,15 @@ class Transaction:
logger.exception('Error installing package: %s', exception) logger.exception('Error installing package: %s', exception)
raise raise
def uninstall(self):
"""Run an apt-get transaction to uninstall given packages."""
try:
self._run_apt_command(['remove', self.app_id, '--packages'] +
self.package_names)
except subprocess.CalledProcessError as exception:
logger.exception('Error uninstalling package: %s', exception)
raise
def refresh_package_lists(self): def refresh_package_lists(self):
"""Refresh apt package lists.""" """Refresh apt package lists."""
try: try:
@ -351,7 +360,7 @@ class Transaction:
return_code = process.wait() return_code = process.wait()
if return_code != 0: if return_code != 0:
raise PackageException(_('Error during installation'), self.stderr) raise PackageException(_('Error running apt-get'), self.stderr)
def _read_stdout(self, process): def _read_stdout(self, process):
"""Read the stdout of the process and update progress.""" """Read the stdout of the process and update progress."""
@ -421,6 +430,30 @@ def install(package_names, skip_recommends=False, force_configuration=None,
force_missing_configuration) force_missing_configuration)
def uninstall(package_names):
"""Uninstall a set of packages."""
try:
operation = operation_module.Operation.get_operation()
except AttributeError:
raise RuntimeError(
'uninstall() must be called from within an operation.')
start_time = time.time()
while is_package_manager_busy():
if time.time() - start_time >= 24 * 3600: # One day
raise PackageException(_('Timeout waiting for package manager'))
time.sleep(3) # seconds
logger.info('Running uninstall 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.uninstall()
def is_package_manager_busy(): def is_package_manager_busy():
"""Return whether a package manager is running.""" """Return whether a package manager is running."""
try: try:
@ -467,11 +500,3 @@ def packages_installed(candidates: Union[list, tuple]) -> list:
pass pass
return installed_packages return installed_packages
def remove(packages: Union[list, tuple]) -> None:
"""Remove packages."""
try:
actions.superuser_run('packages', ['remove', '--packages'] + packages)
except ActionError:
pass

View File

@ -9,8 +9,8 @@ from unittest.mock import Mock, call, patch
import pytest import pytest
from plinth.app import App from plinth.app import App
from plinth.errors import ActionError, MissingPackageError from plinth.errors import MissingPackageError
from plinth.package import Package, Packages, packages_installed, remove from plinth.package import Package, Packages, packages_installed
class TestPackageExpressions(unittest.TestCase): class TestPackageExpressions(unittest.TestCase):
@ -54,7 +54,7 @@ def test_packages_init():
assert component.possible_packages == ['foo', 'bar'] assert component.possible_packages == ['foo', 'bar']
assert component.component_id == 'test-component' assert component.component_id == 'test-component'
assert not component.skip_recommends assert not component.skip_recommends
assert component.conflicts is None assert component.conflicts == []
assert component.conflicts_action is None assert component.conflicts_action is None
with pytest.raises(ValueError): with pytest.raises(ValueError):
@ -189,17 +189,3 @@ def test_packages_installed():
assert len(packages_installed(())) == 0 assert len(packages_installed(())) == 0
assert len(packages_installed(('unknown-package', ))) == 0 assert len(packages_installed(('unknown-package', ))) == 0
assert len(packages_installed(('python3', ))) == 1 assert len(packages_installed(('python3', ))) == 1
@patch('plinth.actions.superuser_run')
def test_remove(run):
"""Test removing packages."""
remove(['package1', 'package2'])
run.assert_has_calls(
[call('packages', ['remove', '--packages', 'package1', 'package2'])])
run.reset_mock()
run.side_effect = ActionError()
remove(['package1'])
run.assert_has_calls(
[call('packages', ['remove', '--packages', 'package1'])])