diff options
Diffstat (limited to 'internal/app')
| -rw-r--r-- | internal/app/dbus.go | 14 | ||||
| -rw-r--r-- | internal/app/launch.go | 83 | ||||
| -rw-r--r-- | internal/app/pulse.go | 68 | ||||
| -rw-r--r-- | internal/app/run.go | 178 | ||||
| -rw-r--r-- | internal/app/setup.go | 77 | ||||
| -rw-r--r-- | internal/app/wayland.go | 39 | ||||
| -rw-r--r-- | internal/app/x.go | 35 |
7 files changed, 343 insertions, 151 deletions
diff --git a/internal/app/dbus.go b/internal/app/dbus.go new file mode 100644 index 00000000..89394f15 --- /dev/null +++ b/internal/app/dbus.go @@ -0,0 +1,14 @@ +package app + +import ( + "fmt" + + "git.ophivana.moe/cat/fortify/internal/state" +) + +func (a *App) ShareDBus() { + a.setEnablement(state.EnableDBus) + + // TODO: start xdg-dbus-proxy + fmt.Println("warn: dbus proxy not implemented") +} diff --git a/internal/app/launch.go b/internal/app/launch.go index ab48baad..02204aae 100644 --- a/internal/app/launch.go +++ b/internal/app/launch.go @@ -10,14 +10,10 @@ import ( "syscall" "git.ophivana.moe/cat/fortify/internal/state" - "git.ophivana.moe/cat/fortify/internal/system" "git.ophivana.moe/cat/fortify/internal/util" ) -const ( - sudoAskPass = "SUDO_ASKPASS" - launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD" -) +const launcherPayload = "FORTIFY_LAUNCHER_PAYLOAD" func (a *App) launcherPayloadEnv() string { r := &bytes.Buffer{} @@ -75,80 +71,3 @@ func Early(printVersion bool) { } } } - -func (a *App) launchBySudo() (args []string) { - args = make([]string, 0, 4+len(a.env)+len(a.command)) - - // -Hiu $USER - args = append(args, "-Hiu", a.Username) - - // -A? - if _, ok := os.LookupEnv(sudoAskPass); ok { - if system.V.Verbose { - fmt.Printf("%s set, adding askpass flag\n", sudoAskPass) - } - args = append(args, "-A") - } - - // environ - args = append(args, a.env...) - - // -- $@ - args = append(args, "--") - args = append(args, a.command...) - - return -} - -func (a *App) launchByMachineCtl(bare bool) (args []string) { - args = make([]string, 0, 9+len(a.env)) - - // shell --uid=$USER - args = append(args, "shell", "--uid="+a.Username) - - // --quiet - if !system.V.Verbose { - args = append(args, "--quiet") - } - - // environ - envQ := make([]string, len(a.env)+1) - for i, e := range a.env { - envQ[i] = "-E" + e - } - envQ[len(a.env)] = "-E" + a.launcherPayloadEnv() - args = append(args, envQ...) - - // -- .host - args = append(args, "--", ".host") - - // /bin/sh -c - if sh, ok := util.Which("sh"); !ok { - state.Fatal("Did not find 'sh' in PATH") - } else { - args = append(args, sh, "-c") - } - - if len(a.command) == 0 { // execute shell if command is not provided - a.command = []string{"$SHELL"} - } - - innerCommand := strings.Builder{} - - if !bare { - innerCommand.WriteString("dbus-update-activation-environment --systemd") - for _, e := range a.env { - innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0]) - } - innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ") - } - - if executable, err := os.Executable(); err != nil { - state.Fatal("Error reading executable path:", err) - } else { - innerCommand.WriteString("exec " + executable + " -V") - } - args = append(args, innerCommand.String()) - - return -} diff --git a/internal/app/pulse.go b/internal/app/pulse.go new file mode 100644 index 00000000..848ac717 --- /dev/null +++ b/internal/app/pulse.go @@ -0,0 +1,68 @@ +package app + +import ( + "errors" + "fmt" + "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" +) + +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") + 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) + } + state.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) + } else { + state.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") + } + state.Fatal("Error accessing PulseAudio socket:", err) + } else { + if m := s.Mode(); m&0o006 != 0o006 { + state.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") + a.AppendEnv(util.PulseCookie, pulseCookieFinal) + if system.V.Verbose { + fmt.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) + } + if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil { + state.Fatal("Error publishing PulseAudio cookie:", err) + } else { + state.RegisterRevertPath(pulseCookieFinal) + } + + if system.V.Verbose { + fmt.Printf("PulseAudio dir '%s' configured\n", pulse) + } + } +} diff --git a/internal/app/run.go b/internal/app/run.go new file mode 100644 index 00000000..bd3bd9bf --- /dev/null +++ b/internal/app/run.go @@ -0,0 +1,178 @@ +package app + +import ( + "errors" + "fmt" + "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" +) + +const ( + term = "TERM" + sudoAskPass = "SUDO_ASKPASS" +) +const ( + LaunchMethodSudo = iota + LaunchMethodMachineCtl + + LaunchBare + launchOptionLength +) + +var ( + // LaunchOptions is set in main's cli.go + LaunchOptions [launchOptionLength]bool +) + +func (a *App) Run() { + // pass $TERM to launcher + if t, ok := os.LookupEnv(term); ok { + a.AppendEnv(term, t) + } + + commandBuilder := a.commandBuilderSudo + + var toolPath string + + // dependency checks + const sudoFallback = "Falling back to 'sudo', some desktop integration features may not work" + if LaunchOptions[LaunchMethodMachineCtl] && !LaunchOptions[LaunchMethodSudo] { // sudo argument takes priority + if !util.SdBooted() { + fmt.Println("This system was not booted through systemd") + fmt.Println(sudoFallback) + } else if machineCtlPath, ok := util.Which("machinectl"); !ok { + fmt.Println("Did not find 'machinectl' in PATH") + fmt.Println(sudoFallback) + } else { + toolPath = machineCtlPath + commandBuilder = a.commandBuilderMachineCtl + } + } else if sudoPath, ok := util.Which("sudo"); !ok { + state.Fatal("Did not find 'sudo' in PATH") + } else { + toolPath = sudoPath + } + + if system.V.Verbose { + fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, LaunchOptions[LaunchBare]) + } + + cmd := exec.Command(toolPath, commandBuilder(LaunchOptions[LaunchBare])...) + cmd.Env = a.env + cmd.Stdin = os.Stdin + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + cmd.Dir = system.V.RunDir + + if system.V.Verbose { + fmt.Println("Executing:", cmd) + } + + if err := cmd.Start(); err != nil { + state.Fatal("Error starting process:", err) + } + + state.RegisterEnablement(a.enablements) + + if err := state.SaveProcess(a.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) { + state.Fatal("Error running process:", err) + } + } + + if system.V.Verbose { + fmt.Println("Process exited with exit code", r) + } + state.BeforeExit() + os.Exit(r) +} + +func (a *App) commandBuilderSudo(bare bool) (args []string) { + args = make([]string, 0, 4+len(a.env)+len(a.command)) + + // -Hiu $USER + args = append(args, "-Hiu", a.Username) + + // -A? + if _, ok := os.LookupEnv(sudoAskPass); ok { + if system.V.Verbose { + fmt.Printf("%s set, adding askpass flag\n", sudoAskPass) + } + args = append(args, "-A") + } + + // environ + args = append(args, a.env...) + + // -- $@ + args = append(args, "--") + args = append(args, a.command...) + + return +} + +func (a *App) commandBuilderMachineCtl(bare bool) (args []string) { + args = make([]string, 0, 9+len(a.env)) + + // shell --uid=$USER + args = append(args, "shell", "--uid="+a.Username) + + // --quiet + if !system.V.Verbose { + args = append(args, "--quiet") + } + + // environ + envQ := make([]string, len(a.env)+1) + for i, e := range a.env { + envQ[i] = "-E" + e + } + envQ[len(a.env)] = "-E" + a.launcherPayloadEnv() + args = append(args, envQ...) + + // -- .host + args = append(args, "--", ".host") + + // /bin/sh -c + if sh, ok := util.Which("sh"); !ok { + state.Fatal("Did not find 'sh' in PATH") + } else { + args = append(args, sh, "-c") + } + + if len(a.command) == 0 { // execute shell if command is not provided + a.command = []string{"$SHELL"} + } + + innerCommand := strings.Builder{} + + if !bare { + innerCommand.WriteString("dbus-update-activation-environment --systemd") + for _, e := range a.env { + innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0]) + } + innerCommand.WriteString("; ") + //innerCommand.WriteString("systemctl --user start xdg-desktop-portal-gtk; ") + } + + if executable, err := os.Executable(); err != nil { + state.Fatal("Error reading executable path:", err) + } else { + innerCommand.WriteString("exec " + executable + " -V") + } + args = append(args, innerCommand.String()) + + return +} diff --git a/internal/app/setup.go b/internal/app/setup.go index 7b5cd2ec..82765dfb 100644 --- a/internal/app/setup.go +++ b/internal/app/setup.go @@ -4,13 +4,11 @@ import ( "errors" "fmt" "os" - "os/exec" "os/user" "strconv" "git.ophivana.moe/cat/fortify/internal/state" "git.ophivana.moe/cat/fortify/internal/system" - "git.ophivana.moe/cat/fortify/internal/util" ) type App struct { @@ -18,78 +16,19 @@ type App struct { env []string command []string + enablements state.Enablements *user.User -} - -func (a *App) Run() { - f := a.launchBySudo - m, b := false, false - switch { - case system.MethodFlags[0]: // sudo - case system.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 !util.SdBooted() { - fmt.Println("This system was not booted through systemd") - fmt.Println(sudoFallback) - } else if tp, ok := util.Which("machinectl"); !ok { - fmt.Println("Did not find 'machinectl' in PATH") - fmt.Println(sudoFallback) - } else { - toolPath = tp - f = func() []string { return a.launchByMachineCtl(b) } - } - } else if tp, ok := util.Which("sudo"); !ok { - state.Fatal("Did not find 'sudo' in PATH") - } else { - toolPath = tp - } - - if system.V.Verbose { - fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b) - } - cmd := exec.Command(toolPath, f()...) - cmd.Env = a.env - cmd.Stdin = os.Stdin - cmd.Stdout = os.Stdout - cmd.Stderr = os.Stderr - cmd.Dir = system.V.RunDir - - if system.V.Verbose { - fmt.Println("Executing:", cmd) - } - - if err := cmd.Start(); err != nil { - state.Fatal("Error starting process:", err) - } - - if err := state.SaveProcess(a.Uid, cmd); err != nil { - // process already started, shouldn't be fatal - fmt.Println("Error registering process:", err) - } + // absolutely *no* method of this type is thread-safe + // so don't treat it as if it is +} - var r int - if err := cmd.Wait(); err != nil { - var exitError *exec.ExitError - if !errors.As(err, &exitError) { - state.Fatal("Error running process:", err) - } +func (a *App) setEnablement(e state.Enablement) { + if a.enablements.Has(e) { + panic("enablement " + e.String() + " set twice") } - if system.V.Verbose { - fmt.Println("Process exited with exit code", r) - } - state.BeforeExit() - os.Exit(r) + a.enablements |= e.Mask() } func New(userName string, args []string) *App { diff --git a/internal/app/wayland.go b/internal/app/wayland.go new file mode 100644 index 00000000..1cabdbdb --- /dev/null +++ b/internal/app/wayland.go @@ -0,0 +1,39 @@ +package app + +import ( + "fmt" + "os" + "path" + + "git.ophivana.moe/cat/fortify/internal/acl" + "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" +) + +const ( + // https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html + waylandDisplay = "WAYLAND_DISPLAY" +) + +func (a *App) ShareWayland() { + a.setEnablement(state.EnableWayland) + + // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`) + if w, ok := os.LookupEnv(waylandDisplay); !ok { + if system.V.Verbose { + fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping") + } + } else { + // add environment variable for new process + wp := path.Join(system.V.Runtime, 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) + } else { + state.RegisterRevertPath(wp) + } + if system.V.Verbose { + fmt.Printf("Wayland socket '%s' configured\n", w) + } + } +} diff --git a/internal/app/x.go b/internal/app/x.go new file mode 100644 index 00000000..02c40a68 --- /dev/null +++ b/internal/app/x.go @@ -0,0 +1,35 @@ +package app + +import ( + "fmt" + "os" + + "git.ophivana.moe/cat/fortify/internal/state" + "git.ophivana.moe/cat/fortify/internal/system" + "git.ophivana.moe/cat/fortify/internal/xcb" +) + +const display = "DISPLAY" + +func (a *App) ShareX() { + a.setEnablement(state.EnableX) + + // discovery X11 and grant user permission via the `ChangeHosts` command + if d, ok := os.LookupEnv(display); !ok { + if system.V.Verbose { + fmt.Println("X11: DISPLAY not set, skipping") + } + } else { + // add environment variable for new process + a.AppendEnv(display, d) + + if system.V.Verbose { + fmt.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) + } else { + state.XcbActionComplete() + } + } +} |
