aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra Umiker <cat@ophivana.moe>2024-07-16 14:19:43 +0900
committerOphestra Umiker <cat@ophivana.moe>2024-07-16 14:19:43 +0900
commita3c2916c1a16d9f49b76b2baae4e80c85559070e (patch)
treea82119a6d3eb49a02532c6481ca77ec82723a250
parent392717c6dcfbd19a03ec9285d1071f1c46b89ab5 (diff)
state: track launcher states in runDir and clean up before exit
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>
-rw-r--r--main.go48
-rw-r--r--state.go162
2 files changed, 189 insertions, 21 deletions
diff --git a/main.go b/main.go
index aa07280e..ed5566eb 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 00000000..22047085
--- /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)
+}