Files
latchd/legacy/go/main.go
Zane Walker faf5b27328 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.
2026-09-25 21:33:46 +05:30

264 lines
6.2 KiB
Go

// latchd — TUI display/login manager for GNU/Linux and BSD.
//
// Supports TTY, X11, and Wayland sessions with PAM authentication.
// Go rewrite of Lemurs (Rust source under vendor/).
package main
import (
"flag"
"fmt"
"io"
"log"
"os"
"os/signal"
"syscall"
"unsafe"
tea "github.com/charmbracelet/bubbletea"
"latchd/auth"
"latchd/config"
"latchd/login"
"latchd/session"
"latchd/tui/bg"
)
var (
Version = "0.1.0"
Commit = "unknown"
BuildTime = "unknown"
)
const defaultConfig = "/etc/latchd/config.toml"
const defaultVars = "/etc/latchd/variables.toml"
func main() {
var (
configPath string
varsPath string
ttyNum int
preview bool
noLog bool
showConf bool
printVer bool
xsessions string
wlsessions string
initPath string
)
flag.StringVar(&configPath, "config", defaultConfig, "")
flag.StringVar(&configPath, "c", defaultConfig, "")
flag.StringVar(&varsPath, "variables", defaultVars, "")
flag.StringVar(&varsPath, "v", defaultVars, "")
flag.IntVar(&ttyNum, "tty", 0, "")
flag.BoolVar(&preview, "preview", false, "")
flag.BoolVar(&noLog, "no-log", false, "")
flag.BoolVar(&showConf, "show-config", false, "")
flag.BoolVar(&printVer, "version", false, "")
flag.BoolVar(&printVer, "V", false, "")
flag.StringVar(&xsessions, "xsessions", "", "")
flag.StringVar(&wlsessions, "wlsessions", "", "")
flag.StringVar(&initPath, "initial-path", "", "")
flag.Parse()
if printVer {
fmt.Printf("latchd %s commit %s built %s\n", Version, Commit, BuildTime)
os.Exit(0)
}
cfg := config.Default()
if initPath != "" {
cfg.InitialPath = initPath
}
if xsessions != "" {
cfg.X11.SessionsPath = xsessions
}
if wlsessions != "" {
cfg.Wayland.SessionsPath = wlsessions
}
vars, _ := config.LoadVariables(varsPath)
if err := config.LoadPartial(configPath, vars, &cfg); err != nil {
if configPath != defaultConfig {
fmt.Fprintf(os.Stderr, "config: %v\n", err)
os.Exit(1)
}
}
if showConf {
fmt.Printf("tty = %d\npam_service = %q\nshell = %q\nfocus = %q\n",
cfg.TTY, cfg.PAMService, cfg.SystemShell, cfg.FocusBehaviour)
os.Exit(0)
}
if !noLog && !preview {
setupLog(cfg.MainLogPath)
}
if !preview {
if _, ok := os.LookupEnv("XDG_SESSION_TYPE"); ok {
fmt.Fprintln(os.Stderr, "latchd: already in a session — use --preview")
os.Exit(1)
}
if os.Getuid() != 0 {
fmt.Fprintf(os.Stderr, "latchd: must be root (uid %d)\n", os.Getuid())
os.Exit(1)
}
if ttyNum != 0 {
cfg.TTY = uint8(ttyNum)
}
if err := chvt(int(cfg.TTY)); err != nil {
log.Printf("chvt %d: %v", cfg.TTY, err)
}
}
for {
res := launchUI(cfg, preview)
if res == nil {
break
}
if err := spawnSession(res); err != nil {
log.Printf("session: %v", err)
}
if !preview {
chvt(int(cfg.TTY))
}
}
}
func launchUI(cfg config.Config, preview bool) *login.Result {
m := login.New(cfg, preview)
if cfg.Background.ShowBackground {
m.SetAutomata(bg.NewAutomata(80, 24))
}
p := tea.NewProgram(m, tea.WithAltScreen(), tea.WithMouseCellMotion())
final, err := p.Run()
if err != nil {
log.Printf("tui: %v", err)
os.Exit(1)
}
lm, ok := final.(*login.Model)
if !ok {
return nil
}
return lm.Result()
}
func spawnSession(res *login.Result) error {
if res.Preview {
log.Println("preview — skipping real session")
return nil
}
log.Printf("spawning %s → %s (%s)", res.Username, res.Env.Title, res.Env.Kind)
creds, err := auth.Validate(res.Username, res.Password, res.Config.PAMService)
if err != nil {
return fmt.Errorf("auth: %w", err)
}
defer creds.CloseSession()
if err := creds.OpenSession(); err != nil {
log.Printf("pam session warning: %v", err)
}
session.SetBasicVariables(creds.Username, creds.HomeDir, creds.Shell, res.Config.InitialPath)
if res.Env.Kind == "x11" {
session.SetDisplay(res.Config.X11.Display)
}
session.RemoveXDG()
session.SetSessionParams(res.Env.Kind)
session.SetSeatVars(res.Config.TTY)
session.SetSessionVars(creds.UID)
session.SetXDGCommonPaths(creds.HomeDir)
session.SetDesktopVars(res.Env.DesktopNames, res.Env.SessionDesktop)
sess, err := session.Spawn(&res.Env, creds.UID, creds.PrimaryGID, creds.AllGIDs, &res.Config)
if err != nil {
return fmt.Errorf("spawn: %w", err)
}
utmpx := auth.AddUtmpx(creds.Username, res.Config.TTY, uint32(sess.PID()))
defer utmpx.Close()
err = sess.Wait()
if err != nil {
log.Printf("session exited: %v", err)
}
log.Printf("session pid %d ended", sess.PID())
return nil
}
func setupLog(path string) {
f, err := os.OpenFile(path, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0644)
if err != nil {
fmt.Fprintf(os.Stderr, "log %s: %v\n", path, err)
os.Exit(1)
}
log.SetOutput(io.MultiWriter(f, os.Stderr))
log.SetFlags(log.LstdFlags)
}
// ── chvt ────────────────────────────────────────────────────────────────
const vtA = 0x5606
const vtW = 0x5607
const kbt = 0x4B33
const kb1 = 0x02
const kb2 = 0x01
func isCon(fd uintptr) bool {
var a int32
_, _, e := syscall.Syscall(syscall.SYS_IOCTL, fd, kbt, uintptr(unsafe.Pointer(&a)))
return e == 0 && (a == kb1 || a == kb2)
}
func openCon(fn string) (int, error) {
for _, fl := range []int{os.O_RDWR, os.O_RDONLY, os.O_WRONLY} {
fd, err := syscall.Open(fn, fl, 0)
if err != nil {
if err == syscall.EACCES {
continue
}
break
}
if isCon(uintptr(fd)) {
return fd, nil
}
syscall.Close(fd)
}
return 0, fmt.Errorf("openCon: %s", fn)
}
func getCon() (int, error) {
for _, p := range []string{"/dev/tty", "/dev/tty0", "/dev/vc/0", "/dev/console"} {
if fd, err := openCon(p); err == nil {
return fd, nil
}
}
for fd := 0; fd < 3; fd++ {
if isCon(uintptr(fd)) {
return fd, nil
}
}
return 0, fmt.Errorf("no console")
}
func chvt(n int) error {
fd, err := getCon()
if err != nil {
return err
}
defer syscall.Close(fd)
if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), vtA, uintptr(n)); e != 0 {
return fmt.Errorf("VT_ACTIVATE")
}
if _, _, e := syscall.Syscall(syscall.SYS_IOCTL, uintptr(fd), vtW, uintptr(n)); e != 0 {
return fmt.Errorf("VT_WAITACTIVE")
}
return nil
}
func init() {
ch := make(chan os.Signal, 1)
signal.Notify(ch, syscall.SIGTERM, syscall.SIGINT)
go func() {
<-ch
os.Exit(0)
}()
}