258 lines
5.9 KiB
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)
|
|
}()
|
|
}
|