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"