Files
latchd/legacy/go/session/exec.go

198 lines
4.3 KiB
Go

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"
}