feat: ship Rust login manager and GitLab release pipeline

Replace the legacy Go implementation as the production path, quarantine the reference tree, add deterministic release packaging and version metadata, and run verification through GitLab CI.
This commit is contained in:
2026-09-25 21:33:46 +05:30
parent 1723ab53b8
commit faf5b27328
49 changed files with 6795 additions and 747 deletions

73
legacy/go/session/env.go Normal file
View File

@@ -0,0 +1,73 @@
// session — environment variable setup for latchd sessions.
//
// Mirrors vendor/src/post_login/env_variables.rs.
package session
import (
"fmt"
"os"
"strconv"
)
func SetBasicVariables(user, home, shell, path string) {
os.Chdir(home)
os.Setenv("HOME", home)
os.Setenv("SHELL", shell)
os.Setenv("USER", user)
os.Setenv("LOGNAME", user)
os.Setenv("PATH", path)
}
func SetDisplay(display string) {
os.Setenv("DISPLAY", display)
}
func RemoveXDG() {
for _, v := range []string{
"XDG_SESSION_CLASS", "XDG_CURRENT_DESKTOP", "XDG_SESSION_DESKTOP",
"XDG_SEAT", "XDG_VTNR", "XDG_RUNTIME_DIR", "XDG_SESSION_ID",
"XDG_CONFIG_DIR", "XDG_CACHE_HOME", "XDG_DATA_HOME",
"XDG_STATE_HOME", "XDG_DATA_DIRS", "XDG_CONFIG_DIRS",
} {
os.Unsetenv(v)
}
}
func SetSessionParams(sessionType string) {
os.Setenv("XDG_SESSION_CLASS", "user")
os.Setenv("XDG_SESSION_TYPE", sessionType)
}
func SetSeatVars(tty uint8) {
setOrOwn("XDG_SEAT", "seat0")
setOrOwn("XDG_VTNR", strconv.Itoa(int(tty)))
}
func SetSessionVars(uid uint32) {
setOrOwn("XDG_RUNTIME_DIR", fmt.Sprintf("/run/user/%d", uid))
setOrOwn("XDG_SESSION_ID", "1")
}
func SetXDGCommonPaths(home string) {
setOrOwn("XDG_CONFIG_HOME", home+"/.config")
setOrOwn("XDG_CACHE_HOME", home+"/.cache")
setOrOwn("XDG_DATA_HOME", home+"/.local/share")
setOrOwn("XDG_STATE_HOME", home+"/.local/state")
setOrOwn("XDG_DATA_DIRS", "/usr/local/share:/usr/share")
setOrOwn("XDG_CONFIG_DIRS", "/etc/xdg")
}
func SetDesktopVars(currentDesktop, sessionDesktop string) {
if currentDesktop != "" {
setOrOwn("XDG_CURRENT_DESKTOP", currentDesktop)
}
if sessionDesktop != "" {
setOrOwn("XDG_SESSION_DESKTOP", sessionDesktop)
}
}
func setOrOwn(k, v string) {
if _, ok := os.LookupEnv(k); !ok {
os.Setenv(k, v)
}
}

200
legacy/go/session/exec.go Normal file
View File

@@ -0,0 +1,200 @@
// session — process execution and session spawning.
//
// Mirrors vendor/src/post_login/mod.rs and wait_with_log.rs.
package session
import (
"fmt"
"io"
"os"
"os/exec"
"strconv"
"strings"
"syscall"
"latchd/config"
)
type PostLoginEnv struct {
Kind string // "x11", "wayland", "tty"
Title string
XinitrcPath string
ScriptPath string
DesktopNames string // for XDG_CURRENT_DESKTOP (colon-separated)
SessionDesktop string // for XDG_SESSION_DESKTOP
}
type SpawnedEnv struct {
Kind string
Server *exec.Cmd
Client *exec.Cmd
logFd *os.File
}
const logLimit = 67_108_864
type limitWriter struct {
w io.Writer
n int64
max int64
}
func (l *limitWriter) Write(p []byte) (int, error) {
if l.n >= l.max {
return len(p), nil
}
rem := l.max - l.n
if int64(len(p)) > rem {
p = p[:rem]
}
n, err := l.w.Write(p)
l.n += int64(n)
return n, err
}
func Spawn(env *PostLoginEnv, uid, gid uint32, groups []uint32, cfg *config.Config) (*SpawnedEnv, error) {
var loginFlag string
switch cfg.ShellLoginFlag {
case config.ShellLoginShort:
loginFlag = "-l"
case config.ShellLoginLong:
loginFlag = "--login"
}
switch env.Kind {
case "x11":
return spawnX11(env, uid, gid, groups, cfg, loginFlag)
case "wayland":
return spawnWayland(env, uid, gid, groups, cfg, loginFlag)
default:
return spawnTTY(env, uid, gid, groups, cfg)
}
}
func spawnX11(env *PostLoginEnv, uid, gid uint32, groups []uint32, cfg *config.Config, loginFlag string) (*SpawnedEnv, error) {
server, err := setupX(uid, gid, groups, cfg)
if err != nil {
return nil, fmt.Errorf("X setup: %w", err)
}
args := shellCmd(cfg.SystemShell, loginFlag, cfg.X11.SetupPath+" "+env.XinitrcPath)
client := privCmd(args, uid, gid, groups)
logF := openLog(cfg.ClientLogPath)
if logF != nil {
lw := &limitWriter{w: logF, max: logLimit}
client.Stdout = lw
client.Stderr = lw
}
if err := client.Start(); err != nil {
return nil, fmt.Errorf("X11 client: %w", err)
}
return &SpawnedEnv{Kind: "x11", Server: server, Client: client, logFd: logF}, nil
}
func spawnWayland(env *PostLoginEnv, uid, gid uint32, groups []uint32, cfg *config.Config, loginFlag string) (*SpawnedEnv, error) {
args := shellCmd(cfg.SystemShell, loginFlag, env.ScriptPath)
client := privCmd(args, uid, gid, groups)
logF := openLog(cfg.ClientLogPath)
if logF != nil {
lw := &limitWriter{w: logF, max: logLimit}
client.Stdout = lw
client.Stderr = lw
}
if err := client.Start(); err != nil {
return nil, fmt.Errorf("Wayland: %w", err)
}
return &SpawnedEnv{Kind: "wayland", Client: client, logFd: logF}, nil
}
func spawnTTY(env *PostLoginEnv, uid, gid uint32, groups []uint32, cfg *config.Config) (*SpawnedEnv, error) {
shell := userShell(int(uid))
_ = env
_ = cfg
client := privCmd([]string{shell}, uid, gid, groups)
client.Stdin = os.Stdin
client.Stdout = os.Stdout
client.Stderr = os.Stderr
if err := client.Start(); err != nil {
return nil, fmt.Errorf("TTY: %w", err)
}
return &SpawnedEnv{Kind: "tty", Client: client}, nil
}
func (s *SpawnedEnv) Wait() error {
defer func() {
if s.logFd != nil {
s.logFd.Close()
}
}()
if s.Kind == "x11" && s.Server != nil {
defer func() {
s.Server.Process.Signal(syscall.SIGTERM)
s.Server.Wait()
}()
}
if s.Client != nil {
return s.Client.Wait()
}
return nil
}
func (s *SpawnedEnv) PID() int {
if s.Client != nil && s.Client.Process != nil {
return s.Client.Process.Pid
}
return 0
}
func shellCmd(shell, loginFlag, command string) []string {
a := []string{shell}
if loginFlag != "" {
a = append(a, loginFlag)
}
return append(a, "-c", command)
}
func privCmd(args []string, uid, gid uint32, groups []uint32) *exec.Cmd {
cmd := exec.Command(args[0], args[1:]...)
cmd.SysProcAttr = &syscall.SysProcAttr{
Credential: &syscall.Credential{
Uid: uid,
Gid: gid,
Groups: groups,
},
Setsid: true,
Setctty: false,
}
return cmd
}
func openLog(path string) *os.File {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
if err != nil {
return nil
}
return f
}
func userShell(uid int) string {
data, err := os.ReadFile("/etc/passwd")
if err != nil {
return "/bin/sh"
}
uidStr := strconv.Itoa(uid)
for _, line := range strings.Split(string(data), "\n") {
parts := strings.Split(line, ":")
if len(parts) >= 7 && parts[2] == uidStr && parts[6] != "" {
sh := parts[6]
if sh != "/sbin/nologin" && sh != "/usr/sbin/nologin" && sh != "/bin/false" {
return sh
}
}
}
return "/bin/sh"
}

149
legacy/go/session/scan.go Normal file
View File

@@ -0,0 +1,149 @@
// session — environment scanning from xsessions, wayland-sessions,
// and custom script directories. Includes desktop entry parser.
//
// Mirrors vendor/src/post_login/mod.rs:get_envs and parse_desktop_entry.
package session
import (
"bufio"
"fmt"
"os"
"path/filepath"
"strings"
)
type DesktopEntry struct {
Name string
Exec string
DesktopNames string
Hidden bool
NoDisplay bool
}
func parseDesktopEntry(path string) (*DesktopEntry, error) {
f, err := os.Open(path)
if err != nil {
return nil, fmt.Errorf("open %s: %w", path, err)
}
defer f.Close()
e := &DesktopEntry{}
inSection := false
sc := bufio.NewScanner(f)
for sc.Scan() {
line := strings.TrimSpace(sc.Text())
if line == "" || line[0] == '#' {
continue
}
if line[0] == '[' && line[len(line)-1] == ']' {
inSection = line == "[Desktop Entry]"
continue
}
if !inSection {
continue
}
eq := strings.IndexByte(line, '=')
if eq < 0 {
continue
}
k, v := line[:eq], line[eq+1:]
switch k {
case "Name":
e.Name = v
case "Exec":
e.Exec = v
case "DesktopNames":
e.DesktopNames = v
case "Hidden":
e.Hidden = strings.EqualFold(v, "true")
case "NoDisplay":
e.NoDisplay = strings.EqualFold(v, "true")
}
}
if sc.Err() != nil {
return nil, sc.Err()
}
if e.Exec == "" {
return nil, fmt.Errorf("no Exec in %s", path)
}
if e.Name == "" {
e.Name = e.Exec
}
return e, nil
}
func ScanEnvs(xSessions, wlSessions, xScripts, wlScripts string, includeTTY bool) []PostLoginEnv {
var envs []PostLoginEnv
scanDesktopDir(xSessions, "x11", &envs)
scanDesktopDir(wlSessions, "wayland", &envs)
scanScriptDir(xScripts, "x11", &envs)
scanScriptDir(wlScripts, "wayland", &envs)
if len(envs) == 0 || includeTTY {
envs = append(envs, PostLoginEnv{Kind: "tty", Title: "TTYSHELL"})
}
return envs
}
func scanDesktopDir(dir, kind string, envs *[]PostLoginEnv) {
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for _, e := range entries {
if e.IsDir() || filepath.Ext(e.Name()) != ".desktop" {
continue
}
path := filepath.Join(dir, e.Name())
de, err := parseDesktopEntry(path)
if err != nil || de.Hidden || de.NoDisplay {
continue
}
desktopNames, sessionDesktop := splitDesktopNames(de.DesktopNames, e.Name())
*envs = append(*envs, PostLoginEnv{
Kind: kind,
Title: de.Name,
XinitrcPath: de.Exec,
ScriptPath: de.Exec,
DesktopNames: desktopNames,
SessionDesktop: sessionDesktop,
})
}
}
func splitDesktopNames(raw, fallback string) (currentDesktop, sessionDesktop string) {
if raw == "" {
return fallback, fallback
}
parts := strings.SplitN(raw, ";", 2)
sessionDesktop = strings.TrimSpace(parts[0])
currentDesktop = strings.ReplaceAll(raw, ";", ":")
return
}
func scanScriptDir(dir, kind string, envs *[]PostLoginEnv) {
entries, err := os.ReadDir(dir)
if err != nil {
return
}
for _, e := range entries {
if e.IsDir() {
continue
}
info, err := e.Info()
if err != nil {
continue
}
if info.Mode()&0111 == 0 {
continue
}
path := filepath.Join(dir, e.Name())
*envs = append(*envs, PostLoginEnv{
Kind: kind,
Title: e.Name(),
XinitrcPath: path,
ScriptPath: path,
})
}
}

View File

@@ -0,0 +1,4 @@
package session
// Wayland session spawning is handled in exec.go/spawnWayland.
// This file exists for future Wayland-specific compositor logic.

103
legacy/go/session/x11.go Normal file
View File

@@ -0,0 +1,103 @@
// session — X11 server setup.
//
// Mirrors vendor/src/post_login/x.rs:setup_x.
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
}
// Ignore SIGUSR1 so Xorg sends it when ready.
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
}