Skip to content

Per-user local IPC transport, atomic recovery, daemon lifecycle (from #513) - #519

Closed
mgth wants to merge 4 commits into
hook-ui-fixesfrom
ipc-transport
Closed

Per-user local IPC transport, atomic recovery, daemon lifecycle (from #513)#519
mgth wants to merge 4 commits into
hook-ui-fixesfrom
ipc-transport

Conversation

@mgth

@mgth mgth commented Jul 25, 2026

Copy link
Copy Markdown
Owner

Third extraction from #513 (stacked on #518): the transport rework, reviewed fix by fix, with three of its bugs fixed and two of its regressions left out.

Why replace the TCP transport

Port 25196 was open to every local process (commands + FocusChanged paths readable by anyone, port squatting = daemon panic at boot), the old client dispatched events out of order (Task.Run), swallowed send failures after 10 retries — a Stop could silently leave the hook capturing the mouse — and matched events by substring inside payloads.

What this brings

  • Rust server (ipc/server.rs, tokio): per-session named pipe with user/SYSTEM DACL + session check on Windows; 0600 UDS in $XDG_RUNTIME_DIR on Linux. Bounded queues, 4-client cap, serialized command worker (Load→Run order), broadcast that never blocks the hook thread.
  • Framing (ipc/framing.rs / LocalIpcClient): u32-LE length prefix + UTF-8, 1 MiB cap, both sides tested.
  • Strict .NET parsing (DaemonMessage.TryParse): real XML parse, DTD refused, replaces args.Contains("Stopped").
  • XML escaping of FocusChanged payloads (a path with & used to produce invalid XML).
  • Atomic recovery: AtomicRecoveryFile (write-temp-fsync-replace) + .bak fallback in the daemon — only on unreadable/unparsable primary; a valid stopped state is authoritative, and Run without a successful Load is ignored.
  • Stop safety net: IPC down → kill known hook processes of this session (on Unix, only this UI's child) instead of leaving them routing input.
  • Lifecycle: serialized LaunchDaemon, ordered UI-thread event dispatch with exception isolation, tray Start persists Enabled=true.

Fixed on top of #513

  • Windows accept loop no longer dies permanently on a transient create_pipe error (log + 500 ms backoff + retry)
  • ConnectionFailed re-raises every ~5 s instead of once — a daemon that crashes right after launch gets relaunched (idempotent thanks to the already-running check)
  • UnauthorizedAccessException / malformed frame no longer kill the event listener silently
  • Unix bind no longer chmods a parent directory it did not create (EPERM on /tmp)

Deliberately NOT taken from #513

  • The MainService resume-watchdog gating (would reintroduce the stale-layout-on-wake bug the watchdog fixes)
  • DaemonLaunchPolicy/runas (conflicts with the 5.4.1.0 does not work #512 elevation model — the daemon inherits the UI's level)

Acted decisions

  • The C++ LittleBigMouse.Hook (TCP-only) is retired from distributable builds.
  • LBM_HOOK_ENDPOINT replaces LBM_HOOK_PORT for side-by-side dev testing.
  • Installer must kill a resident old daemon on upgrade (else the UI shows Dead until reboot) — to land in the same release; iss block to follow.

Tested

  • Rust: 45 lib + 6 wire-contract tests over a real UDS on Linux, including full-duplex under load (50 interleaved broadcast/command rounds) and listener reconnection ×3 — the historical pipe failure modes; clippy clean vs baseline
  • .NET: 74 DisplayLayout (incl. DaemonProtocolTests) + 6 UI tests, full UI build on Linux
  • Windows still needed: cold start, daemon restart, suspend/resume storms, upgrade-over-old-daemon, two UIs, elevated pipe ACL

🤖 Generated with Claude Code

mgth and others added 2 commits July 25, 2026 11:39
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…513)

Adopts the transport rework from #513 (commits 3fdf340/6641ae5), reviewed
fix by fix and rebased on the master elevation and exclusion models.

Why the TCP transport had to go: port 25196 was open to every local
process (any user could command the daemon or read FocusChanged paths),
a squatted port made the daemon panic at boot, the client dispatched
events out of order via Task.Run, swallowed send failures after 10
retries (a Stop could silently leave the hook capturing the mouse), and
matched events by substring inside payloads.

The replacement: a per-session named pipe with a user/SYSTEM DACL and
session check on Windows, a 0600 Unix socket in XDG_RUNTIME_DIR on
Linux; u32-length-prefixed UTF-8 frames capped at 1 MiB; bounded queues;
a serialized command worker preserving Load-then-Run order; four-client
cap. Same XML message contract - settings and Current.xml stay
compatible; DaemonPort remains as a dead property so existing settings
deserialize. Current.xml is now written atomically with a .bak the
daemon falls back to only when the primary is unreadable or fails to
parse - a valid stopped state no longer triggers recovery, and a Run
without a successful Load is ignored. Stop gets a safety net: if IPC is
down, known hook processes of this session (this UI's child on Unix)
are killed rather than left routing input.

On top of the #513 version, three of its bugs are fixed here:
- the Windows accept loop died permanently on a transient create_pipe
  error; it now logs, backs off 500 ms and retries
- the listener raised ConnectionFailed only once per connect cycle, so a
  daemon that crashed right after launch was never relaunched; it now
  re-raises every ~5 s (LaunchDaemon's already-running check makes that
  idempotent)
- UnauthorizedAccessException or a malformed frame killed the event
  listener silently; both now reconnect like I/O failures
And two of its regressions are not taken: the resume watchdog stays
ungated (the #513 gating reintroduced the stale-layout-on-wake bug), and
daemon launch keeps the #512 elevation model (no runas).

The Unix bind also no longer chmods a parent directory it did not
create (EPERM on shared parents like /tmp).

LBM_HOOK_ENDPOINT replaces LBM_HOOK_PORT for side-by-side testing. The
C++ LittleBigMouse.Hook daemon, which only speaks TCP, is retired from
distributable builds (decision from the #513 review).

Wire contract covered by tests on both sides, including full-duplex
under load (events streaming while commands flow) and listener
reconnection - the historical pipe failure modes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Co-authored-by: MathieuARS <mathieu.maik.15@gmail.com>
mathieu and others added 2 commits July 25, 2026 16:13
Two review findings on the local IPC client:

- A frame that is not valid UTF-8 threw DecoderFallbackException, which the
  listener's catch filter does not list: the listener died silently, forever.
  Surface it as InvalidDataException like an oversized frame so the reconnect
  path handles it.
- The daemon honours LBM_HOOK_ENDPOINT but the UI client did not: setting the
  documented side-by-side testing override left the launched daemon bound to
  the test endpoint while the UI polled the production name — permanent Dead.
  The client now reads the variable (full pipe path on Windows, socket path
  elsewhere), and the README documents the full-path format.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…out-Run

- The zone serializer wrote attribute values raw: a monitor name containing
  & or a quote produced XML that the new recovery-file validation rejects
  (failing Start after the engine already started) and that the daemon
  parser could never read. Escape attribute values; roxmltree unescapes
  them on the daemon side.
- Only Load batches rewrote Current.xml, so a user Stop left Load+Run in
  the file and a standalone (autostart) daemon re-hooked the mouse at the
  next boot despite the Stop. Strip the Run line on Stop: Load-without-Run
  is exactly the persisted stopped state the daemon replay already honours
  (valid_stopped_primary_does_not_fall_back_to_backup). Stop stays a
  safety operation — persistence failures are logged, never faulted.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@mgth
mgth deleted the branch hook-ui-fixes July 25, 2026 14:34
@mgth mgth closed this Jul 25, 2026
@mgth

mgth commented Jul 25, 2026

Copy link
Copy Markdown
Owner Author

Landed on master as a0f3110..6d31774 (fast-forward of the rebased branch — byte-identical to a rebase merge). The PR was auto-closed when its stacked base branch hook-ui-fixes was deleted after #518's merge; reopening was not possible with the base gone. Twin submodule PR: mgth/HLab.Core#4 (merged).

@mgth
mgth deleted the ipc-transport branch July 25, 2026 14:54
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