Defer office runtime preparation during startup

Alessandro committed May 4, 2026 at 23:04 UTC d3265139833afdb0b17042cc40e4f641dd9be3aa
2 files changed +60 -6
plugins/_office/extensions/python/startup_migration/_20_office_routes.py
+36 -5
@@ -1,16 +1,47 @@
1 from __future__ import annotations
2
3 +import threading
4 +from typing import Any
5 +
6 from helpers.extension import Extension
7 from helpers.print_style import PrintStyle
8 from plugins._office import hooks
9 from plugins._office.helpers import libreoffice_desktop_routes
10
11
12 +_startup_preparation_thread: threading.Thread | None = None
13 +
14 +
15 class OfficeStartupCleanup(Extension):
16 def execute(self, **kwargs):
17 libreoffice_desktop_routes.install_route_hooks()
12 - result = hooks.cleanup_stale_runtime_state()
13 - if result.get("errors"):
14 - PrintStyle.warning("Office runtime preparation reported errors:", result["errors"])
15 - elif result.get("installed") or result.get("removed"):
16 - PrintStyle.info("Office runtime prepared:", result)
18 + _start_background_runtime_preparation()
19 +
20 +
21 +def _start_background_runtime_preparation() -> threading.Thread:
22 + global _startup_preparation_thread
23 +
24 + if _startup_preparation_thread and _startup_preparation_thread.is_alive():
25 + return _startup_preparation_thread
26 +
27 + _startup_preparation_thread = threading.Thread(
28 + target=_prepare_runtime_safely,
29 + name="a0-office-runtime-preparation",
30 + daemon=True,
31 + )
32 + _startup_preparation_thread.start()
33 + return _startup_preparation_thread
34 +
35 +
36 +def _prepare_runtime_safely() -> None:
37 + try:
38 + _log_runtime_preparation_result(hooks.cleanup_stale_runtime_state())
39 + except Exception as exc:
40 + PrintStyle.warning("Office runtime preparation failed:", exc)
41 +
42 +
43 +def _log_runtime_preparation_result(result: dict[str, Any]) -> None:
44 + if result.get("errors"):
45 + PrintStyle.warning("Office runtime preparation reported errors:", result["errors"])
46 + elif result.get("installed") or result.get("removed"):
47 + PrintStyle.info("Office runtime prepared:", result)
tests/test_office_document_store.py
+24 -1
@@ -686,6 +686,8 @@ def test_cleanup_hook_removes_stale_runtime_state_idempotently(tmp_path, monkeyp
686
687 def test_office_startup_defers_persistent_desktop_runtime(monkeypatch):
688 calls = []
689 + cleanup_calls = []
690 + started_threads = []
691 routes_module = types.ModuleType("plugins._office.helpers.libreoffice_desktop_routes")
692 routes_module.install_route_hooks = lambda: calls.append("routes")
693 monkeypatch.setitem(sys.modules, "plugins._office.helpers.libreoffice_desktop_routes", routes_module)
@@ -700,14 +702,35 @@ def test_office_startup_defers_persistent_desktop_runtime(monkeypatch):
702 monkeypatch.setattr(
703 office_startup.hooks,
704 "cleanup_stale_runtime_state",
703 - lambda: {"ok": True, "errors": [], "installed": [], "removed": []},
705 + lambda: cleanup_calls.append("cleanup") or {"ok": True, "errors": [], "installed": [], "removed": []},
706 )
707
708 + class FakeThread:
709 + def __init__(self, *, target, name, daemon):
710 + self.target = target
711 + self.name = name
712 + self.daemon = daemon
713 +
714 + def is_alive(self):
715 + return False
716 +
717 + def start(self):
718 + started_threads.append(self)
719 +
720 + monkeypatch.setattr(office_startup.threading, "Thread", FakeThread)
721 +
722 office_startup.OfficeStartupCleanup(agent=None).execute()
723
724 assert calls == ["routes"]
725 + assert cleanup_calls == []
726 + assert len(started_threads) == 1
727 + assert started_threads[0].name == "a0-office-runtime-preparation"
728 + assert started_threads[0].daemon is True
729 assert not hasattr(office_startup, "libreoffice_desktop")
730
731 + started_threads[0].target()
732 + assert cleanup_calls == ["cleanup"]
733 +
734
735 def test_cleanup_hook_reruns_when_stale_packages_exist_after_old_marker(tmp_path, monkeypatch):
736 marker = tmp_path / "state" / "cleanup.done"