Clean up legacy runtime artifacts
Remove stale runtime directories and expand retired package coverage for startup/self-update cleanup. Discover installed collaboraoffice* split packages dynamically so future package-name changes are still purged, and extend Office cleanup tests for the legacy /opt paths, known leftover packages, idempotency, and marker rerun behavior.
Alessandro committed
May 7, 2026 at 20:20 UTC
fa0d2beaf2c15dedbc84c16959a9f0fbbefb9914
2 files changed
+102
-5
plugins/_office/hooks.py
+35
-2
@@ -22,6 +22,8 @@ RETIRED_WEB_APT_KEYRING_FILE = Path("/etc/apt/keyrings/collaboraonline-release-k
22
RETIRED_WEB_SUPERVISOR_FILE = Path("/etc/supervisor/conf.d/a0_office_collabora.conf")
23
RETIRED_WEB_SUPERVISOR_PROGRAM = "a0_office_collabora"
24
RETIRED_WEB_RUNTIME_DIRS = [
25
+ Path("/opt/cool"),
26
+ Path("/opt/collaboraoffice"),
27
Path("/a0/tmp/_office/collabora"),
28
Path("/a0/usr/plugins/_office/collabora"),
29
PROJECT_ROOT / "tmp" / "_office" / "collabora",
@@ -32,7 +34,9 @@ RETIRED_WEB_PACKAGES = (
34
"coolwsd-deprecated",
35
"code-brand",
36
"collaboraoffice",
37
+ "collaboraoffice-ure",
38
"collaboraofficebasis-calc",
39
+ "collaboraofficebasis-core",
40
"collaboraofficebasis-draw",
41
"collaboraofficebasis-en-us",
42
"collaboraofficebasis-extension-pdf-import",
@@ -41,6 +45,7 @@ RETIRED_WEB_PACKAGES = (
45
"collaboraofficebasis-impress",
46
"collaboraofficebasis-math",
47
"collaboraofficebasis-ooolinguistic",
48
+ "collaboraofficebasis-ooofonts",
49
"collaboraofficebasis-writer",
50
)
51
RUNTIME_PACKAGES = (
@@ -90,7 +95,7 @@ def cleanup_stale_runtime_state(force: bool = False) -> dict[str, Any]:
95
]
96
if path.exists() or path.is_symlink()
97
]
93
- retired_web_packages = _installed_packages(RETIRED_WEB_PACKAGES)
98
+ retired_web_packages = _installed_retired_web_packages()
99
cleanup_needed = force or not CLEANUP_MARKER.exists() or bool(retired_web_paths or retired_web_packages)
100
101
if cleanup_needed:
@@ -294,6 +299,34 @@ def _installed_packages(packages: tuple[str, ...]) -> list[str]:
299
return [package for package in packages if _package_installed(package)]
300
301
302
+def _installed_retired_web_packages() -> list[str]:
303
+ packages = [
304
+ *_installed_packages(RETIRED_WEB_PACKAGES),
305
+ *_installed_collabora_packages(),
306
+ ]
307
+ return list(dict.fromkeys(packages))
308
+
309
+
310
+def _installed_collabora_packages() -> list[str]:
311
+ if not shutil.which("dpkg-query"):
312
+ return []
313
+
314
+ result = subprocess.run(
315
+ ["dpkg-query", "-W", "-f=${binary:Package}\t${Status}\n", "collaboraoffice*"],
316
+ check=False,
317
+ text=True,
318
+ capture_output=True,
319
+ timeout=15,
320
+ )
321
+
322
+ packages: list[str] = []
323
+ for line in result.stdout.splitlines():
324
+ package, _, status = line.partition("\t")
325
+ if package.startswith("collaboraoffice") and "install ok installed" in status:
326
+ packages.append(package)
327
+ return packages
328
+
329
+
330
def _purge_packages(
331
removed: list[str],
332
errors: list[str],
@@ -302,7 +335,7 @@ def _purge_packages(
335
) -> None:
336
if os.geteuid() != 0 or not shutil.which("apt-get") or not shutil.which("dpkg-query"):
337
return
305
- installed = installed_packages if installed_packages is not None else _installed_packages(RETIRED_WEB_PACKAGES)
338
+ installed = installed_packages if installed_packages is not None else _installed_retired_web_packages()
339
if not installed:
340
return
341
result = _run_apt_command(["apt-get", "purge", "-y", *installed], timeout=180)
tests/test_office_document_store.py
+67
-3
@@ -1098,6 +1098,7 @@ def _isolate_office_cleanup_hook(monkeypatch, tmp_path):
1098
monkeypatch.setattr(hooks, "RETIRED_WEB_SUPERVISOR_FILE", tmp_path / "missing.conf")
1099
monkeypatch.setattr(hooks, "RETIRED_WEB_RUNTIME_DIRS", [])
1100
monkeypatch.setattr(hooks, "CLEANUP_MARKER", tmp_path / "state" / "cleanup.done")
1101
+ monkeypatch.setattr(hooks, "_installed_retired_web_packages", lambda: [])
1102
monkeypatch.setattr(hooks, "_installed_packages", lambda packages: [])
1103
monkeypatch.setattr(hooks, "_kill_old_processes", lambda errors: None)
1104
monkeypatch.setattr(hooks, "_ensure_runtime_dependencies", lambda installed, errors: None)
@@ -1175,6 +1176,52 @@ def test_office_hook_desktop_compat_forwards_runtime_result(monkeypatch):
1176
assert errors == ["desktop error"]
1177
1178
1179
+def test_cleanup_hook_targets_legacy_collabora_runtime_artifacts():
1180
+ assert Path("/opt/cool") in hooks.RETIRED_WEB_RUNTIME_DIRS
1181
+ assert Path("/opt/collaboraoffice") in hooks.RETIRED_WEB_RUNTIME_DIRS
1182
+ assert {
1183
+ "collaboraoffice-ure",
1184
+ "collaboraofficebasis-core",
1185
+ "collaboraofficebasis-ooofonts",
1186
+ }.issubset(hooks.RETIRED_WEB_PACKAGES)
1187
+
1188
+
1189
+def test_installed_retired_web_packages_discovers_collabora_split_packages(monkeypatch):
1190
+ monkeypatch.setattr(
1191
+ hooks.shutil,
1192
+ "which",
1193
+ lambda name: "/usr/bin/dpkg-query" if name == "dpkg-query" else "",
1194
+ )
1195
+ monkeypatch.setattr(
1196
+ hooks,
1197
+ "_package_installed",
1198
+ lambda package: package in {"coolwsd", "collaboraofficebasis-core"},
1199
+ )
1200
+
1201
+ def fake_run(command, **kwargs):
1202
+ assert command == ["dpkg-query", "-W", "-f=${binary:Package}\t${Status}\n", "collaboraoffice*"]
1203
+ return types.SimpleNamespace(
1204
+ returncode=0,
1205
+ stdout=(
1206
+ "collaboraofficebasis-core\tinstall ok installed\n"
1207
+ "collaboraofficebasis-extra-future\tinstall ok installed\n"
1208
+ "collaboraofficebasis-config-files\tdeinstall ok config-files\n"
1209
+ "notcollaboraoffice\tinstall ok installed\n"
1210
+ ),
1211
+ stderr="",
1212
+ )
1213
+
1214
+ monkeypatch.setattr(hooks.subprocess, "run", fake_run)
1215
+
1216
+ packages = hooks._installed_retired_web_packages()
1217
+
1218
+ assert packages == [
1219
+ "coolwsd",
1220
+ "collaboraofficebasis-core",
1221
+ "collaboraofficebasis-extra-future",
1222
+ ]
1223
+
1224
+
1225
def test_cleanup_hook_delegates_desktop_runtime_for_legacy_self_update(tmp_path, monkeypatch):
1226
_isolate_office_cleanup_hook(monkeypatch, tmp_path)
1227
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", tmp_path / "usr" / "_office" / "documents")
@@ -1202,6 +1249,8 @@ def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeyp
1249
keyring = tmp_path / "keyrings" / "retired.gpg"
1250
supervisor = tmp_path / "supervisor" / "retired.conf"
1251
runtime_dir = tmp_path / "runtime"
1252
+ legacy_cool_dir = tmp_path / "opt" / "cool"
1253
+ legacy_collabora_dir = tmp_path / "opt" / "collaboraoffice"
1254
marker = tmp_path / "state" / "cleanup.done"
1255
1256
for path in (source, keyring, supervisor):
@@ -1209,14 +1258,19 @@ def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeyp
1258
path.write_text("old\n", encoding="utf-8")
1259
(runtime_dir / "nested").mkdir(parents=True, exist_ok=True)
1260
(runtime_dir / "nested" / "state.txt").write_text("old\n", encoding="utf-8")
1261
+ (legacy_cool_dir / "state").mkdir(parents=True, exist_ok=True)
1262
+ (legacy_cool_dir / "state" / "cool.txt").write_text("old\n", encoding="utf-8")
1263
+ (legacy_collabora_dir / "program").mkdir(parents=True, exist_ok=True)
1264
+ (legacy_collabora_dir / "program" / "office.txt").write_text("old\n", encoding="utf-8")
1265
1266
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_SOURCE_FILE", source)
1267
monkeypatch.setattr(hooks, "RETIRED_WEB_APT_KEYRING_FILE", keyring)
1268
monkeypatch.setattr(hooks, "RETIRED_WEB_SUPERVISOR_FILE", supervisor)
1216
- monkeypatch.setattr(hooks, "RETIRED_WEB_RUNTIME_DIRS", [runtime_dir])
1269
+ monkeypatch.setattr(hooks, "RETIRED_WEB_RUNTIME_DIRS", [runtime_dir, legacy_cool_dir, legacy_collabora_dir])
1270
monkeypatch.setattr(hooks, "CLEANUP_MARKER", marker)
1271
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", tmp_path / "usr" / "_office" / "documents")
1272
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [])
1273
+ monkeypatch.setattr(hooks, "_installed_retired_web_packages", lambda: [])
1274
monkeypatch.setattr(hooks, "_installed_packages", lambda packages: [])
1275
monkeypatch.setattr(hooks, "_kill_old_processes", lambda errors: None)
1276
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", lambda installed, removed, warnings, errors: None)
@@ -1243,6 +1297,8 @@ def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeyp
1297
assert not keyring.exists()
1298
assert not supervisor.exists()
1299
assert not runtime_dir.exists()
1300
+ assert not legacy_cool_dir.exists()
1301
+ assert not legacy_collabora_dir.exists()
1302
assert marker.exists()
1303
1304
@@ -1301,7 +1357,14 @@ def test_cleanup_hook_reruns_when_stale_packages_exist_after_old_marker(tmp_path
1357
monkeypatch.setattr(hooks, "CLEANUP_MARKER", marker)
1358
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", tmp_path / "usr" / "_office" / "documents")
1359
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [])
1304
- monkeypatch.setattr(hooks, "_installed_packages", lambda packages: ["coolwsd"])
1360
+ retired_web_packages = [
1361
+ "coolwsd",
1362
+ "collaboraoffice-ure",
1363
+ "collaboraofficebasis-core",
1364
+ "collaboraofficebasis-ooofonts",
1365
+ ]
1366
+ monkeypatch.setattr(hooks, "_installed_retired_web_packages", lambda: retired_web_packages)
1367
+ monkeypatch.setattr(hooks, "_installed_packages", lambda packages: [])
1368
monkeypatch.setattr(hooks, "_ensure_runtime_dependencies", lambda installed, errors: None)
1369
monkeypatch.setattr(hooks, "_kill_old_processes", lambda errors: None)
1370
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", lambda installed, removed, warnings, errors: None)
@@ -1314,7 +1377,7 @@ def test_cleanup_hook_reruns_when_stale_packages_exist_after_old_marker(tmp_path
1377
result = hooks.cleanup_stale_runtime_state()
1378
1379
assert result["skipped"] is False
1317
- assert result["removed"] == ["coolwsd"]
1380
+ assert result["removed"] == retired_web_packages
1381
1382
1383
def test_cleanup_hook_removes_retired_supervisor_program_after_marker(tmp_path, monkeypatch):
@@ -1330,6 +1393,7 @@ def test_cleanup_hook_removes_retired_supervisor_program_after_marker(tmp_path,
1393
monkeypatch.setattr(hooks, "CLEANUP_MARKER", marker)
1394
monkeypatch.setattr(hooks, "DOCUMENT_STATE_DIR", tmp_path / "usr" / "_office" / "documents")
1395
monkeypatch.setattr(hooks, "LEGACY_DOCUMENT_STATE_DIRS", [])
1396
+ monkeypatch.setattr(hooks, "_installed_retired_web_packages", lambda: [])
1397
monkeypatch.setattr(hooks, "_installed_packages", lambda packages: [])
1398
monkeypatch.setattr(hooks, "_ensure_runtime_dependencies", lambda installed, errors: None)
1399
monkeypatch.setattr(hooks, "_ensure_desktop_runtime_compat", lambda installed, removed, warnings, errors: None)