Ophestra Umiker
a3c2916c1a
X11 hosts and ACL rules are no longer necessary after all launcher processes exit. This reverts all changes to the system made during setup when no launchers remain. State information is also saved in runDir which can be tracked externally. Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
358 lines
8.9 KiB
Go
358 lines
8.9 KiB
Go
package main
|
|
|
|
import (
|
|
"errors"
|
|
"flag"
|
|
"fmt"
|
|
"io/fs"
|
|
"os"
|
|
"os/exec"
|
|
"os/user"
|
|
"path"
|
|
"strconv"
|
|
"strings"
|
|
"syscall"
|
|
)
|
|
|
|
var Version = "impure"
|
|
|
|
var (
|
|
ego *user.User
|
|
uid int
|
|
env []string
|
|
command []string
|
|
verbose bool
|
|
runtime string
|
|
runDir string
|
|
)
|
|
|
|
const (
|
|
term = "TERM"
|
|
home = "HOME"
|
|
sudoAskPass = "SUDO_ASKPASS"
|
|
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
|
xdgConfigHome = "XDG_CONFIG_HOME"
|
|
display = "DISPLAY"
|
|
pulseServer = "PULSE_SERVER"
|
|
pulseCookie = "PULSE_COOKIE"
|
|
|
|
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
|
waylandDisplay = "WAYLAND_DISPLAY"
|
|
)
|
|
|
|
func main() {
|
|
flag.Parse()
|
|
tryLauncher()
|
|
copyArgs()
|
|
|
|
if u, err := strconv.Atoi(ego.Uid); err != nil {
|
|
// usually unreachable
|
|
panic("ego uid parse")
|
|
} else {
|
|
uid = u
|
|
}
|
|
|
|
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
|
|
fatal("Env variable", xdgRuntimeDir, "unset")
|
|
} else {
|
|
runtime = r
|
|
runDir = path.Join(runtime, "ego")
|
|
}
|
|
|
|
// state query command
|
|
tryState()
|
|
|
|
// Report warning if user home directory does not exist or has wrong ownership
|
|
if stat, err := os.Stat(ego.HomeDir); err != nil {
|
|
if verbose {
|
|
switch {
|
|
case errors.Is(err, fs.ErrPermission):
|
|
fmt.Printf("User %s home directory %s is not accessible", ego.Username, ego.HomeDir)
|
|
case errors.Is(err, fs.ErrNotExist):
|
|
fmt.Printf("User %s home directory %s does not exist", ego.Username, ego.HomeDir)
|
|
default:
|
|
fmt.Printf("Error stat user %s home directory %s: %s", ego.Username, ego.HomeDir, err)
|
|
}
|
|
}
|
|
return
|
|
} else {
|
|
// FreeBSD: not cross-platform
|
|
if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != ego.Uid {
|
|
fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", ego.Username, ego.HomeDir, ego.Uid, u)
|
|
}
|
|
}
|
|
|
|
// Add execute perm to runtime dir, e.g. `/run/user/%d`
|
|
if s, err := os.Stat(runtime); err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
fatal("Runtime directory does not exist")
|
|
}
|
|
fatal("Error accessing runtime directory:", err)
|
|
} else if !s.IsDir() {
|
|
fatal(fmt.Sprintf("Path '%s' is not a directory", runtime))
|
|
} else {
|
|
if err = aclUpdatePerm(runtime, uid, aclExecute); err != nil {
|
|
fatal("Error preparing runtime dir:", err)
|
|
} else {
|
|
registerRevertPath(runtime)
|
|
}
|
|
if verbose {
|
|
fmt.Printf("Runtime data dir '%s' configured\n", runtime)
|
|
}
|
|
}
|
|
|
|
// Create runtime dir for Ego itself (e.g. `/run/user/%d/ego`) and make it readable for target
|
|
if err := os.Mkdir(runDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
fatal("Error creating Ego runtime dir:", err)
|
|
}
|
|
if err := aclUpdatePerm(runDir, uid, aclExecute); err != nil {
|
|
fatal("Error preparing Ego runtime dir:", err)
|
|
} else {
|
|
registerRevertPath(runDir)
|
|
}
|
|
|
|
// Add rwx permissions to Wayland socket (e.g. `/run/user/%d/wayland-0`)
|
|
if w, ok := os.LookupEnv(waylandDisplay); !ok {
|
|
if verbose {
|
|
fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping")
|
|
}
|
|
} else {
|
|
// add environment variable for new process
|
|
env = append(env, waylandDisplay+"="+path.Join(runtime, w))
|
|
wp := path.Join(runtime, w)
|
|
if err := aclUpdatePerm(wp, uid, aclRead, aclWrite, aclExecute); err != nil {
|
|
fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
|
|
} else {
|
|
registerRevertPath(wp)
|
|
}
|
|
if verbose {
|
|
fmt.Printf("Wayland socket '%s' configured\n", w)
|
|
}
|
|
}
|
|
|
|
// Detect `DISPLAY` and grant permissions via X11 protocol `ChangeHosts` command
|
|
if d, ok := os.LookupEnv(display); !ok {
|
|
if verbose {
|
|
fmt.Println("X11: DISPLAY not set, skipping")
|
|
}
|
|
} else {
|
|
// add environment variable for new process
|
|
env = append(env, display+"="+d)
|
|
|
|
if verbose {
|
|
fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", ego.Username, d)
|
|
}
|
|
if err := changeHosts(xcbHostModeInsert, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
|
|
fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
|
|
} else {
|
|
xcbActionComplete = true
|
|
}
|
|
}
|
|
|
|
// Add execute permissions to PulseAudio directory (e.g. `/run/user/%d/pulse`)
|
|
pulse := path.Join(runtime, "pulse")
|
|
pulseS := path.Join(pulse, "native")
|
|
if s, err := os.Stat(pulse); err != nil {
|
|
if !errors.Is(err, fs.ErrNotExist) {
|
|
fatal("Error accessing PulseAudio directory:", err)
|
|
}
|
|
if mustPulse {
|
|
fatal("PulseAudio is unavailable")
|
|
}
|
|
if verbose {
|
|
fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse)
|
|
}
|
|
} else {
|
|
// add environment variable for new process
|
|
env = append(env, pulseServer+"=unix:"+pulseS)
|
|
if err = aclUpdatePerm(pulse, uid, aclExecute); err != nil {
|
|
fatal("Error preparing PulseAudio:", err)
|
|
} else {
|
|
registerRevertPath(pulse)
|
|
}
|
|
|
|
// Ensure permissions of PulseAudio socket `/run/user/%d/pulse/native`
|
|
if s, err = os.Stat(pulseS); err != nil {
|
|
if errors.Is(err, fs.ErrNotExist) {
|
|
fatal("PulseAudio directory found but socket does not exist")
|
|
}
|
|
fatal("Error accessing PulseAudio socket:", err)
|
|
} else {
|
|
if m := s.Mode(); m&0o006 != 0o006 {
|
|
fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
|
|
}
|
|
}
|
|
|
|
// Publish current user's pulse-cookie for target user
|
|
pulseCookieSource := discoverPulseCookie()
|
|
env = append(env, pulseCookie+"="+pulseCookieSource)
|
|
pulseCookieFinal := path.Join(runDir, "pulse-cookie")
|
|
if verbose {
|
|
fmt.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal)
|
|
}
|
|
if err = copyFile(pulseCookieFinal, pulseCookieSource); err != nil {
|
|
fatal("Error copying PulseAudio cookie:", err)
|
|
}
|
|
if err = aclUpdatePerm(pulseCookieFinal, uid, aclRead); err != nil {
|
|
fatal("Error publishing PulseAudio cookie:", err)
|
|
} else {
|
|
registerRevertPath(pulseCookieFinal)
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Printf("PulseAudio dir '%s' configured\n", pulse)
|
|
}
|
|
}
|
|
|
|
// pass $TERM to launcher
|
|
if t, ok := os.LookupEnv(term); ok {
|
|
env = append(env, term+"="+t)
|
|
}
|
|
|
|
f := launchBySudo
|
|
m, b := false, false
|
|
switch {
|
|
case methodFlags[0]: // sudo
|
|
case methodFlags[1]: // bare
|
|
m, b = true, true
|
|
default: // machinectl
|
|
m, b = true, false
|
|
}
|
|
|
|
var toolPath string
|
|
|
|
// dependency checks
|
|
const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work"
|
|
if m {
|
|
if !sdBooted() {
|
|
fmt.Println("This system was not booted through systemd")
|
|
fmt.Println(sudoFallback)
|
|
} else if tp, ok := which("machinectl"); !ok {
|
|
fmt.Println("Did not find 'machinectl' in PATH")
|
|
fmt.Println(sudoFallback)
|
|
} else {
|
|
toolPath = tp
|
|
f = func() []string { return launchByMachineCtl(b) }
|
|
}
|
|
} else if tp, ok := which("sudo"); !ok {
|
|
fatal("Did not find 'sudo' in PATH")
|
|
} else {
|
|
toolPath = tp
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b)
|
|
}
|
|
|
|
cmd := exec.Command(toolPath, f()...)
|
|
cmd.Env = env
|
|
cmd.Stdin = os.Stdin
|
|
cmd.Stdout = os.Stdout
|
|
cmd.Stderr = os.Stderr
|
|
cmd.Dir = runDir
|
|
|
|
if verbose {
|
|
fmt.Println("Executing:", cmd)
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
fatal("Error starting process:", err)
|
|
}
|
|
|
|
if err := registerProcess(ego.Uid, cmd); err != nil {
|
|
// process already started, shouldn't be fatal
|
|
fmt.Println("Error registering process:", err)
|
|
}
|
|
|
|
var r int
|
|
if err := cmd.Wait(); err != nil {
|
|
var exitError *exec.ExitError
|
|
if !errors.As(err, &exitError) {
|
|
fatal("Error running process:", err)
|
|
}
|
|
}
|
|
|
|
if verbose {
|
|
fmt.Println("Process exited with exit code", r)
|
|
}
|
|
beforeExit()
|
|
os.Exit(r)
|
|
}
|
|
|
|
func launchBySudo() (args []string) {
|
|
args = make([]string, 0, 4+len(env)+len(command))
|
|
|
|
// -Hiu $USER
|
|
args = append(args, "-Hiu", ego.Username)
|
|
|
|
// -A?
|
|
if _, ok := os.LookupEnv(sudoAskPass); ok {
|
|
if verbose {
|
|
fmt.Printf("%s set, adding askpass flag\n", sudoAskPass)
|
|
}
|
|
args = append(args, "-A")
|
|
}
|
|
|
|
// environ
|
|
args = append(args, env...)
|
|
|
|
// -- $@
|
|
args = append(args, "--")
|
|
args = append(args, command...)
|
|
|
|
return
|
|
}
|
|
|
|
func launchByMachineCtl(bare bool) (args []string) {
|
|
args = make([]string, 0, 9+len(env))
|
|
|
|
// shell --uid=$USER
|
|
args = append(args, "shell", "--uid="+ego.Username)
|
|
|
|
// --quiet
|
|
if !verbose {
|
|
args = append(args, "--quiet")
|
|
}
|
|
|
|
// environ
|
|
envQ := make([]string, len(env)+1)
|
|
for i, e := range env {
|
|
envQ[i] = "-E" + e
|
|
}
|
|
envQ[len(env)] = "-E" + launcherPayloadEnv()
|
|
args = append(args, envQ...)
|
|
|
|
// -- .host
|
|
args = append(args, "--", ".host")
|
|
|
|
// /bin/sh -c
|
|
if sh, ok := which("sh"); !ok {
|
|
fatal("Did not find 'sh' in PATH")
|
|
} else {
|
|
args = append(args, sh, "-c")
|
|
}
|
|
|
|
if len(command) == 0 { // execute shell if command is not provided
|
|
command = []string{"$SHELL"}
|
|
}
|
|
|
|
innerCommand := strings.Builder{}
|
|
|
|
if !bare {
|
|
innerCommand.WriteString("dbus-update-activation-environment --systemd")
|
|
for _, e := range env {
|
|
innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
|
|
}
|
|
innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ")
|
|
}
|
|
|
|
if executable, err := os.Executable(); err != nil {
|
|
fatal("Error reading executable path:", err)
|
|
} else {
|
|
innerCommand.WriteString("exec " + executable + " -V")
|
|
}
|
|
args = append(args, innerCommand.String())
|
|
|
|
return
|
|
}
|