aboutsummaryrefslogtreecommitdiffhomepage
path: root/main.go
diff options
context:
space:
mode:
authorOphestra Umiker <cat@ophivana.moe>2024-07-15 23:29:21 +0900
committerOphestra Umiker <cat@ophivana.moe>2024-07-15 23:29:21 +0900
commitda7e404bcffe49240554888b1ca665030073799b (patch)
treeca9f296b59e6632f4ebf0fe6e4069744f8d90fef /main.go
parent1cd0846dc92fdcf43ea25bf0013e2681d0b7903d (diff)
main: implement sudo and machinectl launcher methods
This does almost exactly what github:intgr/ego does, with some minor optimisations and corrections. Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
Diffstat (limited to 'main.go')
-rw-r--r--main.go339
1 files changed, 339 insertions, 0 deletions
diff --git a/main.go b/main.go
index 159d6d77..aa07280e 100644
--- a/main.go
+++ b/main.go
@@ -1,12 +1,351 @@
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
+ }
+
+ // 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 {
+ // Cleanup: need revert
+ if err = aclUpdatePerm(runtime, uid, aclExecute); err != nil {
+ fatal("Error preparing runtime dir:", err)
+ }
+ 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
+ runDir = path.Join(runtime, "ego")
+ if err := os.Mkdir(runDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
+ fatal("Error creating Ego runtime dir:", err)
+ }
+ // Cleanup: need revert
+ if err := aclUpdatePerm(runDir, uid, aclExecute); err != nil {
+ fatal("Error preparing Ego runtime dir:", err)
+ }
+ // Cleanup: need register control PID
+
+ // 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))
+ // Cleanup: need revert
+ if err := aclUpdatePerm(path.Join(runtime, w), uid, aclRead, aclWrite, aclExecute); err != nil {
+ fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
+ }
+ 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)
+ // Cleanup: need revert
+ if err := changeHosts(xcbHostModeInsert, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
+ fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
+ }
+ if verbose {
+ fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", ego.Username, d)
+ }
+ }
+
+ // 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)
+ // Cleanup: need revert
+ if err = aclUpdatePerm(pulse, uid, aclExecute); err != nil {
+ fatal("Error preparing PulseAudio:", err)
+ }
+
+ // 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)
+ }
+ // Cleanup: need revert
+ if err = aclUpdatePerm(pulseCookieFinal, uid, aclRead); err != nil {
+ fatal("Error publishing PulseAudio cookie:", err)
+ }
+
+ 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)
+ }
+
+ // Cleanup: need register
+
+ var r int
+ if err := cmd.Wait(); err != nil {
+ var exitError *exec.ExitError
+ if !errors.As(err, &exitError) {
+ fatal("Error running process:", err)
+ }
+ }
+
+ // Cleanup: deregister, call revert
+
+ if verbose {
+ fmt.Println("Process exited with exit code", r)
+ }
+ 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
+}
+
+func fatal(msg ...any) {
+ // Cleanup: call revert
+ fmt.Println(msg...)
+ os.Exit(1)
}