diff --git a/internal/app/dbus.go b/internal/app/dbus.go index 60cf3f5..06523e8 100644 --- a/internal/app/dbus.go +++ b/internal/app/dbus.go @@ -3,6 +3,7 @@ package app import ( "errors" "fmt" + "git.ophivana.moe/cat/fortify/internal/final" "os" "path" "strconv" @@ -10,7 +11,6 @@ import ( "git.ophivana.moe/cat/fortify/dbus" "git.ophivana.moe/cat/fortify/internal/acl" "git.ophivana.moe/cat/fortify/internal/state" - "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/util" "git.ophivana.moe/cat/fortify/internal/verbose" ) @@ -32,7 +32,7 @@ func (a *App) ShareDBus(dse, dsg *dbus.Config, log bool) { var binPath string var sessionBus, systemBus [2]string - target := path.Join(system.V.Share, strconv.Itoa(os.Getpid())) + target := path.Join(a.sharePath, strconv.Itoa(os.Getpid())) sessionBus[1] = target + ".bus" systemBus[1] = target + ".system-bus" dbusAddress = [2]string{ @@ -41,7 +41,7 @@ func (a *App) ShareDBus(dse, dsg *dbus.Config, log bool) { } if b, ok := util.Which("xdg-dbus-proxy"); !ok { - state.Fatal("D-Bus: Did not find 'xdg-dbus-proxy' in PATH") + final.Fatal("D-Bus: Did not find 'xdg-dbus-proxy' in PATH") } else { binPath = b } @@ -69,7 +69,7 @@ func (a *App) ShareDBus(dse, dsg *dbus.Config, log bool) { verbose.Println("D-Bus: sealing system proxy", dsg.Args(systemBus)) } if err := p.Seal(dse, dsg); err != nil { - state.Fatal("D-Bus: invalid config when sealing proxy,", err) + final.Fatal("D-Bus: invalid config when sealing proxy,", err) } ready := make(chan bool, 1) @@ -80,7 +80,7 @@ func (a *App) ShareDBus(dse, dsg *dbus.Config, log bool) { verbose.Printf("Starting system bus proxy '%s' for address '%s'\n", dbusAddress[1], systemBus[0]) } if err := p.Start(&ready); err != nil { - state.Fatal("D-Bus: error starting proxy,", err) + final.Fatal("D-Bus: error starting proxy,", err) } verbose.Println("D-Bus proxy launch:", p) @@ -97,24 +97,24 @@ func (a *App) ShareDBus(dse, dsg *dbus.Config, log bool) { }() // register early to enable Fatal cleanup - state.RegisterDBus(p, &done) + final.RegisterDBus(p, &done) if !<-ready { - state.Fatal("D-Bus: proxy did not start correctly") + final.Fatal("D-Bus: proxy did not start correctly") } a.AppendEnv(dbusSessionBusAddress, dbusAddress[0]) if err := acl.UpdatePerm(sessionBus[1], a.UID(), acl.Read, acl.Write); err != nil { - state.Fatal(fmt.Sprintf("Error preparing D-Bus session proxy '%s':", dbusAddress[0]), err) + final.Fatal(fmt.Sprintf("Error preparing D-Bus session proxy '%s':", dbusAddress[0]), err) } else { - state.RegisterRevertPath(sessionBus[1]) + final.RegisterRevertPath(sessionBus[1]) } if dsg != nil { a.AppendEnv(dbusSystemBusAddress, dbusAddress[1]) if err := acl.UpdatePerm(systemBus[1], a.UID(), acl.Read, acl.Write); err != nil { - state.Fatal(fmt.Sprintf("Error preparing D-Bus system proxy '%s':", dbusAddress[1]), err) + final.Fatal(fmt.Sprintf("Error preparing D-Bus system proxy '%s':", dbusAddress[1]), err) } else { - state.RegisterRevertPath(systemBus[1]) + final.RegisterRevertPath(systemBus[1]) } } verbose.Printf("Session bus proxy '%s' for address '%s' configured\n", dbusAddress[0], sessionBus[0]) diff --git a/internal/app/ensure.go b/internal/app/ensure.go new file mode 100644 index 0000000..9ab63ff --- /dev/null +++ b/internal/app/ensure.go @@ -0,0 +1,62 @@ +package app + +import ( + "errors" + "fmt" + "git.ophivana.moe/cat/fortify/internal/acl" + "git.ophivana.moe/cat/fortify/internal/final" + "git.ophivana.moe/cat/fortify/internal/verbose" + "io/fs" + "os" + "path" +) + +func (a *App) EnsureRunDir() { + if err := os.Mkdir(a.runDirPath, 0700); err != nil && !errors.Is(err, fs.ErrExist) { + final.Fatal("Error creating runtime directory:", err) + } +} + +func (a *App) EnsureRuntime() { + if s, err := os.Stat(a.runtimePath); err != nil { + if errors.Is(err, fs.ErrNotExist) { + final.Fatal("Runtime directory does not exist") + } + final.Fatal("Error accessing runtime directory:", err) + } else if !s.IsDir() { + final.Fatal(fmt.Sprintf("Path '%s' is not a directory", a.runtimePath)) + } else { + if err = acl.UpdatePerm(a.runtimePath, a.UID(), acl.Execute); err != nil { + final.Fatal("Error preparing runtime directory:", err) + } else { + final.RegisterRevertPath(a.runtimePath) + } + verbose.Printf("Runtime data dir '%s' configured\n", a.runtimePath) + } +} + +func (a *App) EnsureShare() { + // acl is unnecessary as this directory is world executable + if err := os.Mkdir(a.sharePath, 0701); err != nil && !errors.Is(err, fs.ErrExist) { + final.Fatal("Error creating shared directory:", err) + } + + // workaround for launch method sudo + if a.LaunchOption() == LaunchMethodSudo { + // ensure child runtime directory (e.g. `/tmp/fortify.%d/%d.share`) + cr := path.Join(a.sharePath, a.Uid+".share") + if err := os.Mkdir(cr, 0700); err != nil && !errors.Is(err, fs.ErrExist) { + final.Fatal("Error creating child runtime directory:", err) + } else { + if err = acl.UpdatePerm(cr, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil { + final.Fatal("Error preparing child runtime directory:", err) + } else { + final.RegisterRevertPath(cr) + } + a.AppendEnv("XDG_RUNTIME_DIR", cr) + a.AppendEnv("XDG_SESSION_CLASS", "user") + a.AppendEnv("XDG_SESSION_TYPE", "tty") + verbose.Printf("Child runtime data dir '%s' configured\n", cr) + } + } +} diff --git a/internal/app/launch.go b/internal/app/launch.go index 02204aa..886e683 100644 --- a/internal/app/launch.go +++ b/internal/app/launch.go @@ -5,11 +5,11 @@ import ( "encoding/base64" "encoding/gob" "fmt" + "git.ophivana.moe/cat/fortify/internal/final" "os" "strings" "syscall" - "git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/util" ) @@ -20,7 +20,7 @@ func (a *App) launcherPayloadEnv() string { enc := base64.NewEncoder(base64.StdEncoding, r) if err := gob.NewEncoder(enc).Encode(a.command); err != nil { - state.Fatal("Error encoding launcher payload:", err) + final.Fatal("Error encoding launcher payload:", err) } _ = enc.Close() diff --git a/internal/app/pulse.go b/internal/app/pulse.go index d7e08ba..4b67ff2 100644 --- a/internal/app/pulse.go +++ b/internal/app/pulse.go @@ -3,13 +3,13 @@ package app import ( "errors" "fmt" + "git.ophivana.moe/cat/fortify/internal/final" "io/fs" "os" "path" "git.ophivana.moe/cat/fortify/internal/acl" "git.ophivana.moe/cat/fortify/internal/state" - "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/util" "git.ophivana.moe/cat/fortify/internal/verbose" ) @@ -18,46 +18,46 @@ func (a *App) SharePulse() { a.setEnablement(state.EnablePulse) // ensure PulseAudio directory ACL (e.g. `/run/user/%d/pulse`) - pulse := path.Join(system.V.Runtime, "pulse") + pulse := path.Join(a.runtimePath, "pulse") pulseS := path.Join(pulse, "native") if s, err := os.Stat(pulse); err != nil { if !errors.Is(err, fs.ErrNotExist) { - state.Fatal("Error accessing PulseAudio directory:", err) + final.Fatal("Error accessing PulseAudio directory:", err) } - state.Fatal(fmt.Sprintf("PulseAudio dir '%s' not found", pulse)) + final.Fatal(fmt.Sprintf("PulseAudio dir '%s' not found", pulse)) } else { // add environment variable for new process a.AppendEnv(util.PulseServer, "unix:"+pulseS) if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil { - state.Fatal("Error preparing PulseAudio:", err) + final.Fatal("Error preparing PulseAudio:", err) } else { - state.RegisterRevertPath(pulse) + final.RegisterRevertPath(pulse) } // ensure PulseAudio socket permission (e.g. `/run/user/%d/pulse/native`) if s, err = os.Stat(pulseS); err != nil { if errors.Is(err, fs.ErrNotExist) { - state.Fatal("PulseAudio directory found but socket does not exist") + final.Fatal("PulseAudio directory found but socket does not exist") } - state.Fatal("Error accessing PulseAudio socket:", err) + final.Fatal("Error accessing PulseAudio socket:", err) } else { if m := s.Mode(); m&0o006 != 0o006 { - state.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m) + final.Fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m) } } // Publish current user's pulse-cookie for target user pulseCookieSource := util.DiscoverPulseCookie() - pulseCookieFinal := path.Join(system.V.Share, "pulse-cookie") + pulseCookieFinal := path.Join(a.sharePath, "pulse-cookie") a.AppendEnv(util.PulseCookie, pulseCookieFinal) verbose.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal) if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil { - state.Fatal("Error copying PulseAudio cookie:", err) + final.Fatal("Error copying PulseAudio cookie:", err) } if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil { - state.Fatal("Error publishing PulseAudio cookie:", err) + final.Fatal("Error publishing PulseAudio cookie:", err) } else { - state.RegisterRevertPath(pulseCookieFinal) + final.RegisterRevertPath(pulseCookieFinal) } verbose.Printf("PulseAudio dir '%s' configured\n", pulse) diff --git a/internal/app/run.go b/internal/app/run.go index 53548d7..d4a9e13 100644 --- a/internal/app/run.go +++ b/internal/app/run.go @@ -3,12 +3,12 @@ package app import ( "errors" "fmt" + "git.ophivana.moe/cat/fortify/internal/final" "os" "os/exec" "strings" "git.ophivana.moe/cat/fortify/internal/state" - "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/util" "git.ophivana.moe/cat/fortify/internal/verbose" ) @@ -47,31 +47,33 @@ func (a *App) Run() { cmd.Stdin = os.Stdin cmd.Stdout = os.Stdout cmd.Stderr = os.Stderr - cmd.Dir = system.V.RunDir + cmd.Dir = a.runDirPath verbose.Println("Executing:", cmd) if err := cmd.Start(); err != nil { - state.Fatal("Error starting process:", err) + final.Fatal("Error starting process:", err) } - state.RegisterEnablement(a.enablements) + final.RegisterEnablement(a.enablements) - if err := state.SaveProcess(a.Uid, cmd); err != nil { + if statePath, err := state.SaveProcess(a.Uid, cmd, a.runDirPath, a.command, a.enablements); err != nil { // process already started, shouldn't be fatal fmt.Println("Error registering process:", err) + } else { + final.RegisterStatePath(statePath) } var r int if err := cmd.Wait(); err != nil { var exitError *exec.ExitError if !errors.As(err, &exitError) { - state.Fatal("Error running process:", err) + final.Fatal("Error running process:", err) } } verbose.Println("Process exited with exit code", r) - state.BeforeExit() + final.BeforeExit() os.Exit(r) } @@ -99,7 +101,7 @@ func (a *App) commandBuilderSudo() (args []string) { func (a *App) commandBuilderBwrap() (args []string) { // TODO: build bwrap command - state.Fatal("bwrap") + final.Fatal("bwrap") panic("unreachable") } @@ -127,7 +129,7 @@ func (a *App) commandBuilderMachineCtl() (args []string) { // /bin/sh -c if sh, ok := util.Which("sh"); !ok { - state.Fatal("Did not find 'sh' in PATH") + final.Fatal("Did not find 'sh' in PATH") } else { args = append(args, sh, "-c") } @@ -145,7 +147,7 @@ func (a *App) commandBuilderMachineCtl() (args []string) { innerCommand.WriteString("; ") if executable, err := os.Executable(); err != nil { - state.Fatal("Error reading executable path:", err) + final.Fatal("Error reading executable path:", err) } else { if a.enablements.Has(state.EnableDBus) { innerCommand.WriteString(dbusSessionBusAddress + "=" + "'" + dbusAddress[0] + "' ") diff --git a/internal/app/setup.go b/internal/app/setup.go index c6b4461..8d6eb88 100644 --- a/internal/app/setup.go +++ b/internal/app/setup.go @@ -5,6 +5,7 @@ import ( "fmt" "os" "os/user" + "path" "strconv" "git.ophivana.moe/cat/fortify/internal/state" @@ -12,18 +13,25 @@ import ( "git.ophivana.moe/cat/fortify/internal/verbose" ) +const ( + xdgRuntimeDir = "XDG_RUNTIME_DIR" +) + type App struct { - launchOptionText string + uid int // assigned + env []string // modified via AppendEnv + command []string // set on initialisation - uid int - env []string - command []string + launchOptionText string // set on initialisation + launchOption uint8 // assigned - launchOption uint8 - toolPath string + sharePath string // set on initialisation + runtimePath string // assigned + runDirPath string // assigned + toolPath string // assigned - enablements state.Enablements - *user.User + enablements state.Enablements // set via setEnablement + *user.User // assigned // absolutely *no* method of this type is thread-safe // so don't treat it as if it is @@ -33,6 +41,10 @@ func (a *App) LaunchOption() uint8 { return a.launchOption } +func (a *App) RunDir() string { + return a.runDirPath +} + func (a *App) setEnablement(e state.Enablement) { if a.enablements.Has(e) { panic("enablement " + e.String() + " set twice") @@ -42,8 +54,25 @@ func (a *App) setEnablement(e state.Enablement) { } func New(userName string, args []string, launchOptionText string) *App { - a := &App{command: args, launchOptionText: launchOptionText} + a := &App{ + command: args, + launchOptionText: launchOptionText, + sharePath: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())), + } + // runtimePath, runDirPath + if r, ok := os.LookupEnv(xdgRuntimeDir); !ok { + fmt.Println("Env variable", xdgRuntimeDir, "unset") + + // too early for fatal + os.Exit(1) + } else { + a.runtimePath = r + a.runDirPath = path.Join(a.runtimePath, "fortify") + verbose.Println("Runtime directory at", a.runDirPath) + } + + // *user.User if u, err := user.Lookup(userName); err != nil { if errors.As(err, new(user.UnknownUserError)) { fmt.Println("unknown user", userName) @@ -58,6 +87,7 @@ func New(userName string, args []string, launchOptionText string) *App { a.User = u } + // uid if u, err := strconv.Atoi(a.Uid); err != nil { // usually unreachable panic("uid parse") @@ -70,6 +100,7 @@ func New(userName string, args []string, launchOptionText string) *App { verbose.Println("System booted with systemd as init system (PID 1).") } + // launchOption, toolPath switch a.launchOptionText { case "sudo": a.launchOption = LaunchMethodSudo diff --git a/internal/app/wayland.go b/internal/app/wayland.go index 1256871..9834fea 100644 --- a/internal/app/wayland.go +++ b/internal/app/wayland.go @@ -2,12 +2,12 @@ package app import ( "fmt" + "git.ophivana.moe/cat/fortify/internal/final" "os" "path" "git.ophivana.moe/cat/fortify/internal/acl" "git.ophivana.moe/cat/fortify/internal/state" - "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/verbose" ) @@ -21,15 +21,15 @@ func (a *App) ShareWayland() { // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) if w, ok := os.LookupEnv(waylandDisplay); !ok { - state.Fatal("Wayland: WAYLAND_DISPLAY not set") + final.Fatal("Wayland: WAYLAND_DISPLAY not set") } else { // add environment variable for new process - wp := path.Join(system.V.Runtime, w) + wp := path.Join(a.runtimePath, w) a.AppendEnv(waylandDisplay, wp) if err := acl.UpdatePerm(wp, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil { - state.Fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err) + final.Fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err) } else { - state.RegisterRevertPath(wp) + final.RegisterRevertPath(wp) } verbose.Printf("Wayland socket '%s' configured\n", w) } diff --git a/internal/app/x.go b/internal/app/x.go index 20c0de1..0703402 100644 --- a/internal/app/x.go +++ b/internal/app/x.go @@ -2,6 +2,7 @@ package app import ( "fmt" + "git.ophivana.moe/cat/fortify/internal/final" "os" "git.ophivana.moe/cat/fortify/internal/state" @@ -16,16 +17,16 @@ func (a *App) ShareX() { // discovery X11 and grant user permission via the `ChangeHosts` command if d, ok := os.LookupEnv(display); !ok { - state.Fatal("X11: DISPLAY not set") + final.Fatal("X11: DISPLAY not set") } else { // add environment variable for new process a.AppendEnv(display, d) verbose.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d) if err := xcb.ChangeHosts(xcb.HostModeInsert, xcb.FamilyServerInterpreted, "localuser\x00"+a.Username); err != nil { - state.Fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err) + final.Fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err) } else { - state.XcbActionComplete() + final.XcbActionComplete() } } } diff --git a/internal/state/exit.go b/internal/final/exit.go similarity index 93% rename from internal/state/exit.go rename to internal/final/exit.go index 0eb80a8..86cc2e7 100644 --- a/internal/state/exit.go +++ b/internal/final/exit.go @@ -1,14 +1,14 @@ -package state +package final import ( "errors" "fmt" - "io/fs" - "os" - "git.ophivana.moe/cat/fortify/internal/acl" + "git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/verbose" "git.ophivana.moe/cat/fortify/internal/xcb" + "io/fs" + "os" ) func Fatal(msg ...any) { @@ -31,7 +31,7 @@ func BeforeExit() { } } - if d, err := readLaunchers(u.Uid); err != nil { + if d, err := state.ReadLaunchers(runDirPath, u.Uid); err != nil { fmt.Println("Error reading active launchers:", err) os.Exit(1) } else if len(d) > 0 { diff --git a/internal/final/prepare.go b/internal/final/prepare.go new file mode 100644 index 0000000..859013c --- /dev/null +++ b/internal/final/prepare.go @@ -0,0 +1,20 @@ +package final + +import "os/user" + +var ( + u *user.User + uid int + + runDirPath string +) + +func Prepare(val user.User, d int, s string) { + if u != nil { + panic("final prepared twice") + } + + u = &val + uid = d + runDirPath = s +} diff --git a/internal/state/register.go b/internal/final/register.go similarity index 60% rename from internal/state/register.go rename to internal/final/register.go index be9ce54..16e8d86 100644 --- a/internal/state/register.go +++ b/internal/final/register.go @@ -1,21 +1,26 @@ -package state +package final -import "git.ophivana.moe/cat/fortify/dbus" +import ( + "git.ophivana.moe/cat/fortify/dbus" + "git.ophivana.moe/cat/fortify/internal/state" +) var ( cleanupCandidate []string - enablements *Enablements + enablements *state.Enablements xcbActionComplete bool dbusProxy *dbus.Proxy dbusDone *chan struct{} + + statePath string ) func RegisterRevertPath(p string) { cleanupCandidate = append(cleanupCandidate, p) } -func RegisterEnablement(e Enablements) { +func RegisterEnablement(e state.Enablements) { if enablements != nil { panic("enablement state set twice") } @@ -33,3 +38,11 @@ func RegisterDBus(p *dbus.Proxy, done *chan struct{}) { dbusProxy = p dbusDone = done } + +func RegisterStatePath(v string) { + if statePath != "" { + panic("statePath set twice") + } + + statePath = v +} diff --git a/internal/state/data.go b/internal/state/data.go new file mode 100644 index 0000000..65b6395 --- /dev/null +++ b/internal/state/data.go @@ -0,0 +1,51 @@ +package state + +import ( + "encoding/gob" + "os" + "path" +) + +// we unfortunately have to assume there are never races between processes +// this and launcher should eventually be replaced by a server process + +type launcherState struct { + PID int + Launcher string + Argv []string + Command []string + Capability Enablements +} + +func ReadLaunchers(runDirPath, uid string) ([]*launcherState, error) { + var f *os.File + var r []*launcherState + launcherPrefix := path.Join(runDirPath, 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 +} diff --git a/internal/state/print.go b/internal/state/print.go index 0881f19..83912d3 100644 --- a/internal/state/print.go +++ b/internal/state/print.go @@ -1,68 +1,37 @@ package state import ( - "flag" "fmt" "os" "strconv" "strings" "text/tabwriter" - "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/verbose" ) -var ( - stateActionEarly bool - stateActionEarlyC bool -) - -func init() { - flag.BoolVar(&stateActionEarly, "state", false, "print state information of active launchers") - flag.BoolVar(&stateActionEarlyC, "state-current", false, "print state information of active launchers for the specified user") -} - -func Early() { - var w *tabwriter.Writer - - switch { - case stateActionEarly: - if runDir, err := os.ReadDir(system.V.RunDir); err != nil { - fmt.Println("Error reading runtime directory:", err) - } else { - for _, e := range runDir { - if !e.IsDir() { - verbose.Println("Skipped non-directory entry", e.Name()) - continue - } - - if _, err = strconv.Atoi(e.Name()); err != nil { - verbose.Println("Skipped non-uid entry", e.Name()) - continue - } - - printLauncherState(e.Name(), &w) - } - } - case stateActionEarlyC: - printLauncherState(u.Uid, &w) - default: - return - } - - if w != nil { - if err := w.Flush(); err != nil { - fmt.Println("warn: error formatting output:", err) - } +func MustPrintLauncherStateGlobal(w **tabwriter.Writer, runDirPath string) { + if dirs, err := os.ReadDir(runDirPath); err != nil { + fmt.Println("Error reading runtime directory:", err) } else { - fmt.Println("No information available.") - } + for _, e := range dirs { + if !e.IsDir() { + verbose.Println("Skipped non-directory entry", e.Name()) + continue + } - os.Exit(0) + if _, err = strconv.Atoi(e.Name()); err != nil { + verbose.Println("Skipped non-uid entry", e.Name()) + continue + } + + MustPrintLauncherState(w, runDirPath, e.Name()) + } + } } -func printLauncherState(uid string, w **tabwriter.Writer) { - launchers, err := readLaunchers(uid) +func MustPrintLauncherState(w **tabwriter.Writer, runDirPath, uid string) { + launchers, err := ReadLaunchers(runDirPath, uid) if err != nil { fmt.Println("Error reading launchers:", err) os.Exit(1) diff --git a/internal/state/track.go b/internal/state/track.go index 198abeb..308be28 100644 --- a/internal/state/track.go +++ b/internal/state/track.go @@ -8,42 +8,25 @@ import ( "os/exec" "path" "strconv" - - "git.ophivana.moe/cat/fortify/internal/system" ) -// we unfortunately have to assume there are never races between processes -// this and launcher should eventually be replaced by a server process - -var ( - statePath string -) - -type launcherState struct { - PID int - Launcher string - Argv []string - Command []string - Capability Enablements -} - // SaveProcess called after process start, before wait -func SaveProcess(uid string, cmd *exec.Cmd) error { - statePath = path.Join(system.V.RunDir, uid, strconv.Itoa(cmd.Process.Pid)) +func SaveProcess(uid string, cmd *exec.Cmd, runDirPath string, command []string, enablements Enablements) (string, error) { + statePath := path.Join(runDirPath, uid, strconv.Itoa(cmd.Process.Pid)) state := launcherState{ PID: cmd.Process.Pid, Launcher: cmd.Path, Argv: cmd.Args, Command: command, - Capability: *enablements, + Capability: enablements, } - if err := os.Mkdir(path.Join(system.V.RunDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) { - return err + if err := os.Mkdir(path.Join(runDirPath, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) { + return statePath, err } if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil { - return err + return statePath, err } else { defer func() { if f.Close() != nil { @@ -51,39 +34,6 @@ func SaveProcess(uid string, cmd *exec.Cmd) error { panic("state file closed prematurely") } }() - return gob.NewEncoder(f).Encode(state) + return statePath, gob.NewEncoder(f).Encode(state) } } - -func readLaunchers(uid string) ([]*launcherState, error) { - var f *os.File - var r []*launcherState - launcherPrefix := path.Join(system.V.RunDir, 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 -} diff --git a/internal/state/value.go b/internal/state/value.go deleted file mode 100644 index cb20818..0000000 --- a/internal/state/value.go +++ /dev/null @@ -1,21 +0,0 @@ -package state - -import ( - "os/user" -) - -var ( - u *user.User - uid int - command []string -) - -func Set(val user.User, c []string, d int) { - if u != nil { - panic("state set twice") - } - - u = &val - command = c - uid = d -} diff --git a/internal/system/retrieve.go b/internal/system/retrieve.go deleted file mode 100644 index 0679842..0000000 --- a/internal/system/retrieve.go +++ /dev/null @@ -1,28 +0,0 @@ -package system - -import ( - "fmt" - "os" - "path" - "strconv" -) - -func Retrieve() { - if V != nil { - panic("system info retrieved twice") - } - - v := &Values{Share: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid()))} - - if r, ok := os.LookupEnv(xdgRuntimeDir); !ok { - fmt.Println("Env variable", xdgRuntimeDir, "unset") - - // too early for fatal - os.Exit(1) - } else { - v.Runtime = r - v.RunDir = path.Join(v.Runtime, "fortify") - } - - V = v -} diff --git a/internal/system/value.go b/internal/system/value.go deleted file mode 100644 index c34e7fa..0000000 --- a/internal/system/value.go +++ /dev/null @@ -1,13 +0,0 @@ -package system - -const ( - xdgRuntimeDir = "XDG_RUNTIME_DIR" -) - -type Values struct { - Share string - Runtime string - RunDir string -} - -var V *Values diff --git a/internal/util/std.go b/internal/util/std.go index 4f304fb..9709cc2 100644 --- a/internal/util/std.go +++ b/internal/util/std.go @@ -3,11 +3,10 @@ package util import ( "errors" "fmt" + "git.ophivana.moe/cat/fortify/internal/final" "io/fs" "os" "path" - - "git.ophivana.moe/cat/fortify/internal/state" ) const ( @@ -43,7 +42,7 @@ func DiscoverPulseCookie() string { p = path.Join(p, ".pulse-cookie") if s, err := os.Stat(p); err != nil { if !errors.Is(err, fs.ErrNotExist) { - state.Fatal("Error accessing PulseAudio cookie:", err) + final.Fatal("Error accessing PulseAudio cookie:", err) // unreachable return p } @@ -56,7 +55,7 @@ func DiscoverPulseCookie() string { p = path.Join(p, "pulse", "cookie") if s, err := os.Stat(p); err != nil { if !errors.Is(err, fs.ErrNotExist) { - state.Fatal("Error accessing PulseAudio cookie:", err) + final.Fatal("Error accessing PulseAudio cookie:", err) // unreachable return p } @@ -65,7 +64,7 @@ func DiscoverPulseCookie() string { } } - state.Fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", + final.Fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)", PulseCookie, xdgConfigHome, home)) return "" } diff --git a/main.go b/main.go index ebd5970..f1bf07a 100644 --- a/main.go +++ b/main.go @@ -7,15 +7,13 @@ import ( "fmt" "io/fs" "os" - "path" "strconv" "syscall" + "git.ophivana.moe/cat/fortify/internal/final" + "git.ophivana.moe/cat/fortify/dbus" - "git.ophivana.moe/cat/fortify/internal/acl" "git.ophivana.moe/cat/fortify/internal/app" - "git.ophivana.moe/cat/fortify/internal/state" - "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/verbose" ) @@ -48,9 +46,8 @@ func main() { tryVersion() tryLicense() - system.Retrieve() a = app.New(userName, flag.Args(), launchOptionText) - state.Set(*a.User, a.Command(), a.UID()) + final.Prepare(*a.User, a.UID(), a.RunDir()) // parse D-Bus config file if applicable if mustDBus { @@ -58,10 +55,10 @@ func main() { dbusSession = dbus.NewConfig(dbusID, true, mpris) } else { if f, err := os.Open(dbusConfigSession); err != nil { - state.Fatal("Error opening D-Bus proxy config file:", err) + final.Fatal("Error opening D-Bus proxy config file:", err) } else { if err = json.NewDecoder(f).Decode(&dbusSession); err != nil { - state.Fatal("Error parsing D-Bus proxy config file:", err) + final.Fatal("Error parsing D-Bus proxy config file:", err) } } } @@ -69,46 +66,23 @@ func main() { // system bus proxy is optional if dbusConfigSystem != "nil" { if f, err := os.Open(dbusConfigSystem); err != nil { - state.Fatal("Error opening D-Bus proxy config file:", err) + final.Fatal("Error opening D-Bus proxy config file:", err) } else { if err = json.NewDecoder(f).Decode(&dbusSystem); err != nil { - state.Fatal("Error parsing D-Bus proxy config file:", err) + final.Fatal("Error parsing D-Bus proxy config file:", err) } } } } // ensure RunDir (e.g. `/run/user/%d/fortify`) - if err := os.Mkdir(system.V.RunDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) { - state.Fatal("Error creating runtime directory:", err) - } + a.EnsureRunDir() // state query command early exit - state.Early() + tryState() // ensure Share (e.g. `/tmp/fortify.%d`) - // acl is unnecessary as this directory is world executable - if err := os.Mkdir(system.V.Share, 0701); err != nil && !errors.Is(err, fs.ErrExist) { - state.Fatal("Error creating shared directory:", err) - } - - if a.LaunchOption() == app.LaunchMethodSudo { - // ensure child runtime directory (e.g. `/tmp/fortify.%d/%d.share`) - cr := path.Join(system.V.Share, a.Uid+".share") - if err := os.Mkdir(cr, 0700); err != nil && !errors.Is(err, fs.ErrExist) { - state.Fatal("Error creating child runtime directory:", err) - } else { - if err = acl.UpdatePerm(cr, a.UID(), acl.Read, acl.Write, acl.Execute); err != nil { - state.Fatal("Error preparing child runtime directory:", err) - } else { - state.RegisterRevertPath(cr) - } - a.AppendEnv("XDG_RUNTIME_DIR", cr) - a.AppendEnv("XDG_SESSION_CLASS", "user") - a.AppendEnv("XDG_SESSION_TYPE", "tty") - verbose.Printf("Child runtime data dir '%s' configured\n", cr) - } - } + a.EnsureShare() // warn about target user home directory ownership if stat, err := os.Stat(a.HomeDir); err != nil { @@ -131,21 +105,7 @@ func main() { } // ensure runtime directory ACL (e.g. `/run/user/%d`) - if s, err := os.Stat(system.V.Runtime); err != nil { - if errors.Is(err, fs.ErrNotExist) { - state.Fatal("Runtime directory does not exist") - } - state.Fatal("Error accessing runtime directory:", err) - } else if !s.IsDir() { - state.Fatal(fmt.Sprintf("Path '%s' is not a directory", system.V.Runtime)) - } else { - if err = acl.UpdatePerm(system.V.Runtime, a.UID(), acl.Execute); err != nil { - state.Fatal("Error preparing runtime directory:", err) - } else { - state.RegisterRevertPath(system.V.Runtime) - } - verbose.Printf("Runtime data dir '%s' configured\n", system.V.Runtime) - } + a.EnsureRuntime() if mustWayland { a.ShareWayland() diff --git a/state.go b/state.go new file mode 100644 index 0000000..2e68ddc --- /dev/null +++ b/state.go @@ -0,0 +1,42 @@ +package main + +import ( + "flag" + "fmt" + "git.ophivana.moe/cat/fortify/internal/state" + "os" + "text/tabwriter" +) + +var ( + stateActionEarly [2]bool +) + +func init() { + flag.BoolVar(&stateActionEarly[0], "state", false, "print state information of active launchers") + flag.BoolVar(&stateActionEarly[1], "state-current", false, "print state information of active launchers for the specified user") +} + +// tryState is called after app initialisation +func tryState() { + var w *tabwriter.Writer + + switch { + case stateActionEarly[0]: + state.MustPrintLauncherStateGlobal(&w, a.RunDir()) + case stateActionEarly[1]: + state.MustPrintLauncherState(&w, a.RunDir(), a.Uid) + default: + return + } + + if w != nil { + if err := w.Flush(); err != nil { + fmt.Println("warn: error formatting output:", err) + } + } else { + fmt.Println("No information available") + } + + os.Exit(0) +}