Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
85 changes: 83 additions & 2 deletions commands/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
"io"
"os"
"os/exec"
"path/filepath"
"runtime"
"slices"
"strings"
Expand Down Expand Up @@ -272,12 +273,88 @@ func withLogin(cmd *cobra.Command, cnf *config.Config, m *auth.Manager, fn func(
return fn()
}

// hasDisplay matches the legacy CLI's Url::hasDisplay().
// hasDisplay matches the legacy CLI's Url::hasDisplay(). It also counts WSL, if a Windows browser can be opened.
func hasDisplay() bool {
if d := os.Getenv("DISPLAY"); d != "" {
return d != "none"
}
return runtime.GOOS == "windows" || runtime.GOOS == "darwin"
return runtime.GOOS == "windows" || runtime.GOOS == "darwin" || (isWSL() && wslBrowserCommand() != nil)
Comment thread
upsun-dispatch[bot] marked this conversation as resolved.
}

// isWSL reports whether this is Linux in the Windows Subsystem for Linux. This includes Docker Desktop's containers,
// which share its kernel, but have no Windows browser: see wslBrowserCommand.
func isWSL() bool {
if runtime.GOOS != "linux" {
return false
}
if os.Getenv("WSL_DISTRO_NAME") != "" || os.Getenv("WSL_INTEROP") != "" {
return true
}
b, err := os.ReadFile("/proc/sys/kernel/osrelease")
return err == nil && strings.Contains(strings.ToLower(string(b)), "microsoft")
Comment thread
pjcdawkins marked this conversation as resolved.
}

// WSL's binfmt_misc directory, config file, and default automount root (overridden in tests).
var (
wslBinfmtDir = "/proc/sys/fs/binfmt_misc"
wslConfPath = "/etc/wsl.conf"
wslDefaultMountRoot = "/mnt/"
)

// wslInteropEnabled reports whether WSL can run Windows programs. Its binfmt_misc entry is named WSLInterop or
// WSLInterop-late. WSL1 removes it when interop is off, but WSL2 keeps it enabled, so wsl.conf is checked too.
func wslInteropEnabled(wslConf string) bool {
paths, _ := filepath.Glob(filepath.Join(wslBinfmtDir, "WSLInterop*"))
for _, p := range paths {
if b, err := os.ReadFile(p); err == nil && strings.HasPrefix(string(b), "enabled") {
return !strings.EqualFold(wslConfValue(wslConf, "interop", "enabled"), "false")
}
}
return false
}

// wslConfValue returns the value of a key in a section of wsl.conf's contents, without surrounding quotes, or an
// empty string.
func wslConfValue(wslConf, section, key string) string {
var current string
for line := range strings.Lines(wslConf) {
line, _, _ = strings.Cut(line, "#")
line = strings.TrimSpace(line)
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
current = strings.TrimSpace(line[1 : len(line)-1])
continue
}
k, v, ok := strings.Cut(line, "=")
if ok && strings.EqualFold(current, section) && strings.EqualFold(strings.TrimSpace(k), key) {
v = strings.TrimSpace(v)
if len(v) >= 2 && (v[0] == '"' || v[0] == '\'') && v[len(v)-1] == v[0] {
v = v[1 : len(v)-1]
}
return v
}
}
return ""
}

// wslBrowserCommand returns a command to open URLs in a Windows browser from WSL, or nil if none is found.
func wslBrowserCommand() []string {
b, _ := os.ReadFile(wslConfPath)
wslConf := string(b)
// The openers run Windows programs.
if !wslInteropEnabled(wslConf) {
return nil
}
root := wslConfValue(wslConf, "automount", "root")
Comment thread
upsun-dispatch[bot] marked this conversation as resolved.
if root == "" {
root = wslDefaultMountRoot
}
// Searching PATH is slow under WSL2 (tens of ms, as it has Windows directories), so it is the fallback.
for _, p := range []string{filepath.Join(root, "c", "Windows", "System32", "rundll32.exe"), "rundll32.exe"} {
if p, err := exec.LookPath(p); err == nil {
return []string{p, "url.dll,FileProtocolHandler"}
}
}
return nil
}

