Skip to content

feat(auth): open the login URL in a Windows browser from WSL - #206

Open
pjcdawkins wants to merge 7 commits into
cli-189-go-auth-feature-flagfrom
wsl-browser-login
Open

pjcdawkins wants to merge 7 commits into
cli-189-go-auth-feature-flagfrom
wsl-browser-login

Conversation

@pjcdawkins

@pjcdawkins pjcdawkins commented Oct 9, 2026 •

Copy link
Copy Markdown
Contributor

Stacked on #203 (Go auth, behind <PREFIX>GO_AUTH=1).

On WSL there is usually no DISPLAY and no xdg-open, so the "Log in via a browser?" prompt was not offered, and login printed the URL instead of opening it. Under WSL (detected from WSL_DISTRO_NAME/WSL_INTEROP or the kernel release), if Windows interop is enabled (WSL's WSLInterop binfmt_misc entry, and not disabled in /etc/wsl.conf, which WSL2 needs), the CLI now counts as having a display, and opens URLs with Windows' rundll32.exe url.dll,FileProtocolHandler (as on Windows), from /mnt/c/Windows/System32 or else PATH (searching PATH is slow in WSL2). The local login server's 127.0.0.1 address works from Windows, as WSL forwards localhost.

The Go wrapper passes the opener it finds to the legacy CLI in <PREFIX>WSL_BROWSER, so legacy commands also offer the prompt in WSL (the legacy CLI decides whether to offer it, with or without Go auth), and the PHP login opens the URL too.

Checked manually on Windows 11 with WSL1 and WSL2 (Ubuntu 24.04), as CI has no WSL.

  • login opens the URL in Edge, which reaches the login page, with rundll32.exe from /mnt/c/Windows/System32 and from PATH.
  • A legacy command (project:list) offers "Log in via a browser?" and opens the URL, with and without Go auth.
  • With interop disabled in wsl.conf, there is no prompt and login prints the URL at once.

🤖 Generated with Claude Code

pjcdawkins and others added 2 commits October 9, 2026 01:23
On WSL there is usually no DISPLAY and no xdg-open, so the login prompt
was not offered, and login printed the URL instead of opening it. Under
WSL, URLs are now opened with wslview if it is installed, or else with
Windows' rundll32.exe (as on Windows), found in PATH or in its default
location. The local server's 127.0.0.1 address works from Windows, as
WSL forwards localhost.

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

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 2 warnings

🔍 Full review · 2 files reviewed

Verification
  • browserCommand keeps the explicit --browser option and the "0" opt-out ahead of the new WSL case, so users can still override the WSL choice.
  • isWSL returns false right away on non-Linux GOOS, so Windows and macOS behaviour is unchanged.
  • The rundll32 loop's inner p shadows the loop variable, but it returns the resolved absolute path, which TestBrowserCommand_WSL asserts.
  • TestBrowserCommand_WSL sets PATH to the temp dir only, so a real wslview on the host cannot change its result.

The new unit tests TestBrowserCommand_WSL (Linux only, stubbed rundll32.exe on PATH) and TestIsWSL_NotWSL cover detection and command selection, and run through make test. Nothing covers the case where no Windows opener is found, or opening a URL from real WSL; the PR says this needs a manual check.

Review details
  • Commit: 80b7a16
  • Model: claude-opus-5-5

Review 1 of 10 for this pull request · View the full run

Comment thread commands/auth.go Outdated
Comment thread commands/auth.go
… found

WSL counted as having a display even without a Windows opener, e.g.
with interop disabled, or in a Docker Desktop container, which shares
the WSL kernel. The CLI then offered a browser login with no display,
and xdg-open could start a terminal browser or fail.

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

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning

🔁 Incremental · 2 files reviewed

Verification
  • hasDisplay() now returns true for WSL only when wslBrowserCommand() finds wslview or rundll32.exe, so the xdg-open fallback is never reached with DISPLAY unset.
  • A Docker Desktop container with the microsoft kernel, no /mnt/c and no wslview now gets hasDisplay() false and is not offered a browser login.
  • If openURL fails, runBrowserLogin (auth_login.go:166) falls back to printing the URL, so a failed opener does not block login.

The new TestBrowserCommand_WSLWithoutOpener covers WSL with no opener file on PATH or under /mnt/c, and TestBrowserCommand_WSL covers the case where an opener is found. Nothing covers interop being disabled, or real WSL hosts, which the PR leaves to a manual check.

Review details

Review 2 of 10 for this pull request · View the full run

Comment thread commands/auth.go
On WSL, the legacy CLI found no display or xdg-open, so legacy commands
did not offer "Log in via a browser?" (with or without Go auth), and the
PHP login printed the URL instead of opening it.

Match the Go detection: under WSL (from WSL_DISTRO_NAME/WSL_INTEROP or
the kernel release), count as having a display if a Windows opener is
found, and open URLs with wslview, or else rundll32.exe from PATH or
/mnt/c/Windows/System32. The lookup only runs on WSL, when a URL is to
be opened.

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

upsun-dispatch Bot commented Oct 9, 2026 •

Copy link
Copy Markdown

📋 PR Summary

This PR lets the CLI open the login URL in a Windows browser when it runs under WSL. On WSL with Windows interop enabled, the CLI now counts as having a display and opens URLs with rundll32.exe url.dll,FileProtocolHandler. It looks for that program at /mnt/c/Windows/System32 first and falls back to PATH. The Go wrapper passes the opener it finds to the legacy PHP CLI in &lt;PREFIX>WSL_BROWSER, so legacy commands also offer the "Log in via a browser?" prompt and open the URL. The latest push drops the wslview option and checks the System32 path before searching PATH, because a PATH search is slow under WSL2.

Changes
Layer / File(s) Summary
WSL browser detection (Go)
commands/auth.go Detects WSL and whether Windows interop is enabled, using binfmt_misc and wsl.conf. It then picks rundll32.exe as the URL opener: first the System32 path, which can be overridden in tests, then PATH. wslview is no longer used.
commands/auth_test.go Tests can now fake the binfmt entry, wsl.conf and the System32 rundll32.exe. New tests check that System32 is preferred over PATH, that PATH is used as a fallback, and that nothing is found when interop is off or no opener exists.
Passing the opener to the legacy CLI
commands/root.go Hooks the WSL browser into the root command setup.
internal/legacy/legacy.go Passes the WSL opener that was found to the legacy CLI in the &lt;PREFIX>WSL_BROWSER environment variable.
Legacy PHP URL opening
legacy/src/Service/Url.php Uses the WSL browser from the environment both to decide whether to offer the browser prompt and to open URLs.
legacy/tests/Service/UrlTest.php Tests opening URLs under WSL in the PHP CLI. The wslview test case has been removed.

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Warning

Changes suggested — 🟡 1 warning · 1 still open

🔁 Incremental · 4 files reviewed

Outstanding from earlier reviews:

  • 🟡 #4225570767 — commands/auth.go:280: The user is asked a browser prompt that cannot open a browser. — wslBrowserCommand() still only calls exec.LookPath, so with interop disabled and /mnt/c mounted the browser is still offered. The new PHP getWslBrowser() has the same gap.
Verification
  • Url::hasDisplay() on WSL now needs getWslBrowser() to find an opener. A Docker Desktop container, with no /mnt/c and no Windows PATH, therefore gets no display.
  • getDefaultBrowser() checks for the WSL opener before falling back to xdg-open/gnome-open, and only when isWsl() is true, so plain Linux behaves as before.
  • openUrl() adds url.dll,FileProtocolHandler both when rundll32.exe is found on PATH and when the /mnt/c absolute path is used, because str_ends_with matches both.
  • Go hasDisplay() now ANDs isWSL() with wslBrowserCommand() != nil, which matches the PHP condition.

The diff adds legacy/tests/Service/UrlTest.php, which mocks Shell::commandExists/execute to cover the rundll32, /mnt/c fallback, wslview and no-opener cases. It also adds OsUtilTest::testIsWsl, which tests env-var detection. No test covers a found opener that cannot execute (interop disabled), and real WSL behaviour still needs the manual check.

Review details

Review 3 of 10 for this pull request · View the full run

Comment thread legacy/src/Service/Url.php Outdated
pjcdawkins and others added 2 commits October 9, 2026 08:36
With WSL interop off, rundll32.exe still exists under /mnt/c, so the CLI
offered "Log in via a browser?", but running it fails (exec format
error). Require WSL's binfmt_misc interop entry (WSLInterop or
WSLInterop-late) to be enabled before using wslview or rundll32.exe,
in Go and in the legacy CLI. Checked on WSL1: the entry exists and is
"enabled" with interop on, and is missing with interop off.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
WSL2 keeps the WSLInterop binfmt_misc entry enabled when wsl.conf sets
[interop] enabled=false, so the CLI offered a browser login and then
waited about 10 seconds for rundll32.exe to fail. Also check wsl.conf.

