Skip to content

fix: launch_app reports activation of a running instance instead of guessing - #11

Open
timrichardson wants to merge 5 commits into
tristanmuzzu:mainfrom
timrichardson:fix/launch-activation
Open

timrichardson wants to merge 5 commits into
tristanmuzzu:mainfrom
timrichardson:fix/launch-activation

Conversation

@timrichardson

Copy link
Copy Markdown

Closes #10

The bug (#10)

For a DBusActivatable single-instance app, gio launch only activates the running instance. The new-window-id arrival test then either timed out over a live app (Arm B) or "confirmed" off an unrelated window change — green-lighting writes into the existing document's buffer (Arm A). The bystander variant: any new window of any app confirmed the launch.

The change

  • _desktop_facts(): DBusActivatable, Exec basename, StartupWMClass parsed from the [Desktop Entry] section (Desktop Action Exec lines ignored).
  • Honest activation return. Before any spawn: if the app is DBus-activatable and a window matching its identity is already up, return {activated: true, already_running: true, windows: [...]} — no gio call, no wait. Fixes both arms at once.
  • wm_class normalization. Identity matching collapses the spelling gap (gnome-text-editororg.gnome.TextEditor) via punctuation/case folding plus containment both ways.
  • Scoped confirmation. The wait loop requires the confirming window's wm_class to match the launched app; raw command launches keep the any-new-window rule (no desktop identity to match).
  • Windowless-service fall-through. A lingering --gapplication-service owns no window, so it is NOT "already running" — activating it may still open one (session restore). It takes the normal launch-and-wait path, now correctly scoped.

Verification

  • Unit: 3 new contract tests in tests/test_atspi_addressing.py (activation return, windowless fall-through with real confirm, bystander-window rejection); suite 421 passed, ruff check . clean; sabotage-checked — the activation and scoping tests fail on unfixed code.
  • Live, on the launch_app: single-instance DBus activation either falsely confirms or times out — new-window-id is the wrong arrival test #10 reproducer: cold launch confirms; second call now prints FIXED: second call reports activation of the running instance (1 window(s)) — no timeout, no false window confirm.
  • Dogfooded on this machine's installed server (pipx venv from this branch): launch_app(org.gnome.TextEditor) with the editor running returns activated: true, already_running: true with the window dict attached.

Stacking

Stacked on #9 (fix/set-text-default), which is stacked on #8: merge order #4#5#8#9 → this. Base main per repo convention; until lower PRs merge, the diff includes their commits.

… had nothing to talk to

A private dbus-daemon cannot activate at-spi2-registryd: the a11y broker
routes the start through org.freedesktop.systemd1, and on a bus with no
systemd that name is the stub that answers /bin/false. So the headless
a11y bus existed but no registry ever owned org.a11y.atspi.Registry, and
ui_tree/ui_find failed with 'no application named gnome-shell' while the
user's real session, whose bus has real systemd behind it, worked.

headless.py now starts registryd itself, BEFORE the shell: order matters,
because the shell's atk-bridge stops retrying registration when no
registry answers (measured: registry-after-shell = 16/18 self-test,
registry-before-shell = 18/18).

Unit tests for the spawn/liveness/stop paths run anywhere; the live proof
is tests/test_headless_atspi.py, which fails 2/3 checks on main and
passes 3/3 here, and a cold headless self-test at 18/18.
…ix for every finding

setup --check proves the machine before anything exists; the doctor proves
the RUNNING stack after install and registration, and every finding prints
its action. Born from tristanmuzzu#3, where every static check was green and even the
self-test said 16/18 while the a11y registry quietly did not exist: a
doctor has to ask the live system (does anything own
org.a11y.atspi.Registry on this session's a11y bus? does the registered
command answer MCP on stdio?), not the package manager.

Checks: session, deps (setup's probes, same install lines), the a11y flag,
the registry, the extension (files/enabled/live), the server command
(version + provenance + a real initialize/tools/list handshake), and each
running headless session's registry. --self-test adds the headless
self-test (private virtual monitor, safe unattended). Never changes
anything, never sudos; exit 0 healthy / 1 action needed / 2 not a target.

11 in-process tests (seams only, subprocess booby-trapped); verified live
on Ubuntu 26.04: correctly demands the one logout this machine still owes,
spots a pre-fix headless session and prints the restart command.
…nment

MCP hosts that spawn stdio servers with a sanitized environment (Hermes
passes only PATH/HOME/USER/LANG/XDG_*; others do the same to avoid leaking
credentials) strip DBUS_SESSION_BUS_ADDRESS, so the guard in shell._gdbus
refused every extension and window call with extension_unavailable -- on
the user's own, perfectly working desktop.

The env var is an address hint, not the mechanism: since the systemd user
bus, GLib, gdbus and AT-SPI all connect to $XDG_RUNTIME_DIR/bus when it is
absent, and XDG_RUNTIME_DIR survives every sanitized baseline. Resolve the
reachable bus once (explicit address, else the runtime-dir socket), export
it so subprocesses agree, and refuse only when neither exists -- the bare
ssh login / system service case the guard exists for.

Also normalize execution.session_key() through the resolver: hashing the
raw env var handed two servers on ONE desktop different lease keys
(fallback-connected vs explicit-connected). desktop_health now reports the
resolved address instead of crying MISSING while everything works.
Issue tristanmuzzu#2 chose option 1 ('set means set'), but what landed was option 2's
machinery with option 2's default: replace=False, so the tool still appends
unless a caller knows to pass replace=True. The name, the README ('Preferred
text entry') and the tool description all say set; a caller writing a field
twice, or retrying after a failure, silently doubles the value.

- Default flips to replace=True. replace=False keeps append for the demo/
  progressive-writing case the issue was found building.
- The append path's verify tightens from 'text in after' (a substring check
  that passes on a doubled value) to equality with before+text, symmetric
  with the replace path's existing check. A retried append that corrupts the
  widget now FAILS instead of reporting verified:True.
- Schema default and description updated to say which behaviour is which.

Breaking only for callers relying on the accidental append default.
…uessing

Issue tristanmuzzu#10: for a DBusActivatable single-instance app, gio launch only
activates the running instance, so the new-window-id arrival test either
timed out over a live app (Arm B) or 'confirmed' off an unrelated window
change and green-lit writes into the existing document's buffer (Arm A).

- _desktop_facts(): DBusActivatable, Exec basename, StartupWMClass from the
  [Desktop Entry] section, Desktop Action Exec lines ignored.
- Before any spawn: if the app is DBus-activatable and a window matching its
  identity is already up, return activated/already_running with those
  windows -- no wait, no gio call. A lingering windowless
  --gapplication-service is NOT running under this check (owns no window;
  activating it may still open one) and falls through to launch-and-wait.
- The confirm loop now requires the confirming window's wm_class to match
  the launched app (Exec basename / StartupWMClass, normalized across the
  reverse-DNS spelling gap); raw command launches keep the any-new-window
  rule, having no desktop identity to match.

Validated live with the issue tristanmuzzu#10 reproducer: cold launch confirms, second
call reports activation (1 window), no timeout.
@timrichardson
timrichardson marked this pull request as ready for review September 15, 2026 10:39

This branch has not been deployed

No deployments
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.

launch_app: single-instance DBus activation either falsely confirms or times out — new-window-id is the wrong arrival test

1 participant