Sleep/resume hardening, corner-crossing crash fix, Linux platform seam - #495
Merged
Conversation
…Controller Extract Windows-specific layout building and topology control behind ILayoutFactory / IDisplayController, implemented in a new LittleBigMouse.Platform.Windows project. The UI now depends only on the neutral MonitorsLayout model and these seams — a Linux head plugs in its own factory. - Move the former UI LayoutFactory, RegistryExt and Persistency into the platform project; drop the intermediate data-layer indirection. - DpiAwarenessKind replaces the Win32 WinDef.DpiAwareness in the neutral model. - Wallpaper drawn in place behind each monitor (WindowsWallpaperWatcher + in-place UpdateWallpaper), plus the detached-monitor span/crop guards. - run-lbm.ps1: dev launcher (build Rust hook + UI, stage, relaunch elevated).
Grace tracks every transient IDisposable it creates in its root disposal scope and holds it until the app closes. Monitor ViewModels/Views (ReactiveModel, IDisposable) are rebuilt on every display-change; the container kept every generation alive — gigabytes after a display-event storm (the UI reached ~10 GB after a wake-from-sleep storm), even though HLab.Mvvm disposes them on DetachedFromLogicalTree. Their lifetime is owned by the view tree, not the container, so mark IView/IViewModel exports ExternallyOwned (same fix as MonitorsLayout, #484 — this is the ViewModel follow-up). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Rework how the UI reacts to daemon display/power events: - Rebuild once the OS display config has SETTLED (trailing debounce + two matching DisplaySignature reads) instead of a fixed 5 s delay. - Idempotence guard: skip the rebuild when the settled signature equals the one already built. A spurious WM_DISPLAYCHANGE (DPMS on/off, mode re-apply) used to drop a fresh MonitorsLayout generation for nothing — the storm that filled GBs. - Sleep gating: the daemon now emits Suspended/Resumed (screen off/on). While suspended the UI does nothing (no debounce, no rebuild); on Resumed it reconciles only if the config changed and re-hooks the daemon. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The daemon owns hook lifecycle, so it must be the one that reacts to sleep: register for GUID_CONSOLE_DISPLAY_STATE on the display-listener window and, on screen-off, unhook itself (release the clip — the cursor is never left confined without a UI) and broadcast Suspended; on screen-on, broadcast Resumed and let the UI re-Start us once the config settles. Deduplicated via a `suspended` flag (the notification re-pushes the current state each hook/unhook cycle). Display-state, not APM suspend: this machine's "veille" is a session-state transition, so PBT_APMSUSPEND is never sent; display on/off does fire and also covers lock/idle. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e lock) Touching a monitor corner permanently killed crossing until a full restart. The straight-crossing path mapped the coordinate through the catch-all no-target link (i32::MIN/MAX sentinel bounds) BEFORE checking the target, and ZoneLink::to_target_pixel did its subtractions in i32 (the i64 cast came too late). In a debug build that overflow panics; the panic drops the MutexGuard held by the WH_MOUSE_LL callback, poisoning the engine lock for the whole process. catch_unwind hides the crash, so the hook keeps running but every lock/try_lock then fails -> the engine never acts -> crossing dead, and Stop/Start can't recover (Load's lock is poisoned too); only a fresh process does. C++ never hit it (it maps only inside `if (Target && ...)`, and its long overflow wraps). Fixes: - ZoneLink::to_target_pixel: widen operands to i64 BEFORE subtracting (+ guard s_len==0). - strait_cross: resolve Some(target) BEFORE mapping the coordinate. - Poison recovery in the callback, Load and unhook (lock().unwrap_or_else(into_inner)) so a panic can never again leave crossing permanently dead. Also add a watchdog that reinstalls the low-level hook if Windows silently drops it (LowLevelHooksTimeout): cursor moving while our hook counts nothing -> reinstall. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
After a deep sleep, LBM would come back with the engine stopped (blue tray icon) and needed a manual Start. Two things combined: - The daemon unhooks itself over ANY display change (so the cursor is never confined while the desktop reshapes). A wake from a long sleep re-enumerates the GPU/monitors and fires a WM_DISPLAYCHANGE burst; one arrives ~1-2s AFTER we re-hook and unhooks us again (a late Stopped after Resumed). - The idempotence guard (de98632) returned early when the settled signature equalled the built one — skipping not just the rebuild but the StartAsync re-hook. So once the daemon unhooked on an unchanged config, nothing ever re-hooked it. Confirmed in daemon-events.log: Resumed -> Running -> Stopped, then only "UNCHANGED sig — skip rebuild" until a manual Start. Fix (UI only; the daemon keeps hook-lifecycle ownership): - Idempotence guard now calls EnsureEngineHookedAsync instead of returning bare: StartAsync when Enabled && !suspended && State != Running, WITHOUT a rebuild (so #412 is not reintroduced). - Resumed drives EnsureRunningAfterResumeAsync, a bounded watchdog that re-asserts Start until the engine is Running for a short quiet window (~1.5s), capped at 6 restarts / 30s so a legitimately-paused engine (an excluded game focused at wake) can't loop. _resumeWatchdogActive silences the settle-path reconcile while the watchdog is the active driver. Never reconcile on a bare Stopped: an excluded app taking focus broadcasts Stopped (not a proactive Paused), so that would re-hook over a game. The display-settle / resume paths are the safe triggers, and the daemon's Run path no-ops when paused, so re-asserting Start during a game is harmless. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
The resume re-hook fix is validated on a real sleep/wake cycle, so drop the temporary DcLog / C:\dev\lbm-dc.log tracing added to measure display-settle timing: - Delete DcLog + its lock and every call site (DisplayChangedAsync, the resume watchdog, the idempotence-guard reconcile). The permanent daemon-events.log (TraceDaemonEvent) still records Running/Stopped/Resumed. - Drop the settle diagnostics (steps/stable counters, the EnsureEngineHookedAsync reason arg) that only fed the log. - Set DisplayStabilityMaxSteps to its production value (40 -> 8, ~1.1s cap). Behaviour is unchanged; this only removes tracing. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
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.
Batch of work sitting on this branch after #493 merged (7 commits).
Sleep / resume
GUID_CONSOLE_DISPLAY_STATE; on screen-off unhook + broadcastSuspended, on screen-on broadcastResumed.Resumeddrives a bounded watchdog that re-asserts Start through the storm. Validated on a real sleep/wake cycle.DcLog/lbm-dc.logtracing now that the fix is validated;DisplayStabilityMaxSteps40 → 8 (production value).Daemon stability
Other
ExternallyOwned.ILayoutFactory/IDisplayController.🤖 Generated with Claude Code