Skip to content

feat(screenshot): add capability probe + ScreenShot2 D-Bus primary backend#25

Open
isac322 wants to merge 5 commits into
mainfrom
overhaul/pr4-screenshot
Open

feat(screenshot): add capability probe + ScreenShot2 D-Bus primary backend#25
isac322 wants to merge 5 commits into
mainfrom
overhaul/pr4-screenshot

Conversation

@isac322
Copy link
Copy Markdown
Owner

@isac322 isac322 commented May 5, 2026

Why

Make screenshot backend selection environment-adaptive. PR 1's spike found ScreenShot2 fails on this host, but other environments may succeed — eager probe followed by dispatch.

What

  • src/kwin_mcp/screenshot.py:_probe_screenshot_capability(dbus_address, wayland_socket):
    • Try a 1×1 CaptureArea over D-Bus → on failure, try a real _capture_via_spectacle → return the result
    • Returns: "screenshot2_dbus" | "spectacle_cli" | "unavailable"
    • 10s total cap (5s D-Bus + 5s spectacle)
  • src/kwin_mcp/session.py: probe runs in Session.start() / LiveSession.__init__ → stored in SessionInfo.screenshot_backend
  • src/kwin_mcp/screenshot.py:
    • capture_screenshot_to_file(..., screenshot_backend="unavailable"): match dispatch (D-Bus / spectacle / RuntimeError)
    • capture_frame_burst(..., screenshot_backend="unavailable"): same dispatch
  • src/kwin_mcp/core.py: both call sites now forward screenshot_backend=info.screenshot_backend
  • tests/integration/test_screenshot_backends.py: 4 tests (probe outcome, PNG generation, burst, RuntimeError on unavailable)

Behavior

  • On the host where this PR was developed: probe selects screenshot2_dbus (different environment from PR 1's spike host — probe is environment-adaptive)
  • On unavailable: RuntimeError("No screenshot backend available; install spectacle or fix KWin EglBackend")

Verify

uv run pytest -m kwin tests/integration/test_screenshot_backends.py   # → 4 passed

Series: Stacked on top of PR 3. Base = overhaul/pr3-atspi-pool.

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/pr3-atspi-pool to launch/backend-overhaul May 5, 2026 13:34
@isac322 isac322 force-pushed the overhaul/pr4-screenshot branch from 5ffd09e to c6896c2 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/screenshot.py
src/kwin_mcp/server.py
src/kwin_mcp/session.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/pr4-screenshot branch from c6896c2 to 594617a Compare May 5, 2026 14:00
isac322 added 4 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.
…ckend

PR 4 lands the screenshot-backend overhaul as a single atomic unit.

What changed:
- New `_probe_screenshot_capability(dbus_address, wayland_socket)` runs at
  session_start. Tries a 1x1 `CaptureArea` via in-process ScreenShot2 D-Bus,
  falls back to a real `spectacle` capture, returns one of:
  "screenshot2_dbus" | "spectacle_cli" | "unavailable".
  10s wall-clock cap; bus address GUID redacted in logs.
- `SessionInfo.screenshot_backend: str = "unavailable"` carries the probe
  result; `Session.start` and `LiveSession.__init__` populate it via a
  helper that swallows probe exceptions and warns on failure.
- `capture_screenshot_to_file` and `capture_frame_burst` now accept
  `screenshot_backend` as an explicit keyword argument (no global state).
  Dispatch is a `match` on the three values; "unavailable" raises
  `RuntimeError("No screenshot backend available; install spectacle or
  fix KWin EglBackend")`.
- Two `core.py` call sites pass `screenshot_backend=info.screenshot_backend`.

Tests:
- New `tests/integration/test_screenshot_backends.py` (4 tests, marked
  `@pytest.mark.kwin`):
  1. probe returns one of the three documented strings
  2. screenshot writes a real PNG (signature byte-checked)
  3. burst capture preserves backend invariant + valid PNGs per frame
  4. forcing backend="unavailable" raises with the exact message
  All 4 pass against a real virtual KWin session.

Plan tasks: 16, 17, 18, 19.
@isac322 isac322 force-pushed the overhaul/pr4-screenshot branch from 594617a to d49118e 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