Files
latchd/legacy/go/session/x11.go
Zane Walker f2a885d621 refactor: split the TUI and refresh project docs
Move login state, authentication, discovery, input, rendering, and tests into focused TUI modules. Add detailed CLI error reports with recovery commands, align the build and host-check documentation, and keep the legacy Go tree outside normal builds.

Checks: make check; make integration-test; go test ./... (legacy/go).
2026-10-05 21:37:15 +05:30

101 lines
2.4 KiB
Go

package session
import (
"crypto/rand"
"encoding/hex"
"fmt"
"os"
"os/exec"
"os/signal"
"path/filepath"
"syscall"
"time"
"latchd/config"
)
func setupX(uid, gid uint32, groups []uint32, cfg *config.Config) (*exec.Cmd, error) {
display := os.Getenv("DISPLAY")
vtnr := os.Getenv("XDG_VTNR")
home := os.Getenv("HOME")
if display == "" || vtnr == "" || home == "" {
return nil, fmt.Errorf("missing DISPLAY/XDG_VTNR/HOME env vars")
}
xauthPath := filepath.Join(home, ".Xauthority")
os.Remove(xauthPath)
cookie, err := mCookie()
if err != nil {
return nil, err
}
xauthCmd := exec.Command(cfg.X11.XauthPath, "add", display, ".", cookie)
xauthCmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{Uid: uid, Gid: gid, Groups: groups},
}
if out, err := xauthCmd.CombinedOutput(); err != nil {
return nil, fmt.Errorf("xauth: %w — %s", err, string(out))
}
os.Setenv("XAUTHORITY", xauthPath)
dd := vtnr
if len(dd) == 1 {
dd = "0" + dd
}
// Xorg sends SIGUSR1 only when the parent ignores the signal during startup.
signal.Ignore(syscall.SIGUSR1)
cmd := exec.Command(cfg.SystemShell, "-c",
fmt.Sprintf("%s %s vt%s", cfg.X11.ServerPath, display, dd))
if logF := openLog(cfg.X11.ServerLogPath); logF != nil {
lw := &limitWriter{w: logF, max: logLimit}
cmd.Stdout = lw
cmd.Stderr = lw
defer logF.Close()
}
if err := cmd.Start(); err != nil {
signal.Reset(syscall.SIGUSR1)
return nil, fmt.Errorf("X server: %w", err)
}
signal.Reset(syscall.SIGUSR1)
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGUSR1)
defer signal.Stop(ch)
timeout := time.Duration(cfg.X11.ServerTimeoutSecs) * time.Second
deadline := time.After(timeout)
tick := time.NewTicker(100 * time.Millisecond)
defer tick.Stop()
for {
select {
case <-ch:
return cmd, nil
case <-deadline:
if cfg.X11.ServerTimeoutSecs == 0 {
continue
}
cmd.Process.Signal(syscall.SIGTERM)
cmd.Wait()
return nil, fmt.Errorf("X server timed out after %ds", cfg.X11.ServerTimeoutSecs)
case <-tick.C:
var ws syscall.WaitStatus
if pid, err := syscall.Wait4(cmd.Process.Pid, &ws, syscall.WNOHANG, nil); err == nil && pid == cmd.Process.Pid {
return nil, fmt.Errorf("X server exited with status %d", ws.ExitStatus())
}
}
}
}
func mCookie() (string, error) {
var b [16]byte
if _, err := rand.Read(b[:]); err != nil {
return "", err
}
return hex.EncodeToString(b[:]), nil
}