Skip to content

Fix host hang when Electron exits before the socket bridge connects - #1124

Merged
FlorianRappl merged 3 commits into
ElectronNET:developfrom
DYH1319:fix/socket-bridge-stop-unconnected
Nov 5, 2026
Merged

FlorianRappl merged 3 commits into
ElectronNET:developfrom
DYH1319:fix/socket-bridge-stop-unconnected

Conversation

@DYH1319

@DYH1319 DYH1319 commented Nov 4, 2026 •

Copy link
Copy Markdown
Contributor

Problem

With the DotNet-first startup and SingleInstance enabled, launching the app a second time leaves the second .NET process hanging forever.

The second Electron process loses the single-instance lock and quits right after printing its socket port, before the socket bridge has connected. What happens next:

  1. ElectronProcessActive transitions to Stopped → RuntimeControllerDotNetFirst.HandleStopped() sees the socket bridge is not stopped and calls socketBridge.Stop().
  2. SocketBridgeService.StopCore() disposes the socket, and LifetimeServiceBase.Stop() moves the bridge to Stopping.
  3. The bridge only ever reaches Stopped through BridgeDisconnected, but a socket that never connected never raises it.
  4. The controller therefore never reaches Stopped. WaitReadyTask / WaitStoppedTask never complete, so the host's Main waits forever.

The same applies whenever Electron exits before the bridge connects (e.g. a crash during startup).

Fix

SocketBridgeService overrides Stop(): after the base implementation (dispose + Stopping), it transitions to Stopped if the disconnect event has not already done so. A connected socket that raises BridgeDisconnected behaves as before. The state is only checked after base.Stop() returns, so the Stopping transition inside base.Stop() can't run after Stopped and throw for an invalid backward transition.

The controller then reaches Stopped. Because Stopped > Ready, WaitReadyTask completes too, so the host can check State and exit.

Testing

Added lifecycle tests to ElectronNET.IntegrationTests. Neither class needs an Electron runtime:

  • SocketBridgeServiceLifecycleTests starts a bridge against a closed loopback port, so it can never connect, and checks that after Stop() it:

    • reaches Stopped and completes WaitStoppedTask;
    • raises Stopping then Stopped, exactly once;
    • stays in Stopped after the pending connect attempt gives up.

    Without the fix, the first two fail with the bridge stuck in Stopping and Stopped never raised. The third one also fails, because the bridge never got to Stopped in the first place.

  • LifetimeServiceBaseTests covers the state-machine rules the fix and the controller rely on:

    • Start / Stop transitions;
    • Stopped before Ready completing every earlier wait task, including WaitReadyTask, so a host awaiting readiness can observe the stopped runtime and exit;
    • a transition to the current state being ignored;
    • backward transitions, and Stop() after Stopped, throwing.

Results on Linux:

  • The new tests pass consistently over 5 runs.
  • Full integration suite: 111 passed, 18 skipped, and 1 failure. The failure is GlobalShortcutTests.Can_register_and_unregister, which also fails on its own on this machine: Wayland/Hyprland doesn't allow Electron global shortcuts.
  • In a downstream app (Linux, Electron 43, DotNet-first, packaged), a second launch used to hang until killed. With the fix it exits in about 2 seconds.

🤖 Generated with Claude Code

When Electron exits before the socket bridge connects (for example because
another instance holds the single-instance lock), RuntimeControllerDotNetFirst
stops the bridge, but SocketBridgeService only reaches Stopped through
BridgeDisconnected, which a never-connected socket does not raise. The
controller then never reaches Stopped and the .NET host hangs forever.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DYH1319
DYH1319 marked this pull request as ready for review November 4, 2026 07:16
@FlorianRappl

Copy link
Copy Markdown
Collaborator

Create the tests for the lifecycle. Rest looks fine to me.

…ServiceBase

- SocketBridgeServiceLifecycleTests: a bridge that never connects (closed
  loopback port) reaches Stopped after Stop(), raises Stopping then Stopped
  exactly once, and is not moved out of Stopped by the pending connect
  attempt. These fail without the previous commit (the bridge stays in
  Stopping and Stopped is never raised).
- LifetimeServiceBaseTests: Start/Stop transitions, Stopped before Ready
  completing every earlier wait task (what lets hosts awaiting WaitReadyTask
  observe a stopped runtime), same-state transitions being ignored, and
  backward transitions / Stop after Stopped throwing.

No Electron runtime is required for either.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@DYH1319

DYH1319 commented Nov 4, 2026

Copy link
Copy Markdown
Contributor Author

Thanks! Added lifecycle tests for SocketBridgeService (the never-connected stop path, which fails without the fix) and LifetimeServiceBase in 1a0f287; neither needs an Electron runtime.

@FlorianRappl

Copy link
Copy Markdown
Collaborator

Right now almost all tests are failing. Not sure if this is related but beforehand tests worked and this is not just one faulty machine but across all runners.

@DYH1319

DYH1319 commented Nov 5, 2026

Copy link
Copy Markdown
Contributor Author

Thanks for flagging this. I looked into it, and the failures come from develop rather than from this PR.

Electron crashes at startup with Cannot find module 'inherits', so ElectronFixture times out and every test that uses Electron fails. The develop push run for 05aa92e fails the same way (run 40534971218), and that was before this PR was opened.

The cause is the Host lockfile. It's copied into .electron/ next to the generated package.json, which still requests image-size ^1.2.1, while the lockfile pins 2.0.3. After the #1118 merge it also contains queue without its dependency inherits, so npm never installs inherits.

I've opened #1125 to repair the lockfile. Locally on Linux (Electron 38.2.2), that fix with this PR merged on top gives 112 passed, 18 skipped, 0 failed. Once it's merged, I'll update this branch from develop to re-run CI.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Nov 5, 2026

Copy link
Copy Markdown

pr-comment: Run #161

Tests 📝 Passed ✅ Failed ❌ Skipped ⏭️ Pending ⏳ Other ❓ Flaky 🍂 Duration ⏱️
1820 1658 0 0 0 162 0 3m 51s

🎉 All tests passed!

Github Test Reporter by CTRF 💚

@FlorianRappl FlorianRappl added this to the 1.0.0 milestone Nov 5, 2026
@FlorianRappl
FlorianRappl merged commit b8cf0c5 into ElectronNET:develop Nov 5, 2026
17 checks passed
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.

2 participants