Collapse X11 DISPLAY/XAUTHORITY auto-detect logs into one line

This commit is contained in:
Viktor Liu
2026-05-20 13:13:53 +02:00
parent 896530fd82
commit 517bea0daf
21 changed files with 131 additions and 126 deletions

View File

@@ -48,7 +48,7 @@ var (
procWTSFreeMemory = wtsapi32.NewProc("WTSFreeMemory")
procWTSQuerySessionInformation = wtsapi32.NewProc("WTSQuerySessionInformationW")
iphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
iphlpapi = windows.NewLazySystemDLL("iphlpapi.dll")
procGetExtendedTcpTable = iphlpapi.NewProc("GetExtendedTcpTable")
)
@@ -283,7 +283,6 @@ func getSystemTokenForSession(sessionID uint32) (windows.Token, error) {
return dup, nil
}
// injectEnvVar appends a KEY=VALUE entry to a Unicode environment block.
// The block is a sequence of null-terminated UTF-16 strings, terminated by
// an extra null. Returns the new []uint16 backing slice; the caller must

View File

@@ -22,24 +22,24 @@ import (
var darwinCaptureOnce sync.Once
var (
cgMainDisplayID func() uint32
cgDisplayPixelsWide func(uint32) uintptr
cgDisplayPixelsHigh func(uint32) uintptr
cgDisplayCreateImage func(uint32) uintptr
cgImageGetWidth func(uintptr) uintptr
cgImageGetHeight func(uintptr) uintptr
cgImageGetBytesPerRow func(uintptr) uintptr
cgImageGetBitsPerPixel func(uintptr) uintptr
cgImageGetDataProvider func(uintptr) uintptr
cgDataProviderCopyData func(uintptr) uintptr
cgImageRelease func(uintptr)
cfDataGetLength func(uintptr) int64
cfDataGetBytePtr func(uintptr) uintptr
cfRelease func(uintptr)
cgMainDisplayID func() uint32
cgDisplayPixelsWide func(uint32) uintptr
cgDisplayPixelsHigh func(uint32) uintptr
cgDisplayCreateImage func(uint32) uintptr
cgImageGetWidth func(uintptr) uintptr
cgImageGetHeight func(uintptr) uintptr
cgImageGetBytesPerRow func(uintptr) uintptr
cgImageGetBitsPerPixel func(uintptr) uintptr
cgImageGetDataProvider func(uintptr) uintptr
cgDataProviderCopyData func(uintptr) uintptr
cgImageRelease func(uintptr)
cfDataGetLength func(uintptr) int64
cfDataGetBytePtr func(uintptr) uintptr
cfRelease func(uintptr)
cgRequestScreenCaptureAccess func() bool
cgEventCreate func(uintptr) uintptr
cgEventGetLocation func(uintptr) cgPoint
darwinCaptureReady bool
cgEventCreate func(uintptr) uintptr
cgEventGetLocation func(uintptr) cgPoint
darwinCaptureReady bool
)
// cgPoint mirrors CoreGraphics CGPoint: two doubles, 16 bytes, returned
@@ -98,7 +98,6 @@ func initDarwinCapture() {
})
}
// CGCapturer captures the macOS main display using Core Graphics.
type CGCapturer struct {
displayID uint32

View File

@@ -227,4 +227,3 @@ func swizzleFB32(dst []byte, dstStride int, src []byte, srcStride, w, h int, shi
}
}
}

View File

@@ -109,7 +109,6 @@ func (p *FBPoller) ensureCapturerLocked() error {
return nil
}
var _ ScreenCapturer = (*FBPoller)(nil)
var _ captureIntoer = (*FBPoller)(nil)

View File