// browserCommand returns the command to open URLs, or nil if none should be used.
Expand All @@ -295,6 +372,10 @@ func browserCommand(browserOption string) []string {
return []string{"rundll32", "url.dll,FileProtocolHandler"}
case runtime.GOOS == "darwin":
return []string{"open"}
case isWSL():
if c := wslBrowserCommand(); c != nil {
return c
}
}
for _, b := range []string{"xdg-open", "gnome-open"} {
if _, err := exec.LookPath(b); err == nil {
Expand Down
184 changes: 184 additions & 0 deletions commands/auth_test.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
package commands

import (
"os"
"path/filepath"
"runtime"
"strings"
"testing"

"github.com/platformsh/platformify/vendorization"
Expand Down Expand Up @@ -47,3 +51,183 @@ func TestBrowserCommand_Whitespace(t *testing.T) {
assert.Nil(t, browserCommand("0"))
assert.NotPanics(t, func() { browserCommand(" ") })
}

// setWSLInterop fakes WSL's binfmt_misc entry for Windows interop (an empty state leaves it out), wsl.conf (missing),
// and the default automount root. It returns the paths for wsl.conf and System32's rundll32.exe (missing).
func setWSLInterop(t *testing.T, state string) (conf, rundll string) {
dir := t.TempDir()
if state != "" {
require.NoError(t, os.WriteFile(filepath.Join(dir, "WSLInterop"), []byte(state+"\ninterpreter /init\n"), 0o600))
}
origDir, origConf, origRoot := wslBinfmtDir, wslConfPath, wslDefaultMountRoot
wslBinfmtDir, wslConfPath, wslDefaultMountRoot = dir, filepath.Join(dir, "wsl.conf"), dir+"/"
t.Cleanup(func() { wslBinfmtDir, wslConfPath, wslDefaultMountRoot = origDir, origConf, origRoot })
return wslConfPath, filepath.Join(dir, "c", "Windows", "System32", "rundll32.exe")
}

// wslOpener returns the program wslBrowserCommand finds, or an empty string.
func wslOpener() string {
if c := wslBrowserCommand(); c != nil {
return c[0]
}
return ""
}

// writeStub writes an executable stub, creating its directory.
func writeStub(t *testing.T, path string) {
require.NoError(t, os.MkdirAll(filepath.Dir(path), 0o700))
require.NoError(t, os.WriteFile(path, []byte("#!/bin/sh\n"), 0o700)) //nolint:gosec // an executable stub
}

// TestWSLInteropEnabled covers WSL2, which keeps the binfmt_misc entry enabled when wsl.conf disables interop.
func TestWSLInteropEnabled(t *testing.T) {
cases := []struct {
name string
binfmt string
wslConf string
want bool
}{
{name: "enabled", binfmt: "enabled", want: true},
{name: "no entry", want: false},
{name: "entry disabled", binfmt: "disabled", want: false},
{name: "wsl.conf disables", binfmt: "enabled", wslConf: "[boot]\nsystemd=true\n[interop]\nenabled=false\n"},
{name: "wsl.conf spacing and case", binfmt: "enabled", wslConf: "[Interop]\n Enabled = False # off\n"},
{name: "wsl.conf quoted", binfmt: "enabled", wslConf: "[interop]\nenabled = \"false\"\n"},
{name: "wsl.conf enables", binfmt: "enabled", wslConf: "[interop]\nenabled=true\n", want: true},
{name: "other section", binfmt: "enabled", wslConf: "[automount]\nenabled=false\n", want: true},
{name: "comment", binfmt: "enabled", wslConf: "[interop]\n# enabled=false\n", want: true},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
setWSLInterop(t, c.binfmt)
assert.Equal(t, c.want, wslInteropEnabled(c.wslConf))
})
}
}

func TestBrowserCommand_WSL(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("WSL is Linux")
}
_, system32 := setWSLInterop(t, "enabled")
writeStub(t, system32)
t.Setenv("DISPLAY", "")
t.Setenv("WSL_INTEROP", "")
t.Setenv("WSL_DISTRO_NAME", "Ubuntu")

assert.True(t, isWSL())
assert.Equal(t, system32, wslOpener())
assert.True(t, hasDisplay(), "a Windows browser can be used without a display")
assert.Equal(t, []string{system32, "url.dll,FileProtocolHandler"}, browserCommand(""))
assert.True(t, canOpenURLs(""))
}

