Files
latchd/legacy/go/main.go

258 lines
5.9 KiB
Go

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)
}
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)
}()
}