aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app
diff options
context:
space:
mode:
Diffstat (limited to 'internal/app')
-rw-r--r--internal/app/dbus.go14
-rw-r--r--internal/app/launch.go83
-rw-r--r--internal/app/pulse.go68
-rw-r--r--internal/app/run.go178
-rw-r--r--internal/app/setup.go77
-rw-r--r--internal/app/wayland.go39
-rw-r--r--internal/app/x.go35
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()
+ }
+ }
+}