Skip to content

refactor(atspi): replace per-call subprocess with multiprocessing.Pool worker#24

Open
isac322 wants to merge 4 commits into
mainfrom
overhaul/pr3-atspi-pool
Open

refactor(atspi): replace per-call subprocess with multiprocessing.Pool worker#24
isac322 wants to merge 4 commits into
mainfrom
overhaul/pr3-atspi-pool

Conversation

@isac322
Copy link
Copy Markdown
Owner

@isac322 isac322 commented May 5, 2026

Why

Each AT-SPI call ran subprocess.run([sys.executable, "-m", "kwin_mcp.accessibility"]) (~700ms per call). PyGObject Atspi caches the D-Bus connection process-wide, so isolation is required, but a fresh subprocess per call is overkill.

What

  • src/kwin_mcp/accessibility_worker.py (NEW): spawn-context Pool worker entry
    • _init_atspi_worker(dbus_address): pin env → deferred from gi.repository import Atspi → validate Atspi.get_desktop(0).get_child_count() >= 0. Raises RuntimeError(...) on -1.
    • do_atspi_op(op, **kwargs) -> dict: delegates to the existing _handle_request dispatcher
    • Must never be imported by the parent process — enforced by CI guard 3 (PR 0)
  • src/kwin_mcp/core.py:
    • _ensure_atspi_pool(): lazy multiprocessing.get_context("spawn").Pool(processes=1, initializer=_init_atspi_worker) (deferred import inside method)
    • _run_atspi(op, **kwargs): apply_async().get(timeout=30) + IPC death catch (EOFError/BrokenPipeError/ConnectionResetError/OSError errno∈{32,104}) + 1 retry
    • _teardown_atspi_pool(): close → join 5s → terminate → join 2s → SIGKILL → join 1s. multiprocessing.pool.Pool.join lacks a timeout kwarg, so a threading.Thread wrapper is used.
    • Defensive __del__ cleanup + explicit invocation in session_stop
  • tests/integration/test_atspi_pool.py: 6 lifecycle tests (cold start ≤1.5s, warm <200ms, external SIGKILL recovery, init failure, terminate within 7s with hung worker, no zombies)

Performance

  • Cold start: ≤1.5s (Pool spawn + Atspi import)
  • Warm calls: <200ms (vs ~700ms previously)

Verify

uv run pytest -m kwin tests/integration/test_atspi_pool.py   # → 6 passed

Series: Stacked on top of PR 2. Base = overhaul/pr2-dbus.

@isac322 isac322 force-pushed the overhaul/pr3-atspi-pool branch from de236e3 to 209b892 Compare May 5, 2026 13:02
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent)

Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
@isac322 isac322 changed the base branch from overhaul/pr2-dbus to launch/backend-overhaul May 5, 2026 13:34
@isac322 isac322 force-pushed the overhaul/pr3-atspi-pool branch from 209b892 to 4bbc1be Compare May 5, 2026 13:44
@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 5, 2026

📝 Docs & SEO Review

Source files changed in this PR:

pyproject.toml
src/kwin_mcp/accessibility_worker.py
src/kwin_mcp/core.py
src/kwin_mcp/dbus_args.py
src/kwin_mcp/server.py

Consistency check results:

✅  All documentation/plugin SEO checks passed.

Run @docs-seo in Claude Code to perform a full documentation review.

@isac322 isac322 force-pushed the overhaul/pr3-atspi-pool branch from 4bbc1be to b77379c Compare May 5, 2026 14:00
isac322 added 3 commits May 5, 2026 23:12
- Add dbus_args.py: full dbus-send recursive-descent parser (12 basic
  types + array/dict/variant containers) returning dbus-python types.
- Add typed-JSON args dispatcher: args list now accepts both legacy
  'type:value' strings and {type, value} dicts, mixed in one call.
  Schema widened (additive): list[str] -> list[str | dict].
- core.py:dbus_call now uses dbus.bus.BusConnection + Interface in-process
  (no subprocess). Errors surface as 'D-Bus error: <name>: <msg>'.
- _format_dbus_result: void -> empty, primitive -> bare, container -> JSON.
- 80 unit tests for the parser; 6 integration tests against virtual KWin.
- docs/design/dbus-call-call-sites.md documents the public-tool contract.

Pre-commit: ruff/ty clean, ci_guards pass, 87 tests green.
…l worker

Replaces per-call `subprocess.run([sys.executable, '-m', 'kwin_mcp.accessibility', ...])`
with a long-lived spawn-context multiprocessing.Pool(processes=1) worker.

Key changes:
- New module `kwin_mcp.accessibility_worker` containing picklable top-level
  callables `_init_atspi_worker` and `do_atspi_op`. Module is NEVER imported
  at module-top by core.py (CI guard 3 enforces this); deferred imports inside
  `_ensure_atspi_pool` and `_run_atspi` only.
- Worker init validates `Atspi.get_desktop(0).get_child_count() >= 0` against
  the bus address and raises RuntimeError with the bus address on failure.
- Pool teardown protocol survives hung workers within 7s:
  close → join 5s → terminate → join 2s → SIGKILL → join 1s.
- IPC error handling catches `(EOFError, BrokenPipeError, ConnectionResetError)`
  and `OSError errno in (32, 104)` for transparent recovery; one retry, then
  re-raise.
- Defensive `__del__` teardown swallows exceptions for partial-init safety.
- AGENT_EXEC_APPROVAL=1 prefix used for all uv invocations during this change
  per session permission.

Performance:
- Cold start ≤ 1.5s (verified)
- Warm calls < 200ms (verified, plan-mandated threshold)
- vs ~700ms per subprocess on the previous design

Tests:
- tests/integration/test_atspi_pool.py — 6 tests, all passing under
  `AGENT_EXEC_APPROVAL=1 uv run pytest -m kwin`:
  cold start <1.5s; warm call <200ms; recovers from external SIGKILL;
  init failure surfaces RuntimeError with bus address; teardown within 7s
  even with SIGSTOPped worker; no zombie processes after session_stop.

Plan tasks: 11, 12, 13, 14, 15.
@isac322 isac322 force-pushed the overhaul/pr3-atspi-pool branch from b77379c to 3f248bc Compare May 5, 2026 14:12
Base automatically changed from launch/backend-overhaul to main May 29, 2026 02:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant