feat(screenshot): add capability probe + ScreenShot2 D-Bus primary backend#25
Open
isac322 wants to merge 5 commits into
Open
feat(screenshot): add capability probe + ScreenShot2 D-Bus primary backend#25isac322 wants to merge 5 commits into
isac322 wants to merge 5 commits into
Conversation
d93cd8e to
eeddaf6
Compare
de236e3 to
209b892
Compare
eeddaf6 to
5ffd09e
Compare
Ultraworked with [Sisyphus](https://github.com/code-yeongyu/oh-my-openagent) Co-authored-by: Sisyphus <clio-agent@sisyphuslabs.ai>
5ffd09e to
c6896c2
Compare
📝 Docs & SEO ReviewSource files changed in this PR: Consistency check results:
|
c6896c2 to
594617a
Compare
- 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.
594617a to
d49118e
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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):CaptureAreaover D-Bus → on failure, try a real_capture_via_spectacle→ return the result"screenshot2_dbus" | "spectacle_cli" | "unavailable"src/kwin_mcp/session.py: probe runs inSession.start()/LiveSession.__init__→ stored inSessionInfo.screenshot_backendsrc/kwin_mcp/screenshot.py:capture_screenshot_to_file(..., screenshot_backend="unavailable"):matchdispatch (D-Bus / spectacle / RuntimeError)capture_frame_burst(..., screenshot_backend="unavailable"): same dispatchsrc/kwin_mcp/core.py: both call sites now forwardscreenshot_backend=info.screenshot_backendtests/integration/test_screenshot_backends.py: 4 tests (probe outcome, PNG generation, burst, RuntimeError on unavailable)Behavior
screenshot2_dbus(different environment from PR 1's spike host — probe is environment-adaptive)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 passedSeries: Stacked on top of PR 3. Base =
overhaul/pr3-atspi-pool.