So that the detection lives in one place, the Go wrapper passes the WSL
opener to the legacy CLI in <PREFIX>WSL_BROWSER (always set, empty when
there is none), and the PHP Url service uses it instead of its own WSL
checks.

Checked on Windows 11 with WSL1 (VirtualBox) and WSL2 (QEMU/KVM): with
interop on, legacy commands offer the prompt and open Edge; with it off,
there's no prompt and login prints the URL at once.

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

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Reviewed — No blocking findings · 🔵 1 minor point

🔁 Incremental · 8 files reviewed

🔵 Minor point

  • commands/root.go:348 — makeLegacyCLIWrapper now calls wslBrowser() for every legacy invocation. That includes _complete, which runs on every Tab press, plus list, completion, init and the session-export migrator. Under WSL, wslBrowser() globs binfmt_misc and reads /etc/wsl.conf. It then calls exec.LookPath("wslview") and exec.LookPath("rundll32.exe"), which stat each PATH entry. With the default appendWindowsPath=true, PATH holds many /mnt/c/... directories. On WSL2 those are on the drvfs/9p mount, where each stat is slow. When wslview is not installed, the whole PATH is scanned once for it and again for rundll32. Every legacy command and every completion request on WSL now pays this cost, even though only browser-opening commands need WSL_BROWSER. Computing it lazily, or caching it once per process, would avoid the cost.