// TestWSLBrowser_MountRoot checks that rundll32.exe is found via wsl.conf's automount root, or else via PATH.
func TestWSLBrowser_MountRoot(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("WSL is Linux")
}
custom := t.TempDir()
customRundll := filepath.Join(custom, "c", "Windows", "System32", "rundll32.exe")
cases := []struct {
name string
wslConf string
stub string // Where to write rundll32.exe: "default", "custom" (roots), "path", or nowhere.
want string // Where it is expected to be found, as above, or nowhere.
}{
{name: "default root", stub: "default", want: "default"},
{name: "custom root", wslConf: "[automount]\nroot = " + custom + "/\n", stub: "custom", want: "custom"},
{name: "custom root without slash", wslConf: "[Automount]\nRoot=" + custom + "\n", stub: "custom", want: "custom"},
{name: "custom root, quoted", wslConf: "[automount]\nroot = \"" + custom + "/\"\n", stub: "custom", want: "custom"},
{name: "custom root, rundll32 in default root", wslConf: "[automount]\nroot=" + custom + "/\n", stub: "default"},
{name: "rundll32 in PATH", stub: "path", want: "path"},
{name: "no rundll32"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
conf, defaultRundll := setWSLInterop(t, "enabled")
if c.wslConf != "" {
require.NoError(t, os.WriteFile(conf, []byte(c.wslConf), 0o600))
}
require.NoError(t, os.RemoveAll(filepath.Join(custom, "c")))
pathDir := t.TempDir()
t.Setenv("PATH", pathDir)
t.Setenv("WSL_DISTRO_NAME", "Ubuntu")
switch c.stub {
case "default":
writeStub(t, defaultRundll)
case "custom":
writeStub(t, customRundll)
case "path":
writeStub(t, filepath.Join(pathDir, "rundll32.exe"))
}

switch c.want {
case "default":
assert.Equal(t, defaultRundll, wslOpener())
case "custom":
assert.Equal(t, customRundll, wslOpener())
case "path":
assert.Equal(t, filepath.Join(pathDir, "rundll32.exe"), wslOpener())
default:
assert.Empty(t, wslOpener())
assert.Nil(t, wslBrowserCommand())
}
})
}
}

func TestWSLConfValue(t *testing.T) {
conf := "[boot]\nsystemd=true\nroot=/boot/\n\n[Automount] # mounts\n Root = /win/ # comment\n"
assert.Equal(t, "true", wslConfValue(conf, "boot", "systemd"))
assert.Equal(t, "/win/", wslConfValue(conf, "automount", "root"))
assert.Empty(t, wslConfValue(conf, "interop", "enabled"))
assert.Empty(t, wslConfValue("", "automount", "root"))

quoted := "[a]\nd=\"x\"\ns='y'\nm=\"z'\nq=\"\n"
assert.Equal(t, "x", wslConfValue(quoted, "a", "d"))
assert.Equal(t, "y", wslConfValue(quoted, "a", "s"))
assert.Equal(t, "\"z'", wslConfValue(quoted, "a", "m"))
assert.Equal(t, "\"", wslConfValue(quoted, "a", "q"))
}

// TestBrowserCommand_WSLWithoutOpener checks WSL without a Windows opener, e.g. with interop disabled, or a Docker
// Desktop container (which shares the WSL kernel).
func TestBrowserCommand_WSLWithoutOpener(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("WSL is Linux")
}
setWSLInterop(t, "enabled")
t.Setenv("PATH", t.TempDir())
t.Setenv("DISPLAY", "")
t.Setenv("WSL_DISTRO_NAME", "Ubuntu")

assert.False(t, hasDisplay())
assert.False(t, canOpenURLs(""))
}

// TestBrowserCommand_WSLInteropOff checks that a Windows opener is not used when interop is off, as it cannot run.
func TestBrowserCommand_WSLInteropOff(t *testing.T) {
if runtime.GOOS != "linux" {
t.Skip("WSL is Linux")
}
t.Setenv("DISPLAY", "")
t.Setenv("WSL_DISTRO_NAME", "Ubuntu")

for _, state := range []string{"", "disabled"} {
_, rundll := setWSLInterop(t, state)
writeStub(t, rundll)
assert.False(t, hasDisplay(), "interop state %q", state)
assert.Empty(t, wslOpener(), "interop state %q", state)
assert.False(t, canOpenURLs(""), "interop state %q", state)
}
}

func TestIsWSL_NotWSL(t *testing.T) {
if b, _ := os.ReadFile("/proc/sys/kernel/osrelease"); strings.Contains(strings.ToLower(string(b)), "microsoft") {
t.Skip("running in WSL")
}
t.Setenv("WSL_DISTRO_NAME", "")
t.Setenv("WSL_INTEROP", "")
assert.False(t, isWSL())
}
Loading
Loading