[client] Take the graphical session answer from the caller instead of the daemon environment (#7187)

This commit is contained in:
Viktor Liu
2026-08-13 17:28:34 +09:00
committed by GitHub
parent 58c09ead21
commit e290769df1
10 changed files with 164 additions and 43 deletions

View File

@@ -3,18 +3,69 @@ package util
import (
"os"
"os/exec"
"runtime"
"slices"
"github.com/skratchdot/open-golang/open"
)
const (
// envBrowser overrides the browser OpenBrowser launches
envBrowser = "BROWSER"
// envDesktopSession and envXDGCurrentDesktop are what xdg-open uses to pick a handler
envDesktopSession = "DESKTOP_SESSION"
envXDGCurrentDesktop = "XDG_CURRENT_DESKTOP"
// envDisplay and envWaylandDisplay are what a graphical browser needs to reach a display
envDisplay = "DISPLAY"
envWaylandDisplay = "WAYLAND_DISPLAY"
// envXDGSessionType names the session kind, e.g. tty, x11 or wayland
envXDGSessionType = "XDG_SESSION_TYPE"
)
// OpenBrowser opens the URL in a browser, respecting the BROWSER environment variable.
func OpenBrowser(url string) error {
if browser := os.Getenv("BROWSER"); browser != "" {
if browser := os.Getenv(envBrowser); browser != "" {
return exec.Command(browser, url).Start()
}
return open.Run(url)
}
// browserSessionEnvVars returns the variables that decide whether OpenBrowser can open a URL.
// DISPLAY and WAYLAND_DISPLAY are exactly what xdg-open's own has_display() checks, and without
// them it degrades to terminal browsers. BROWSER is the explicit override both xdg-open and
// OpenBrowser honor first. DESKTOP_SESSION and XDG_CURRENT_DESKTOP only tell xdg-open which
// desktop-specific opener to prefer, so they are weaker evidence, kept because the previous
// detection relied on them alone and dropping them would demote sessions that work today.
func browserSessionEnvVars() []string {
return []string{envDisplay, envWaylandDisplay, envBrowser, envDesktopSession, envXDGCurrentDesktop}
}
// graphicalXDGSessionTypes are the systemd-logind session types that come with a display. The
// other documented values are "tty" and "unspecified"; anything unrecognized is treated as no
// display, so an unknown value picks the device code flow, which works without a browser.
func graphicalXDGSessionTypes() []string {
return []string{"x11", "wayland", "mir"}
}
// HasGraphicalSession reports whether this process can open a browser and serve a loopback
// redirect back to it. Windows and macOS always can. On Linux and FreeBSD the answer is env
// based, so it only holds for a process started from the graphical session itself: a service
// does not inherit those variables and always reports false, which is why callers running in
// the user's session pass their own answer to the daemon.
func HasGraphicalSession() bool {
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
return true
}
for _, env := range browserSessionEnvVars() {
if os.Getenv(env) != "" {
return true
}
}
return slices.Contains(graphicalXDGSessionTypes(), os.Getenv(envXDGSessionType))
}
// SliceDiff returns the elements in slice `x` that are not in slice `y`
func SliceDiff(x, y []string) []string {
mapY := make(map[string]struct{}, len(y))

50
util/session_test.go Normal file
View File

@@ -0,0 +1,50 @@
package util
import (
"os"
"runtime"
"testing"
"github.com/stretchr/testify/assert"
)
func TestHasGraphicalSession(t *testing.T) {
if runtime.GOOS != "linux" && runtime.GOOS != "freebsd" {
assert.True(t, HasGraphicalSession(), "%s always has a graphical session", runtime.GOOS)
return
}
// clear anything inherited from the session running the test, restored on cleanup
for _, env := range append(browserSessionEnvVars(), envXDGSessionType) {
t.Setenv(env, "")
os.Unsetenv(env)
}
assert.False(t, HasGraphicalSession(), "no session variables means no graphical session")
tests := []struct {
env string
value string
expected bool
}{
{env: envDisplay, value: ":0", expected: true},
{env: envWaylandDisplay, value: "wayland-0", expected: true},
{env: envDesktopSession, value: "gnome", expected: true},
{env: envXDGCurrentDesktop, value: "KDE", expected: true},
{env: envBrowser, value: "firefox", expected: true},
{env: envXDGSessionType, value: "wayland", expected: true},
{env: envXDGSessionType, value: "x11", expected: true},
{env: envXDGSessionType, value: "mir", expected: true},
{env: envXDGSessionType, value: "tty", expected: false},
{env: envXDGSessionType, value: "unspecified", expected: false},
// an unrecognized type must not be read as a display: the device code flow works anyway
{env: envXDGSessionType, value: "something-new", expected: false},
}
for _, tt := range tests {
t.Run(tt.env+"="+tt.value, func(t *testing.T) {
t.Setenv(tt.env, tt.value)
assert.Equal(t, tt.expected, HasGraphicalSession(), "%s=%s", tt.env, tt.value)
})
}
}