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).
101 lines
2.4 KiB
Go
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
|
|
}
|