diff --git a/main.go b/main.go index aa07280..ed5566e 100644 --- a/main.go +++ b/main.go @@ -56,8 +56,12 @@ func main() { 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 { @@ -87,9 +91,10 @@ func main() { } 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) + } else { + registerRevertPath(runtime) } if verbose { fmt.Printf("Runtime data dir '%s' configured\n", runtime) @@ -97,15 +102,14 @@ func main() { } // 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) + } else { + registerRevertPath(runDir) } - // 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 { @@ -115,9 +119,11 @@ func main() { } 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 { + 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) @@ -132,13 +138,15 @@ func main() { } 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) } + 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`) @@ -157,9 +165,10 @@ func main() { } 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) + } else { + registerRevertPath(pulse) } // Ensure permissions of PulseAudio socket `/run/user/%d/pulse/native` @@ -184,9 +193,10 @@ func main() { 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) + } else { + registerRevertPath(pulseCookieFinal) } if verbose { @@ -249,7 +259,10 @@ func main() { fatal("Error starting process:", err) } - // Cleanup: need register + 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 { @@ -259,11 +272,10 @@ func main() { } } - // Cleanup: deregister, call revert - if verbose { fmt.Println("Process exited with exit code", r) } + beforeExit() os.Exit(r) } @@ -343,9 +355,3 @@ func launchByMachineCtl(bare bool) (args []string) { return } - -func fatal(msg ...any) { - // Cleanup: call revert - fmt.Println(msg...) - os.Exit(1) -} diff --git a/state.go b/state.go new file mode 100644 index 0000000..2204708 --- /dev/null +++ b/state.go @@ -0,0 +1,162 @@ +package main + +import ( + "encoding/gob" + "errors" + "flag" + "fmt" + "io/fs" + "os" + "os/exec" + "path" + "strconv" +) + +// we unfortunately have to assume there are never races between processes +// this and launcher should eventually be replaced by a server process + +var ( + stateActionEarly bool + statePath string + cleanupCandidate []string + xcbActionComplete bool +) + +type launcherState struct { + PID int + Launcher string + Argv []string + Command []string +} + +func init() { + flag.BoolVar(&stateActionEarly, "state", false, "query state value of current active launchers") +} + +func tryState() { + if !stateActionEarly { + return + } + + launchers, err := readLaunchers() + if err != nil { + fmt.Println("Error reading launchers:", err) + os.Exit(1) + } + + fmt.Println("\tPID\tLauncher") + for _, state := range launchers { + fmt.Printf("\t%d\t%s\nCommand: %s\nArgv: %s\n", state.PID, state.Launcher, state.Command, state.Argv) + } + + os.Exit(0) +} + +func registerRevertPath(p string) { + cleanupCandidate = append(cleanupCandidate, p) +} + +// called after process start, before wait +func registerProcess(uid string, cmd *exec.Cmd) error { + statePath = path.Join(runDir, uid, strconv.Itoa(cmd.Process.Pid)) + state := launcherState{ + PID: cmd.Process.Pid, + Launcher: cmd.Path, + Argv: cmd.Args, + Command: command, + } + + if err := os.Mkdir(path.Join(runDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) { + return err + } + + if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil { + return err + } else { + defer func() { + if f.Close() != nil { + // unreachable + panic("state file closed prematurely") + } + }() + return gob.NewEncoder(f).Encode(state) + } +} + +func readLaunchers() ([]*launcherState, error) { + var f *os.File + var r []*launcherState + launcherPrefix := path.Join(runDir, ego.Uid) + + if pl, err := os.ReadDir(launcherPrefix); err != nil { + return nil, err + } else { + for _, e := range pl { + if err = func() error { + if f, err = os.Open(path.Join(launcherPrefix, e.Name())); err != nil { + return err + } else { + defer func() { + if f.Close() != nil { + // unreachable + panic("foreign state file closed prematurely") + } + }() + + var s launcherState + r = append(r, &s) + return gob.NewDecoder(f).Decode(&s) + } + }(); err != nil { + return nil, err + } + } + } + + return r, nil +} + +func beforeExit() { + if err := os.Remove(statePath); err != nil && !errors.Is(err, fs.ErrNotExist) { + fmt.Println("Error removing state file:", err) + } + + if a, err := readLaunchers(); err != nil { + fmt.Println("Error reading active launchers:", err) + os.Exit(1) + } else if len(a) > 0 { + // other launchers are still active + if verbose { + fmt.Printf("Found %d active launchers, exiting without cleaning up\n", len(a)) + } + return + } + + if verbose { + fmt.Println("No other launchers active, will clean up") + } + + if xcbActionComplete { + if verbose { + fmt.Printf("X11: Removing XHost entry SI:localuser:%s\n", ego.Username) + } + if err := changeHosts(xcbHostModeDelete, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil { + fmt.Println("Error removing XHost entry:", err) + } + } + + for _, candidate := range cleanupCandidate { + if err := aclUpdatePerm(candidate, uid); err != nil { + fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err) + } + if verbose { + fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", ego.Username, candidate) + } + } +} + +func fatal(msg ...any) { + fmt.Println(msg...) + beforeExit() + os.Exit(1) +}