From cdff23db5f4099e344a2fcac4bc230836a3949ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ale=C5=A1=20Mat=C4=9Bj?= Date: Wed, 8 Jul 2026 13:47:31 +0200 Subject: [PATCH 3/4] Add clean command lock demand --- dnf/cli/demand.py | 14 ++++++ dnf/cli/main.py | 98 +++++++++++++++++++++++----------------- dnf/lock.py | 5 ++ tests/cli/test_demand.py | 2 + 4 files changed, 77 insertions(+), 42 deletions(-) diff --git a/dnf/cli/demand.py b/dnf/cli/demand.py index ac19930b..2dc3a3d4 100644 --- a/dnf/cli/demand.py +++ b/dnf/cli/demand.py @@ -20,6 +20,15 @@ from __future__ import unicode_literals +from enum import Enum + + +class CleanCommandLock(Enum): + """Clean command lock modes. :api""" + NONE = 0 + READ = 1 + WRITE = 2 + class _Default(object): def __init__(self, default, choices=None): @@ -70,3 +79,8 @@ class DemandSheet(object): # repositories packages (e.g. versionlock). # If it stays None, the demands.resolving is used as a fallback. plugin_filtering_enabled = _BoolDefault(None) + + # Determines if/how to lock the clean command lock. + # This lock is used to ensure clean command (which uses WRITE mode) running concurrently + # with another dnf process doesn't delete required data (packages). + clean_command_lock = _Default(CleanCommandLock.NONE, tuple(CleanCommandLock)) diff --git a/dnf/cli/main.py b/dnf/cli/main.py index c63baaaa..2537a46d 100644 --- a/dnf/cli/main.py +++ b/dnf/cli/main.py @@ -24,6 +24,7 @@ from __future__ import absolute_import from __future__ import unicode_literals from dnf.conf import Conf from dnf.cli.cli import Cli +from dnf.cli.demand import CleanCommandLock from dnf.cli.option_parser import OptionParser from dnf.i18n import ucd from dnf.cli.utils import show_lock_owner @@ -33,6 +34,7 @@ import dnf.cli import dnf.cli.cli import dnf.cli.option_parser import dnf.exceptions +import dnf.lock import dnf.i18n import dnf.logging import dnf.util @@ -43,6 +45,7 @@ import logging import os import os.path import sys +from contextlib import nullcontext logger = logging.getLogger("dnf") @@ -118,52 +121,63 @@ def cli_run(cli, base): else: f.close() - try: - cli.run() - except dnf.exceptions.LockError: - raise - except (IOError, OSError) as e: - return ex_IOError(e) + lock = None + if cli.demands.clean_command_lock == CleanCommandLock.NONE: + lock = nullcontext() + elif cli.demands.clean_command_lock == CleanCommandLock.READ: + lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, True) + elif cli.demands.clean_command_lock == CleanCommandLock.WRITE: + lock = dnf.lock.build_clean_command_lock(base.conf.cachedir, base.conf.exit_on_lock, False) + else: + raise RuntimeError('Invalid demands.clean_command_lock: %s' % cli.demands.clean_command_lock) - if cli.demands.resolving: + with lock: try: - ret = resolving(cli, base) - except dnf.exceptions.DepsolveError as e: - ex_Error(e) - msg = "" - if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True): - msg += _("try to add '{}' to command line to replace conflicting " - "packages").format("--allowerasing") - if cli.base.conf.strict: - if not msg: - msg += _("try to add '{}' to skip uninstallable packages").format( - "--skip-broken") - else: - msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken") - if cli.base.conf.best: - prio = cli.base.conf._get_priority("best") - if prio <= dnf.conf.PRIO_MAINCONFIG: + cli.run() + except dnf.exceptions.LockError: + raise + except (IOError, OSError) as e: + return ex_IOError(e) + + if cli.demands.resolving: + try: + ret = resolving(cli, base) + except dnf.exceptions.DepsolveError as e: + ex_Error(e) + msg = "" + if not cli.demands.allow_erasing and base._goal.problem_conflicts(available=True): + msg += _("try to add '{}' to command line to replace conflicting " + "packages").format("--allowerasing") + if cli.base.conf.strict: if not msg: - msg += _("try to add '{}' to use not only best candidate packages").format( - "--nobest") + msg += _("try to add '{}' to skip uninstallable packages").format( + "--skip-broken") else: - msg += _(" or '{}' to use not only best candidate packages").format( - "--nobest") - if base._goal.file_dep_problem_present() and 'filelists' not in cli.base.conf.optional_metadata_types: - if not msg: - msg += _("try to add '{}' to load additional filelists metadata").format( - "--setopt=optional_metadata_types=filelists") - else: - msg += _(" or '{}' to load additional filelists metadata").format( - "--setopt=optional_metadata_types=filelists") - if msg: - logger.info("({})".format(msg)) - raise - if ret: - return ret - - cli.command.run_transaction() - return cli.demands.success_exit_status + msg += _(" or '{}' to skip uninstallable packages").format("--skip-broken") + if cli.base.conf.best: + prio = cli.base.conf._get_priority("best") + if prio <= dnf.conf.PRIO_MAINCONFIG: + if not msg: + msg += _("try to add '{}' to use not only best candidate packages").format( + "--nobest") + else: + msg += _(" or '{}' to use not only best candidate packages").format( + "--nobest") + if base._goal.file_dep_problem_present() and 'filelists' not in cli.base.conf.optional_metadata_types: + if not msg: + msg += _("try to add '{}' to load additional filelists metadata").format( + "--setopt=optional_metadata_types=filelists") + else: + msg += _(" or '{}' to load additional filelists metadata").format( + "--setopt=optional_metadata_types=filelists") + if msg: + logger.info("({})".format(msg)) + raise + if ret: + return ret + + cli.command.run_transaction() + return cli.demands.success_exit_status def resolving(cli, base): diff --git a/dnf/lock.py b/dnf/lock.py index cb189c35..aae8a51b 100644 --- a/dnf/lock.py +++ b/dnf/lock.py @@ -63,6 +63,11 @@ def build_log_lock(logdir, exit_on_lock): 'log', not exit_on_lock) +def build_clean_command_lock(cachedir, exit_on_lock, read): + return FileLock(os.path.join(_fit_lock_dir(cachedir), 'clean_command.lock'), + 'clean command', not exit_on_lock, read) + + class ProcessLock(object): def __init__(self, target, description, blocking=False): self.blocking = blocking diff --git a/tests/cli/test_demand.py b/tests/cli/test_demand.py index fc9cfcec..7f99100a 100644 --- a/tests/cli/test_demand.py +++ b/tests/cli/test_demand.py @@ -21,6 +21,7 @@ from __future__ import absolute_import from __future__ import unicode_literals import dnf.cli.demand +from dnf.cli.demand import CleanCommandLock import tests.support @@ -42,6 +43,7 @@ class DemandTest(tests.support.TestCase): self.assertFalse(demands.sack_activation) self.assertFalse(demands.root_user) self.assertEqual(demands.success_exit_status, 0) + self.assertEqual(demands.clean_command_lock, CleanCommandLock.NONE) def test_independence(self): d1 = dnf.cli.demand.DemandSheet() -- 2.54.0