Verification
  • The Go wrapper appends WSL_BROWSER after os.Environ(), and Go's exec keeps the last duplicate. So a user-set value is replaced, as the comment says.
  • Url's new Config constructor argument is autowired by legacy/config/services.yaml (autowire: true), and nothing in legacy/src builds Url by hand.
  • The PHP side reads application.env_prefix, which is the same prefix the wrapper uses (cnf.Application.EnvPrefix) to set WSL_BROWSER. An empty value maps to false through ?:.
  • wslBrowser() returns only c[0]. Url::openUrl adds url.dll,FileProtocolHandler back for any browser ending in rundll32.exe, so the rundll32 path still gets its argument.

New Go tests cover the wsl.conf parsing (TestWSLInteropEnabled) and wslBrowser() with fake binfmt and PATH stubs. The rewritten legacy/tests/Service/UrlTest.php covers the env-var handoff. The CI jobs make test and scripts/test/unit.sh in ci.yml run these tests. Nothing tests real WSL, and no test covers the WSL_BROWSER env injection in CLIWrapper.Exec.

Review details

Review 5 of 10 for this pull request · View the full run

The wrapper looks up the WSL opener for every legacy command. In WSL2,
PATH includes Windows directories on a slow mount: searching it for
wslview (usually missing) and rundll32.exe took about 70ms, against 6ms
for checking /mnt/c/Windows/System32/rundll32.exe. Check that path first,
fall back to PATH, and drop wslview, which rundll32.exe makes unneeded.

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

@upsun-dispatch upsun-dispatch Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Note

Reviewed — No new issues found · 1 still open

🔁 Incremental · 3 files reviewed

Outstanding from earlier reviews:

  • 🔵 commands/root.go:348: Shell completion and every legacy command get slower on WSL2. — makeLegacyCLIWrapper still calls wslBrowser() on every legacy run, including _complete. Trying System32 first skips the PATH scan only when /mnt/c holds rundll32.exe; without it, the binfmt glob, the wsl.conf read and the full PATH scan still happen on every run. (first raised)
Verification
  • wslBrowserCommand returns before any LookPath when wslInteropEnabled() is false, so turning interop off now skips the System32 stat as well as the PATH scan.
  • Every test that calls setWSLInterop now points wslRundll32Path at a temp file and restores it in t.Cleanup, so a real /mnt/c on the host can no longer change the test results.
  • Removing the wslview branch leaves nothing broken: legacy Url::openUrl adds url.dll,FileProtocolHandler for any browser path ending in rundll32.exe, which is the only kind the Go side now passes.
  • In TestBrowserCommand_WSL, rundll32.exe exists both in System32 and on PATH, and the test asserts the System32 one wins, which pins the new lookup order.

The Go behaviour is covered by tests in this change: the TestBrowserCommand_WSL* tests, a new PATH-fallback test, and TestWSLInteropEnabled, all with faked binfmt, wsl.conf and System32 paths. On the PHP side the change only removes the wslview case from UrlTest. The project runs these tests with make test (Go) and PHPUnit (legacy). As the PR description says, nothing in CI runs on real WSL.

Review details

Review 6 of 10 for this pull request · View the full run

@pjcdawkins

Copy link
Copy Markdown
Contributor Author

Re the minor point on commands/root.go:348 (WSL opener lookup on every legacy run): leaving this as is. Measured on WSL2 (Windows 11, default PATH with 7 Windows directories):

  • Interop check (binfmt_misc glob and /etc/wsl.conf read): 0.05ms.
  • /mnt/c/Windows/System32/rundll32.exe check, which succeeds with Windows on C: and the default automount: about 6ms.
  • The PATH scan only runs when that file is missing (Windows on another drive, or another automount root). Then PATH usually holds the Windows directories at their real location, and finding rundll32.exe took about 22ms. With interop off, it returns before any lookup.

_complete runs the legacy PHP CLI anyway, which costs hundreds of milliseconds, so this is small next to it. A special case to skip the lookup for completion isn't worth keeping in sync.

🤖 Replied by Claude Code

This branch has not been deployed

No deployments
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