@@ -18,6 +18,12 @@ import (
"github.com/jezek/xgb/xproto"
)
// x11SocketDir is the well-known directory where X servers create their
// abstract UNIX-domain sockets, named "X<display>". Used both for
// auto-detecting an existing display and for placing/probing sockets of
// virtual sessions we spawn.
const x11SocketDir = "/tmp/.X11-unix"
// X11Capturer captures the screen from an X11 display using the MIT-SHM extension.
type X11Capturer struct {
mu sync.Mutex
@@ -26,7 +32,7 @@ type X11Capturer struct {
w, h int
shmID int
shmAddr []byte
shmSeg uint32 // shm.Seg
shmSeg uint32
useSHM bool
// bufs double-buffers output images so the X11Poller's capture loop can
// overwrite one while the session is still encoding the other. Before
@@ -83,7 +89,7 @@ func detectX11FromProc() bool {
// detectX11FromSockets checks /tmp/.X11-unix/ for X sockets and uses ps
// to find the auth file. Works on FreeBSD and other systems without /proc.
func detectX11FromSockets() bool {
entries, err := os.ReadDir("/tmp/.X11-unix")
entries, err := os.ReadDir(x11SocketDir)
if err != nil {
return false
}
@@ -96,12 +102,12 @@ func detectX11FromSockets() bool {
}
display := ":" + name[1:]
os.Setenv("DISPLAY", display)
log.Infof("auto-detected DISPLAY=%s (from socket)", display)
// Try to find -auth from ps output.
if auth := findXorgAuthFromPS(); auth != "" {
auth := findXorgAuthFromPS()
if auth != "" {
os.Setenv("XAUTHORITY", auth)
log.Infof("auto-detected XAUTHORITY=%s (from ps)", auth)
log.Infof("auto-detected DISPLAY=%s (from socket) XAUTHORITY=%s (from ps)", display, auth)
} else {
log.Infof("auto-detected DISPLAY=%s (from socket)", display)
}
return true
}
@@ -150,11 +156,12 @@ func parseXorgArgs(args []string) (display, auth string) {
func setDisplayEnv(display, auth string) {
os.Setenv("DISPLAY", display)
log.Infof("auto-detected DISPLAY=%s", display)
if auth != "" {
os.Setenv("XAUTHORITY", auth)
log.Infof("auto-detected XAUTHORITY=%s", auth)
log.Infof("auto-detected DISPLAY=%s XAUTHORITY=%s", display, auth)
return
}
log.Infof("auto-detected DISPLAY=%s", display)
}
func splitCmdline(data []byte) []string {

View File

@@ -37,7 +37,7 @@ type copyRectDetector struct {
cols, rows int
// tileHash[ty*cols + tx] is the current hash of the tile at (tx, ty)
// in the previous frame. Lookup uses this to detect stale prevTiles
// entries incremental updates may leave hash→pos entries pointing
// entries: incremental updates may leave hash→pos entries pointing
// at a tile whose content has since changed.
tileHash []uint64
// prevTiles maps a tile hash to a (x, y) origin in the previous frame.

View File

@@ -43,7 +43,7 @@ func TestCopyRectDetector_DetectsVerticalScroll(t *testing.T) {
fillTile(prev, tx*ts, ty*ts, ts, byte(tx*40), byte(ty*60), 0x80)
}
}
// cur: simulate a single-tile-row scroll upward every tile copied from
// cur: simulate a single-tile-row scroll upward, every tile copied from
// the row below in prev, top row is new content.
for ty := 0; ty < 2; ty++ {
for tx := 0; tx < 4; tx++ {

View File

@@ -42,10 +42,10 @@ type winPoint struct {
}
type winCursorInfo struct {
Size uint32
Flags uint32
Cursor windows.Handle
PtPos winPoint
Size uint32
Flags uint32
Cursor windows.Handle
PtPos winPoint
}
type winIconInfo struct {

View File

@@ -104,7 +104,9 @@ var (
userActivityID uint32
preventSleepID uint32
preventSleepHeld bool
preventSleepRef int // refcount across concurrent injectors/sessions
// preventSleepRef tracks the refcount of held assertions across
// concurrent injectors and sessions.
preventSleepRef int
darwinInputReady bool
darwinEventSource uintptr
@@ -503,10 +505,10 @@ func (m *MacInputInjector) postScrollWheel(src uintptr, buttonMask uint16) {
}
// scrollPixelsPerWheelTick is the pixel delta we post for one VNC wheel
// button event. noVNC accumulates the host wheel/trackpad deltaY and
// emits one press+release per ~10 px, so a real gesture arrives as many
// small events; 20 px per event keeps the resulting macOS scroll fluid
// without overshooting on a single notch.
// button event. Browser-based RFB clients typically emit one press+release
// per ~10 px of host wheel/trackpad motion, so a real gesture arrives as
// many small events; ~20 px per event keeps the resulting macOS scroll
// fluid without overshooting on a single notch.
const scrollPixelsPerWheelTick int32 = 22
func (m *MacInputInjector) postMouse(src uintptr, eventType int32, x, y float64, button int32) {
@@ -543,8 +545,8 @@ func (m *MacInputInjector) postScroll(src uintptr, deltaY int32) {
return
}
// CGEventCreateScrollWheelEvent(source, units, wheelCount, wheel1delta).
// Pixel units (0) feel smoother under noVNC's "one event per ~10 px of
// host wheel" emission than line units (1) where each event jumps a
// Pixel units (0) feel smoother given the small per-event deltas typical
// of RFB wheel events than line units (1) where each event jumps a
// whole line. Variadic C function, pass via SyscallN.
r1, _, _ := purego.SyscallN(cgEventCreateScrollWheelEventAddr,
src, 0, 1, uintptr(uint32(deltaY)))
@@ -568,10 +570,10 @@ func (m *MacInputInjector) SetClipboard(text string) {
}
// TypeText synthesizes the given text as keystrokes via Core Graphics.
// Used by the dashboard's Paste button so the host clipboard reaches
// the focused remote app even when the app doesn't honor pbpaste-style
// clipboard sync (e.g. login screens, locked-down apps). ASCII printable
// runes only; others are skipped.
// Lets a client push host clipboard content to the focused remote app
// even when the app doesn't honor pbpaste-style clipboard sync (e.g.
// login screens, locked-down apps). ASCII printable runes only; others
// are skipped.
func (m *MacInputInjector) TypeText(text string) {
wakeDisplay()
src := ensureEventSource()

View File

@@ -48,7 +48,6 @@ const (
keyeventfScanCode = 0x0008
)
// maxTypedClipboardChars caps the number of characters we will synthesize as
// keystrokes when falling back on the Winlogon desktop. Passwords are short;
// a huge clipboard getting typed into the login screen would be surprising.

View File

@@ -219,16 +219,15 @@ func (x *X11InputInjector) SetClipboard(text string) {
}
}
// TypeText synthesizes the given text as keystrokes via XTest. We can
// no longer just stuff the host clipboard with xclip and expect Ctrl+V
// to do the rest, because the Paste button is also used at places where
// the focused application isn't a clipboard-aware one (e.g. a TTY login
// in an X11 session, an SDDM/GDM password field that ignores XSelection,
// or a kiosk app). Typing keystrokes covers all of those.
// TypeText synthesizes the given text as keystrokes via XTest. Used in
// places where the focused application isn't clipboard-aware (e.g. a TTY
// login in an X11 session, an SDDM/GDM password field that ignores
// XSelection, or a kiosk app), so stuffing the X clipboard and relying on
// Ctrl+V would not reach the input.
//
// Limitation: only ASCII printable characters are typed. Non-ASCII runes
// are skipped: a paste workflow for them needs Wayland-aware text input
// or layout introspection that we don't have.
// or layout introspection that this path does not implement.
func (x *X11InputInjector) TypeText(text string) {
const maxChars = 4096
count := 0
@@ -289,7 +288,7 @@ func (x *X11InputInjector) GetClipboard() string {
out, err := cmd.Output()
if err != nil {
// Exit status 1 just means there is no STRING selection set yet,
// which is the steady state on a fresh Xvfb session logging it
// which is the steady state on a fresh Xvfb session, logging it
// every clipboard poll (2s) floods the trace stream.
return ""
}

View File

@@ -26,8 +26,8 @@ type SessionTick struct {
}
// sessionTickInterval is how often metricsConn emits a SessionTick. One
// second matches noVNC's request cadence so each tick covers roughly one
// FBU round-trip during steady-state activity.
// second covers roughly one FBU round-trip at typical client request
// cadences during steady-state activity.
const sessionTickInterval = time.Second
// metricsConn wraps a net.Conn and tracks per-session byte / write / FBU

View File

@@ -42,17 +42,17 @@ const (
// clientNetbirdTypeText is a NetBird-specific message that asks the
// server to synthesize the given text as keystrokes regardless of the
// active desktop. Used by the dashboard's Paste button to push host
// clipboard content into a Windows secure desktop (Winlogon, UAC),
// where the OS clipboard is isolated. Format mirrors clientCutText:
// 1-byte message type + 3-byte padding + 4-byte length + text bytes.
// The opcode is in the vendor-specific range (>=128).
// active desktop. Lets a client push host clipboard content into a
// Windows secure desktop (Winlogon, UAC), where the OS clipboard is
// isolated. Format mirrors clientCutText: 1-byte message type + 3-byte
// padding + 4-byte length + text bytes. The opcode is in the
// vendor-specific range (>=128).
clientNetbirdTypeText = 250
// clientNetbirdShowRemoteCursor toggles "show remote cursor" mode.
// When enabled the encoder composites the server cursor sprite into
// the captured framebuffer and suppresses the Cursor pseudo-encoding
// so the dashboard sees a single pointer at the remote position.
// so the client sees a single pointer at the remote position.
// Wire format: 1-byte msgType + 1-byte enable flag + 6 padding bytes
// reserved for future arguments (so the message is fixed-size).
clientNetbirdShowRemoteCursor = 251
@@ -71,13 +71,13 @@ const (
// Pseudo-encodings carried over wire as rects with a negative
// encoding value. The client advertises supported optional protocol
// extensions by listing these in SetEncodings.
pseudoEncCursor = -239
pseudoEncDesktopSize = -223
pseudoEncLastRect = -224
pseudoEncQEMUExtendedKeyEvent = -258
pseudoEncDesktopName = -307
pseudoEncExtendedDesktopSize = -308
pseudoEncExtendedMouseButtons = -316
pseudoEncCursor = -239
pseudoEncDesktopSize = -223
pseudoEncLastRect = -224
pseudoEncQEMUExtendedKeyEvent = -258
pseudoEncDesktopName = -307
pseudoEncExtendedDesktopSize = -308
pseudoEncExtendedMouseButtons = -316
// Quality/Compression level pseudo-encodings. The client picks one
// value from each range to tune JPEG quality and zlib effort. 0 is
@@ -405,10 +405,10 @@ func coalesceRects(in [][4]int) [][4]int {
// algorithm can be split across small methods without long parameter lists
// and to keep each method's cognitive complexity below Sonar's threshold.
type rectCoalescer struct {
out [][4]int
prevRowStart, prevRowEnd int
curRowStart int
curY int
out [][4]int
prevRowStart, prevRowEnd int
curRowStart int
curY int
}
func newRectCoalescer(capacity int) *rectCoalescer {

View File

@@ -32,8 +32,8 @@ const (
)
// RFB security-failure reason codes sent to the client. These prefixes are
// stable so dashboard integrations can branch on them without parsing
// free text. Format: "CODE: human message".
// stable so clients can branch on them without parsing free text.
// Format: "CODE: human message".
const (
RejectCodeJWTMissing = "AUTH_JWT_MISSING"
RejectCodeJWTExpired = "AUTH_JWT_EXPIRED"
@@ -114,10 +114,9 @@ type InputInjector interface {
// GetClipboard returns the current system clipboard text.
GetClipboard() string
// TypeText synthesizes the given text as keystrokes on the active
// desktop. Used by the dashboard's Paste button to push host clipboard
// content into a secure desktop (Winlogon/UAC) where the clipboard is
// isolated. On platforms or sessions without keystroke synthesis it
// may be a no-op.
// desktop. Used to push host clipboard content into a secure desktop
// (Winlogon/UAC) where the clipboard is isolated. On platforms or
// sessions without keystroke synthesis it may be a no-op.
TypeText(text string)
}
@@ -132,10 +131,11 @@ type JWTConfig struct {
// connectionHeader is sent by the client before the RFB handshake to specify
// the VNC session mode and authenticate.
type connectionHeader struct {
mode byte
username string
jwt string
sessionID uint32 // Windows session ID (0 = console/auto)
mode byte
username string
jwt string
// sessionID is the Windows session ID; 0 selects the console session.
sessionID uint32
// width and height request the virtual display geometry for session mode.
// Zero means use the default.
width uint16
@@ -159,9 +159,11 @@ type Server struct {
injector InputInjector
serviceMode bool
disableAuth bool
localAddr netip.Addr // NetBird WireGuard IP this server is bound to
network netip.Prefix // NetBird overlay network
log *log.Entry
// localAddr is the NetBird WireGuard IP this server is bound to.
localAddr netip.Addr
// network is the NetBird overlay network.
network netip.Prefix
log *log.Entry
mu sync.Mutex
listener net.Listener
@@ -173,7 +175,8 @@ type Server struct {
jwtExtractor *nbjwt.ClaimsExtractor
authorizer *sshauth.Authorizer
netstackNet *netstack.Net
agentToken []byte // raw token bytes for agent-mode auth
// agentToken holds the raw token bytes for agent-mode auth.
agentToken []byte
sessionsMu sync.Mutex
sessionSeq uint64
@@ -212,14 +215,14 @@ type virtualSessionManager interface {
}
// New creates a VNC server with the given screen capturer and input injector.
// Authentication is handled by the dashboard JWT exchange after the RFB
// handshake; the protocol-level VNC password scheme is not supported.
// Authentication uses a JWT supplied by the client in the connection
// header; the protocol-level VNC password scheme is not supported.
func New(capturer ScreenCapturer, injector InputInjector) *Server {
return &Server{
capturer: capturer,
injector: injector,
authorizer: sshauth.NewAuthorizer(),
log: log.WithField("component", "vnc-server"),
capturer: capturer,
injector: injector,
authorizer: sshauth.NewAuthorizer(),
log: log.WithField("component", "vnc-server"),
sessions: make(map[uint64]ActiveSessionInfo),
sessionConns: make(map[uint64]net.Conn),
}
@@ -576,7 +579,7 @@ func (s *Server) handleConnection(conn net.Conn) {
serverH: capturer.Height(),
log: connLog,
// Virtual sessions run on Xvfb which has no usable cursor source,
// so we skip the Cursor pseudo-encoding and let the dashboard's
// so we skip the Cursor pseudo-encoding and let the client's
// local fallback show instead.
disableCursor: header.mode == ModeSession,
}

View File

@@ -27,7 +27,7 @@ func (s *Server) platformSessionManager() virtualSessionManager {
// connection to a per-user agent. The agent is spawned lazily on the
// first connection (and respawned after a console-user change) via
// launchctl asuser, which is the only mechanism that lands a child
// inside the user's Aqua session where WindowServer and TCC grants
// inside the user's Aqua session, where WindowServer and TCC grants
// for screen capture work.
func (s *Server) serviceAcceptLoop() {
mgr := newDarwinAgentManager(s.ctx)

View File

@@ -106,7 +106,7 @@ type session struct {
// showRemoteCursor switches the encoder to compositing the server
// cursor sprite into the captured framebuffer at the remote position
// instead of emitting the Cursor pseudo-encoding. Toggled by the
// dashboard via clientNetbirdShowRemoteCursor.
// client via clientNetbirdShowRemoteCursor.
showRemoteCursor bool
// cursorWarnOnce throttles the diagnostic emitted when remote-cursor
// compositing falls back to a no-op (capturer cannot supply a sprite
@@ -140,9 +140,9 @@ type session struct {
// pointerMu guards the cached last cursor position used by
// releaseStickyInput so the disconnect-time button-release event
// targets the cursor's current spot instead of warping to (0, 0).
pointerMu sync.Mutex
lastPointerX int
lastPointerY int
pointerMu sync.Mutex
lastPointerX int
lastPointerY int
}
type fbRequest struct {
@@ -226,8 +226,9 @@ func (s *session) handshake() error {
}
// sendSecurityTypes advertises only secNone. Authentication and access
// control are layered on top by the dashboard JWT exchange after the RFB
// handshake completes, not by the protocol-level password scheme.
// control happen in the NetBird connection header (JWT, mode, username)
// that precedes the RFB handshake, not via the protocol-level password
// scheme.
func (s *session) sendSecurityTypes() error {
_, err := s.conn.Write([]byte{1, secNone})
return err
@@ -502,9 +503,9 @@ func (s *session) handleFBUpdateRequest() error {
}
// SendDesktopName pushes a DesktopName pseudo-encoded update to the
// client if it advertised support. Used by the server to keep the
// dashboard title in sync with the active session (e.g. username
// changes after login on a virtual session).
// client if it advertised support. Lets the client keep its window title
// in sync with the active session (e.g. username changes after login on
// a virtual session).
func (s *session) SendDesktopName(name string) error {
s.encMu.RLock()
supported := s.clientSupportsDesktopName
@@ -601,9 +602,9 @@ var stickyModifierKeysyms = [...]uint32{
0xffe9, 0xffea, // Alt_L, Alt_R
0xffe7, 0xffe8, // Meta_L, Meta_R
0xffeb, 0xffec, // Super_L, Super_R
0xff7e, // Mode_switch
0xfe03, // ISO_Level3_Shift (AltGr)
0xffe5, // Caps_Lock (release if user dropped mid-press)
0xff7e, // Mode_switch
0xfe03, // ISO_Level3_Shift (AltGr)
0xffe5, // Caps_Lock (release if user dropped mid-press)
}
// releaseStickyInput synthesizes key-up for modifier keysyms and a

View File

@@ -220,9 +220,10 @@ func (s *session) writeExtClipMessage(payload []byte) error {
return err
}
// handleTypeText handles the NetBird-specific PasteAndType message used by
// the dashboard's Paste button. Wire format mirrors CutText: 3-byte
// padding + 4-byte length + text bytes.
// handleTypeText handles the NetBird-specific PasteAndType message that
// pushes host clipboard content as synthesized keystrokes, used to reach
// secure desktops where the clipboard is isolated. Wire format mirrors
// CutText: 3-byte padding + 4-byte length + text bytes.
func (s *session) handleTypeText() error {
var header [7]byte
if _, err := io.ReadFull(s.conn, header[:]); err != nil {

View File

@@ -8,9 +8,9 @@ import (
"io"
)
// handleShowRemoteCursor handles the NetBird-specific RFB message used by
// the dashboard to toggle "show remote cursor" mode. Wire format: 1-byte
// enable flag (0/1) plus 6 padding bytes reserved for future arguments.
// handleShowRemoteCursor handles the NetBird-specific RFB message that
// toggles "show remote cursor" mode. Wire format: 1-byte enable flag
// (0/1) plus 6 padding bytes reserved for future arguments.
func (s *session) handleShowRemoteCursor() error {
var data [7]byte
if _, err := io.ReadFull(s.conn, data[:]); err != nil {
@@ -25,9 +25,8 @@ func (s *session) handleShowRemoteCursor() error {
}
// maybeCompositeCursor blends the current server cursor into img when the
// dashboard has enabled "show remote cursor" mode. Returns silently in
// every error path: a failed compositing must not stop the regular encode
// flow.
// client has enabled "show remote cursor" mode. Returns silently in every
// error path: a failed compositing must not stop the regular encode flow.
func (s *session) maybeCompositeCursor(img *image.RGBA) {
s.encMu.RLock()
enabled := s.showRemoteCursor

View File

@@ -28,4 +28,3 @@ func swizzleBGRAtoRGBA(dst, src []byte) {
dp[i] = 0xFF000000 | (p & 0x0000FF00) | ((p & 0x00FF0000) >> 16) | ((p & 0x000000FF) << 16)
}
}

View File

@@ -101,12 +101,11 @@ func TestEncodeTightJPEG(t *testing.T) {
func TestSampledColorCount(t *testing.T) {
uniform := makeUniformImage(64, 64, 0x10, 0x20, 0x30)
if c := sampledColorCountInto(map[uint32]struct{}{},uniform, 0, 0, 64, 64, 32); c != 1 {
if c := sampledColorCountInto(map[uint32]struct{}{}, uniform, 0, 0, 64, 64, 32); c != 1 {
t.Fatalf("uniform should be 1 colour, got %d", c)
}
rnd := makeBenchImage(128, 128, 1)
if c := sampledColorCountInto(map[uint32]struct{}{},rnd, 0, 0, 128, 128, 16); c <= 16 {
if c := sampledColorCountInto(map[uint32]struct{}{}, rnd, 0, 0, 128, 128, 16); c <= 16 {
t.Fatalf("random image should exceed colour cap, got %d", c)
}
}

View File

@@ -118,7 +118,7 @@ func (vs *VirtualSession) start() error {
return err
}
socketPath := fmt.Sprintf("/tmp/.X11-unix/X%s", vs.display[1:])
socketPath := fmt.Sprintf("%s/X%s", x11SocketDir, vs.display[1:])
if err := waitForPath(socketPath, 5*time.Second); err != nil {
vs.stopXvfb()
return fmt.Errorf("wait for X11 socket %s: %w", socketPath, err)
@@ -196,7 +196,7 @@ func (vs *VirtualSession) isAlive() bool {
return false
}
// Verify the X socket still exists on disk.
socketPath := fmt.Sprintf("/tmp/.X11-unix/X%s", display[1:])
socketPath := fmt.Sprintf("%s/X%s", x11SocketDir, display[1:])
if _, err := os.Stat(socketPath); err != nil {
return false
}
@@ -590,7 +590,7 @@ func bestSessionCandidate(candidates []sessionCandidate) sessionCandidate {
func findFreeDisplay() (string, error) {
for n := 50; n < 200; n++ {
lockFile := fmt.Sprintf("/tmp/.X%d-lock", n)
socketFile := fmt.Sprintf("/tmp/.X11-unix/X%d", n)
socketFile := fmt.Sprintf("%s/X%d", x11SocketDir, n)
if _, err := os.Stat(lockFile); err == nil {
continue
}