diff --git a/plinth/diagnostic_check.py b/plinth/diagnostic_check.py
index d3d6164d2..a280acd6c 100644
--- a/plinth/diagnostic_check.py
+++ b/plinth/diagnostic_check.py
@@ -17,6 +17,7 @@ DiagnosticCheckParameters: TypeAlias = dict[str, str | int | bool | None]
class Result(StrEnum):
"""The result of a diagnostic check."""
NOT_DONE = 'not_done'
+ SKIPPED = 'skipped'
PASSED = 'passed'
WARNING = 'warning'
FAILED = 'failed'
diff --git a/plinth/modules/diagnostics/__init__.py b/plinth/modules/diagnostics/__init__.py
index 8fd453db3..d3369bbd0 100644
--- a/plinth/modules/diagnostics/__init__.py
+++ b/plinth/modules/diagnostics/__init__.py
@@ -96,6 +96,7 @@ def _run_on_all_enabled_modules():
# Four result strings returned by tests, mark for translation and
# translate later.
+ gettext_noop('skipped')
gettext_noop('passed')
gettext_noop('failed')
gettext_noop('error')
diff --git a/plinth/modules/diagnostics/templates/diagnostics_results.html b/plinth/modules/diagnostics/templates/diagnostics_results.html
index 62d786ec8..1dcc0ba5b 100644
--- a/plinth/modules/diagnostics/templates/diagnostics_results.html
+++ b/plinth/modules/diagnostics/templates/diagnostics_results.html
@@ -23,6 +23,8 @@
{% trans result.result %}
{% elif result.result == 'error' or result.result == 'warning' %}
{% trans result.result %}
+ {% elif result.result == 'skipped' %}
+ {% trans result.result %}
{% else %}
{{ result.result }}
{% endif %}
diff --git a/plinth/modules/upgrades/__init__.py b/plinth/modules/upgrades/__init__.py
index 14e7a49c7..56f1553ec 100644
--- a/plinth/modules/upgrades/__init__.py
+++ b/plinth/modules/upgrades/__init__.py
@@ -11,9 +11,10 @@ from django.utils.translation import gettext_noop
import plinth
from plinth import app as app_module
-from plinth import cfg, glib, kvstore, menu
+from plinth import action_utils, cfg, glib, kvstore, menu, package
from plinth.config import DropinConfigs
from plinth.daemon import RelatedDaemon
+from plinth.diagnostic_check import DiagnosticCheck, Result
from plinth.modules.backups.components import BackupRestore
from plinth.package import Packages
@@ -159,6 +160,12 @@ class UpgradesApp(app_module.App):
# install and on version increment.
setup_repositories(None)
+ def diagnose(self) -> list[DiagnosticCheck]:
+ """Run diagnostics and return the results."""
+ results = super().diagnose()
+ results.append(_diagnose_held_packages())
+ return results
+
def setup_repositories(_):
"""Setup apt repositories for backports."""
@@ -296,3 +303,19 @@ def test_dist_upgrade():
"""Test dist-upgrade from stable to testing."""
if can_test_dist_upgrade():
try_start_dist_upgrade(test=True)
+
+
+def _diagnose_held_packages():
+ """Check if any packages have holds."""
+ check = DiagnosticCheck('upgrades-package-holds',
+ gettext_noop('Check for package holds'),
+ Result.NOT_DONE)
+ if (package.is_package_manager_busy()
+ or action_utils.service_is_running('freedombox-dist-upgrade')):
+ check.result = Result.SKIPPED
+ return check
+
+ output = subprocess.check_output(['apt-mark', 'showhold']).decode().strip()
+ held_packages = output.split()
+ check.result = Result.FAILED if held_packages else Result.PASSED
+ return check