diff --git a/cli.go b/cli.go index e5466f0..4f937f9 100644 --- a/cli.go +++ b/cli.go @@ -3,21 +3,33 @@ package main import ( "flag" - "git.ophivana.moe/cat/fortify/internal/system" + "git.ophivana.moe/cat/fortify/internal/app" ) var ( - userName string - printVersion bool - mustPulse bool + userName string + + mustWayland bool + mustX bool + mustDBus bool + mustPulse bool + flagVerbose bool + printVersion bool ) func init() { flag.StringVar(&userName, "u", "chronos", "Specify a username") - flag.BoolVar(&system.MethodFlags[0], "sudo", false, "Use 'sudo' to change user") - flag.BoolVar(&system.MethodFlags[1], "bare", false, "Use 'machinectl' but skip xdg-desktop-portal setup") - flag.BoolVar(&mustPulse, "pulse", false, "Treat unavailable PulseAudio as fatal") + + flag.BoolVar(&mustWayland, "wayland", false, "Share Wayland socket") + flag.BoolVar(&mustX, "X", false, "Share X11 socket and allow connection") + flag.BoolVar(&mustDBus, "dbus", false, "Proxy D-Bus connection") + flag.BoolVar(&mustPulse, "pulse", false, "Share PulseAudio socket and cookie") + + flag.BoolVar(&app.LaunchOptions[app.LaunchMethodSudo], "sudo", false, "Use 'sudo' to switch user") + flag.BoolVar(&app.LaunchOptions[app.LaunchMethodMachineCtl], "machinectl", true, "Use 'machinectl' to switch user") + flag.BoolVar(&app.LaunchOptions[app.LaunchBare], "bare", false, "Only set environment variables for child") + flag.BoolVar(&flagVerbose, "v", false, "Verbose output") flag.BoolVar(&printVersion, "V", false, "Print version") } diff --git a/internal/app/dbus.go b/internal/app/dbus.go new file mode 100644 index 0000000..89394f1 --- /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 ab48baa..02204aa 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 0000000..848ac71 --- /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 0000000..bd3bd9b --- /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 7b5cd2e..82765df 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 + + // absolutely *no* method of this type is thread-safe + // so don't treat it as if it is } -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 +func (a *App) setEnablement(e state.Enablement) { + if a.enablements.Has(e) { + panic("enablement " + e.String() + " set twice") } - 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) - } - - 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) + 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 0000000..1cabdbd --- /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 0000000..02c40a6 --- /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() + } + } +} diff --git a/internal/state/enablement.go b/internal/state/enablement.go new file mode 100644 index 0000000..9358f43 --- /dev/null +++ b/internal/state/enablement.go @@ -0,0 +1,34 @@ +package state + +type ( + Enablement uint8 + Enablements uint64 +) + +const ( + EnableWayland Enablement = iota + EnableX + EnableDBus + EnablePulse + + enableLength +) + +var enablementString = [enableLength]string{ + "Wayland", + "X11", + "D-Bus", + "PulseAudio", +} + +func (e Enablement) String() string { + return enablementString[e] +} + +func (e Enablement) Mask() Enablements { + return 1 << e +} + +func (es Enablements) Has(e Enablement) bool { + return es&e.Mask() != 0 +} diff --git a/internal/state/exit.go b/internal/state/exit.go index e0e2c04..3da1f48 100644 --- a/internal/state/exit.go +++ b/internal/state/exit.go @@ -33,7 +33,7 @@ func BeforeExit() { } } - if d, err := readLaunchers(); err != nil { + if d, err := readLaunchers(u.Uid); err != nil { fmt.Println("Error reading active launchers:", err) os.Exit(1) } else if len(d) > 0 { diff --git a/internal/state/register.go b/internal/state/register.go index 2fff565..25c2289 100644 --- a/internal/state/register.go +++ b/internal/state/register.go @@ -4,6 +4,13 @@ func RegisterRevertPath(p string) { cleanupCandidate = append(cleanupCandidate, p) } +func RegisterEnablement(e Enablements) { + if enablements != nil { + panic("enablement state set twice") + } + enablements = &e +} + func XcbActionComplete() { if xcbActionComplete { Fatal("xcb inserted twice") diff --git a/internal/state/track.go b/internal/state/track.go index 913d684..96e9353 100644 --- a/internal/state/track.go +++ b/internal/state/track.go @@ -10,6 +10,8 @@ import ( "os/exec" "path" "strconv" + "strings" + "text/tabwriter" "git.ophivana.moe/cat/fortify/internal/system" ) @@ -22,13 +24,15 @@ var ( statePath string cleanupCandidate []string xcbActionComplete bool + enablements *Enablements ) type launcherState struct { - PID int - Launcher string - Argv []string - Command []string + PID int + Launcher string + Argv []string + Command []string + Capability Enablements } func init() { @@ -40,15 +44,41 @@ func Early() { return } - launchers, err := readLaunchers() + launchers, err := readLaunchers(u.Uid) if err != nil { fmt.Println("Error reading launchers:", err) os.Exit(1) } - fmt.Println("\tPID\tLauncher") + stdout := tabwriter.NewWriter(os.Stdout, 0, 1, 4, ' ', 0) + if !system.V.Verbose { + _, _ = fmt.Fprintln(stdout, "\tPID\tEnablements\tLauncher\tCommand") + } else { + _, _ = fmt.Fprintln(stdout, "\tPID\tArgv") + } + for _, state := range launchers { - fmt.Printf("\t%d\t%s\nCommand: %s\nArgv: %s\n", state.PID, state.Launcher, state.Command, state.Argv) + enablementsDescription := strings.Builder{} + for i := Enablement(0); i < enableLength; i++ { + if state.Capability.Has(i) { + enablementsDescription.WriteString(", " + i.String()) + } + } + if enablementsDescription.Len() == 0 { + enablementsDescription.WriteString("none") + } + + if !system.V.Verbose { + _, _ = fmt.Fprintf(stdout, "\t%d\t%s\t%s\t%s\n", + state.PID, strings.TrimPrefix(enablementsDescription.String(), ", "), state.Launcher, + state.Command) + } else { + _, _ = fmt.Fprintf(stdout, "\t%d\t%s\n", + state.PID, state.Argv) + } + } + if err = stdout.Flush(); err != nil { + fmt.Println("warn: error formatting output:", err) } os.Exit(0) @@ -58,10 +88,11 @@ func Early() { func SaveProcess(uid string, cmd *exec.Cmd) error { statePath = path.Join(system.V.RunDir, uid, strconv.Itoa(cmd.Process.Pid)) state := launcherState{ - PID: cmd.Process.Pid, - Launcher: cmd.Path, - Argv: cmd.Args, - Command: command, + PID: cmd.Process.Pid, + Launcher: cmd.Path, + Argv: cmd.Args, + Command: command, + Capability: *enablements, } if err := os.Mkdir(path.Join(system.V.RunDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) { @@ -81,10 +112,10 @@ func SaveProcess(uid string, cmd *exec.Cmd) error { } } -func readLaunchers() ([]*launcherState, error) { +func readLaunchers(uid string) ([]*launcherState, error) { var f *os.File var r []*launcherState - launcherPrefix := path.Join(system.V.RunDir, u.Uid) + launcherPrefix := path.Join(system.V.RunDir, uid) if pl, err := os.ReadDir(launcherPrefix); err != nil { return nil, err diff --git a/internal/system/value.go b/internal/system/value.go index aa4da51..fa5458f 100644 --- a/internal/system/value.go +++ b/internal/system/value.go @@ -11,7 +11,4 @@ type Values struct { Verbose bool } -var ( - V *Values - MethodFlags [2]bool -) +var V *Values diff --git a/main.go b/main.go index 102252b..9027660 100644 --- a/main.go +++ b/main.go @@ -6,7 +6,6 @@ import ( "fmt" "io/fs" "os" - "path" "strconv" "syscall" @@ -14,8 +13,6 @@ import ( "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/util" - "git.ophivana.moe/cat/fortify/internal/xcb" ) var ( @@ -30,14 +27,6 @@ func tryVersion() { } } -const ( - term = "TERM" - display = "DISPLAY" - - // https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html - waylandDisplay = "WAYLAND_DISPLAY" -) - func main() { flag.Parse() @@ -105,102 +94,20 @@ func main() { } } - // 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) - } + if mustWayland { + a.ShareWayland() } - // 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() - } + if mustX { + a.ShareX() } - // 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) - } - if mustPulse { - state.Fatal("PulseAudio is unavailable") - } - if system.V.Verbose { - fmt.Printf("PulseAudio dir '%s' not found, skipping\n", 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) - } + if mustDBus { + a.ShareDBus() } - // pass $TERM to launcher - if t, ok := os.LookupEnv(term); ok { - a.AppendEnv(term, t) + if mustPulse { + a.SharePulse() } a.Run()