Skip to content

fix(desktop): probe the distro's ports before binding the WSL backend - #7702

Closed
DraftProducts wants to merge 1 commit into
pingdotgg:mainfrom
DraftProducts:fix/desktop-wsl-port-probe
Closed

DraftProducts wants to merge 1 commit into
pingdotgg:mainfrom
DraftProducts:fix/desktop-wsl-port-probe

Conversation

@DraftProducts

@DraftProducts DraftProducts commented Aug 20, 2026 •

Copy link
Copy Markdown

What Changed

resolveDesktopBackendPort now takes the set of ports the WSL distro already
listens on and skips them before probing the Windows host. DesktopWslEnvironment
learns that set in one wsl.exe -- cat /proc/net/tcp /proc/net/tcp6 round trip,
collecting the port of every row in state 0A (LISTEN) regardless of bind
address, since any listener there blocks the backend's 0.0.0.0 bind. A probe
that fails returns None, logs a warning naming the fallback, and the scan
reverts to today's Windows-only behavior rather than blocking startup.

The probe is budgeted at PRE_WARM_TIMEOUT (10s) rather than the 5s its
immediate neighbours in that file use. Those neighbours always run after the VM
is up, whereas in wsl-only mode this is the first wsl.exe call of the session
and pays the cold start; a warm round trip measures ~100ms here, so the headroom
only applies to a distro that is genuinely slow to boot. The call is guarded by
isAvailable, a cached filesystem check that is hardcoded false off win32, so a
machine without WSL never spawns it.

The choice happens in bootstrap, ahead of serverExposure.configureFromSettings,
because that is the port's only ingress: the renderer origin pinned by
registerDesktopProtocol, the advertised loopback and LAN endpoints, Tailscale
Serve, and the backend's own bind all derive from it. resolvePrimary runs again
on every backend restart, so correcting the port there would let it drift away
from an origin that is already registered and cannot be re-pointed.
buildWslPrimaryConfig still passes backendExposure.port verbatim.

The dual-mode secondary scan in DesktopWslBackend had the identical flaw and
gets the same treatment, one probe per scan rather than one per candidate port.
Non-WSL primaries pass an empty set and take the existing path unchanged. The
"Connecting to WSL" splash moved ahead of the probe, since the probe is what
cold-starts the VM, and the quitting check that gated the splash now gates the
probe as well — a bootstrap racing a quit does neither.

Tests: eight new cases across DesktopApp.test.ts (new file),
DesktopWslBackend.test.ts and DesktopWslEnvironment.test.ts. A distro-held
3773 yields 3774 and is never offered to canListenOnHost; the dual-mode
secondary skips distro-held ports and probes exactly once; the parser collects
LISTEN rows from both address families, ignores an ESTABLISHED row on the same
port, and returns None on garbage; and a TestClock case pins the probe budget
along with the degrade-to-unknown contract — still pending at 9s, None at 10s.
All eight fail without the fix. Targeted run over eight desktop suites: 81
passing. apps/desktop typecheck, lint and format
pass. The two failures already present on main (Net.findAvailablePort and the
ProviderRegistry codex re-probe) reproduce there and are untouched.

Why

With wslOnly enabled the port is scanned from the Electron main process — the
Windows side — and handed to a backend that binds inside the distro. WSL2's
localhost forwarding does not reserve a WSL-side listener's port in the Windows
port namespace, so the two disagree and a Windows probe is not evidence about
the distro.

Anyone who installed the background service inside their distro hits this. The
service holds 3773, the scan logs selected backend port via sequential scan
having just declared 3773 free, and the backend dies on

listen EADDRINUSE: address already in use 0.0.0.0:3773

about every 20 seconds, forever, with nothing shown in the app. The renderer
surfaces it as an unhandled PrimaryEnvironmentRequestError thrown from a route
beforeLoad, which points at authentication rather than at a port.

Skipping the taken port rather than failing with a better message is the right
call: the app can simply start on 3774, and someone running both the service and
the desktop app wanted both to work.

Not addressed: the probe is not a guarantee, since something can take the port
between the probe and the launch, so a launch-time EADDRINUSE still deserves a
readable failure — a separate concern. wslDistroPortsProbed is logged so a
probe that failed is visible after the fact.

Checklist

  • This PR is small and focused
  • I explained what changed and why
  • I included before/after screenshots for any UI changes — N/A, the existing splash is unchanged, only its timing
  • I included a video for animation/interaction changes — N/A, no motion changes

Changes by Claude Opus 5 running in Claude Code.

Note

Probe WSL distro ports before binding backend to prevent EADDRINUSE

  • Adds probeListeningPorts to DesktopWslEnvironment to discover listening TCP ports inside the WSL distro by parsing /proc/net/tcp and /proc/net/tcp6 with a 10-second timeout.
  • Updates resolveDesktopBackendPort and scanForWslPort to accept a portsHeldInDistro set and skip those ports during allocation.
  • In WSL-only mode, shows the 'Connecting to WSL' splash before probing to account for cold-boot delays.
  • Risk: If probeWslListeningPorts times out or fails, it returns None, causing the app to fall back to the previous Windows-only loopback scan which may result in EADDRINUSE inside the distro.

Macroscope summarized 4cd11a5.

@coderabbitai

coderabbitai Bot commented Aug 20, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on this repository. Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 23951f5b-946a-48b7-b615-7017a868f990

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions github-actions Bot added vouch:unvouched PR author is not yet trusted in the VOUCHED list. size:L 100-499 changed lines (additions + deletions). labels Aug 20, 2026

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want fixes drafted automatically? Bugbot Autofix can create code changes for findings. A team admin can enable Autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit 92bd9af158bc0818e8cd00f6af34ffb55f5a290b. Configure here.

Comment thread apps/desktop/src/wsl/DesktopWslEnvironment.ts
@macroscopeapp

macroscopeapp Bot commented Aug 20, 2026 •

Copy link
Copy Markdown
Contributor

Approvability

Verdict: Not approved

Macroscope's review found this PR not approvable — This PR introduces new WSL port-probing functionality (parsing /proc/net/tcp via wsl.exe) and changes the bootstrap flow and port selection logic at multiple points. Despite the 'fix' label, this is a new capability with cross-platform complexity that warrants human review.

You can add or adjust custom eligibility rules. Learn more.

With wsl-only mode enabled, the desktop scanned for a free backend port
from the Electron main process — the Windows side — and handed the result
to a backend that binds inside the WSL distro. WSL2's localhost forwarding
does not reserve a WSL-side listener's port in the Windows port namespace,
so a user already running the `t3` service on 3773 inside their distro got
a port the scan called free and a backend that died on
`listen EADDRINUSE 0.0.0.0:3773` on every restart.

DesktopWslEnvironment can now read the distro's own listening ports in one
`wsl.exe` round trip (`cat /proc/net/tcp /proc/net/tcp6`, LISTEN rows only).
Bootstrap consults it before choosing the port, so every consumer — the
renderer origin, the advertised endpoints, the backend's bind — derives from
a port that is free on both sides. The dual-mode WSL secondary's scan gets
the same treatment. A probe that fails falls back to the previous
Windows-only behavior instead of blocking startup.

Model: Claude Opus 5 via Claude Code
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size:L 100-499 changed lines (additions + deletions). vouch:unvouched PR author is not yet trusted in the VOUCHED list.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant