Skip to content

fix: start the engine when the daemon connects before the first layout - #744

Open
emrecpp wants to merge 1 commit into
mgth:masterfrom
emrecpp:fix/start-when-daemon-connects-before-layout
Open

emrecpp wants to merge 1 commit into
mgth:masterfrom
emrecpp:fix/start-when-daemon-connects-before-layout

Conversation

@emrecpp

@emrecpp emrecpp commented Nov 4, 2026

Copy link
Copy Markdown

Problem

On Windows, with "load at startup" on, the engine sometimes stays stopped after logon: the UI and the hook are both running, but the mouse is not hooked until a manual Start.

The rolling daemon event trace (daemon-events.log) shows the pattern across several logons on one machine:

Logon Events after Connected Result
A Stopped 8 s later, then Loaded, Running hooked
B Stopped 8 ms later, then nothing stopped
C Stopped 2.6 s later, then Loaded only at the manual Start ~90 min later stopped
D Stopped 0.5 s later, then Loaded only at the manual Start ~80 min later stopped

On the bad logons Current.xml was not written either, so no Load/Run was sent at all.

Cause

The UI answers a daemon's first Stopped after Connected by sending it the layout, but only if (MonitorsLayout is not null && …). At a cold logon the daemon is launched as soon as the services are resolved (MainBootloader construction → MainService → LittleBigMouseClientService.Listen), while the bootloaders are still running on the same dispatcher as the daemon's events. A daemon that connects and reports Stopped before MainBootloader.LoadAsync reaches UpdateLayout() gets no answer, and nothing asks again.

The logs fit this: the logon that worked is the one where Stopped came late. I could not prove it outright, because the 5.6.0 build on that machine logs handler failures with Debug.WriteLine, so nothing reaches ui.log in Release.

Fix

  • The post-connect Stopped is kept pending while there is no layout yet (StartIfJustConnectedAsync).
  • The first UpdateLayout() answers it, on the dispatcher, when the daemon still reports Stopped and the layout is enabled. Later rebuilds keep going through the display-change path (ReconcileFreshLayoutAsync), so nothing gets started twice.
  • MainService's post to the UI thread can be swapped (internal PostToUi) so tests can run the handlers inline: Avalonia's UI dispatcher belongs to whichever thread touched it first, which made a Dispatcher.UIThread.RunJobs()-based test order-dependent.

Testing

  • 3 new tests in MainServiceLifecycleTests:
    • a daemon that connects before the first layout is started once the layout is built (fails without the fix, passes with it), and a later rebuild does not start it again;
    • a daemon that connects after the first layout is started exactly once;
    • a layout the user turned off is not started.
  • The whole LittleBigMouse.Ui.Avalonia.Tests suite passes. One unrelated test, LocationControlViewModelTests.AfterDisposalTheLayoutNoLongerReachesTheViewModel, fails intermittently on master too (2 runs in 10 without this change).
  • I have not yet logged in again with a build carrying this fix.

🤖 Generated with Claude Code

At a cold logon the UI launches the daemon as soon as its services are
resolved, and the other bootloaders run on the same dispatcher as the
daemon's events. A daemon that connected and reported Stopped before
MainBootloader built the layout got no answer: the Stopped was dropped
because MonitorsLayout was still null, and nothing asked again, so the
engine stayed stopped until a manual Start.

That Stopped is now kept pending, and the first layout build answers it
when the daemon still reports Stopped and the layout is enabled. Later
rebuilds keep going through the display-change path.

MainService's post to the UI thread can be swapped in tests: Avalonia's
UI dispatcher belongs to whichever thread touched it first.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
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