main: implement sudo and machinectl launcher methods
This does almost exactly what github:intgr/ego does, with some minor optimisations and corrections. Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
parent
1cd0846dc9
commit
da7e404bcf
22
cli.go
22
cli.go
@ -9,26 +9,17 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
ego *user.User
|
||||
command []string
|
||||
verbose bool
|
||||
method = machinectl
|
||||
|
||||
userName string
|
||||
methodFlags [2]bool
|
||||
printVersion bool
|
||||
)
|
||||
|
||||
const (
|
||||
machinectl uint8 = iota
|
||||
machinectlBare
|
||||
sudo
|
||||
mustPulse bool
|
||||
)
|
||||
|
||||
func init() {
|
||||
flag.StringVar(&userName, "u", "ego", "Specify a username")
|
||||
flag.BoolVar(&methodFlags[0], "sudo", false, "Use 'sudo' to change user")
|
||||
flag.BoolVar(&methodFlags[1], "bare", false, "Use 'machinectl' but skip xdg-desktop-portal setup")
|
||||
flag.BoolVar(&mustPulse, "pulse", false, "Treat unavailable PulseAudio as fatal")
|
||||
flag.BoolVar(&verbose, "v", false, "Verbose output")
|
||||
flag.BoolVar(&printVersion, "V", false, "Print version")
|
||||
}
|
||||
@ -41,13 +32,6 @@ func copyArgs() {
|
||||
|
||||
command = flag.Args()
|
||||
|
||||
switch { // zero value is machinectl
|
||||
case methodFlags[0]:
|
||||
method = sudo
|
||||
case methodFlags[1]:
|
||||
method = machinectlBare
|
||||
}
|
||||
|
||||
if u, err := user.Lookup(userName); err != nil {
|
||||
if errors.As(err, new(user.UnknownUserError)) {
|
||||
fmt.Println("unknown user", userName)
|
||||
@ -62,6 +46,6 @@ func copyArgs() {
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Println("Running command", command, "as user", ego.Username, "("+ego.Uid+")")
|
||||
fmt.Println("Running as user", ego.Username, "("+ego.Uid+"),", "command:", command)
|
||||
}
|
||||
}
|
||||
|
339
main.go
339
main.go
@ -1,12 +1,351 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
"os/user"
|
||||
"path"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
var Version = "impure"
|
||||
|
||||
var (
|
||||
ego *user.User
|
||||
uid int
|
||||
env []string
|
||||
command []string
|
||||
verbose bool
|
||||
runtime string
|
||||
runDir string
|
||||
)
|
||||
|
||||
const (
|
||||
term = "TERM"
|
||||
home = "HOME"
|
||||
sudoAskPass = "SUDO_ASKPASS"
|
||||
xdgRuntimeDir = "XDG_RUNTIME_DIR"
|
||||
xdgConfigHome = "XDG_CONFIG_HOME"
|
||||
display = "DISPLAY"
|
||||
pulseServer = "PULSE_SERVER"
|
||||
pulseCookie = "PULSE_COOKIE"
|
||||
|
||||
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
|
||||
waylandDisplay = "WAYLAND_DISPLAY"
|
||||
)
|
||||
|
||||
func main() {
|
||||
flag.Parse()
|
||||
tryLauncher()
|
||||
copyArgs()
|
||||
|
||||
if u, err := strconv.Atoi(ego.Uid); err != nil {
|
||||
// usually unreachable
|
||||
panic("ego uid parse")
|
||||
} else {
|
||||
uid = u
|
||||
}
|
||||
|
||||
if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
|
||||
fatal("Env variable", xdgRuntimeDir, "unset")
|
||||
} else {
|
||||
runtime = r
|
||||
}
|
||||
|
||||
// Report warning if user home directory does not exist or has wrong ownership
|
||||
if stat, err := os.Stat(ego.HomeDir); err != nil {
|
||||
if verbose {
|
||||
switch {
|
||||
case errors.Is(err, fs.ErrPermission):
|
||||
fmt.Printf("User %s home directory %s is not accessible", ego.Username, ego.HomeDir)
|
||||
case errors.Is(err, fs.ErrNotExist):
|
||||
fmt.Printf("User %s home directory %s does not exist", ego.Username, ego.HomeDir)
|
||||
default:
|
||||
fmt.Printf("Error stat user %s home directory %s: %s", ego.Username, ego.HomeDir, err)
|
||||
}
|
||||
}
|
||||
return
|
||||
} else {
|
||||
// FreeBSD: not cross-platform
|
||||
if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != ego.Uid {
|
||||
fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", ego.Username, ego.HomeDir, ego.Uid, u)
|
||||
}
|
||||
}
|
||||
|
||||
// Add execute perm to runtime dir, e.g. `/run/user/%d`
|
||||
if s, err := os.Stat(runtime); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
fatal("Runtime directory does not exist")
|
||||
}
|
||||
fatal("Error accessing runtime directory:", err)
|
||||
} 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)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Runtime data dir '%s' configured\n", runtime)
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
// 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 {
|
||||
if verbose {
|
||||
fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping")
|
||||
}
|
||||
} 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 {
|
||||
fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("Wayland socket '%s' configured\n", w)
|
||||
}
|
||||
}
|
||||
|
||||
// Detect `DISPLAY` and grant permissions via X11 protocol `ChangeHosts` command
|
||||
if d, ok := os.LookupEnv(display); !ok {
|
||||
if verbose {
|
||||
fmt.Println("X11: DISPLAY not set, skipping")
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
|
||||
// Add execute permissions to PulseAudio directory (e.g. `/run/user/%d/pulse`)
|
||||
pulse := path.Join(runtime, "pulse")
|
||||
pulseS := path.Join(pulse, "native")
|
||||
if s, err := os.Stat(pulse); err != nil {
|
||||
if !errors.Is(err, fs.ErrNotExist) {
|
||||
fatal("Error accessing PulseAudio directory:", err)
|
||||
}
|
||||
if mustPulse {
|
||||
fatal("PulseAudio is unavailable")
|
||||
}
|
||||
if verbose {
|
||||
fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse)
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
|
||||
// Ensure permissions of PulseAudio socket `/run/user/%d/pulse/native`
|
||||
if s, err = os.Stat(pulseS); err != nil {
|
||||
if errors.Is(err, fs.ErrNotExist) {
|
||||
fatal("PulseAudio directory found but socket does not exist")
|
||||
}
|
||||
fatal("Error accessing PulseAudio socket:", err)
|
||||
} else {
|
||||
if m := s.Mode(); m&0o006 != 0o006 {
|
||||
fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
|
||||
}
|
||||
}
|
||||
|
||||
// Publish current user's pulse-cookie for target user
|
||||
pulseCookieSource := discoverPulseCookie()
|
||||
env = append(env, pulseCookie+"="+pulseCookieSource)
|
||||
pulseCookieFinal := path.Join(runDir, "pulse-cookie")
|
||||
if verbose {
|
||||
fmt.Printf("Publishing PulseAudio cookie '%s' to '%s'\n", pulseCookieSource, pulseCookieFinal)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("PulseAudio dir '%s' configured\n", pulse)
|
||||
}
|
||||
}
|
||||
|
||||
// pass $TERM to launcher
|
||||
if t, ok := os.LookupEnv(term); ok {
|
||||
env = append(env, term+"="+t)
|
||||
}
|
||||
|
||||
f := launchBySudo
|
||||
m, b := false, false
|
||||
switch {
|
||||
case methodFlags[0]: // sudo
|
||||
case 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 !sdBooted() {
|
||||
fmt.Println("This system was not booted through systemd")
|
||||
fmt.Println(sudoFallback)
|
||||
} else if tp, ok := which("machinectl"); !ok {
|
||||
fmt.Println("Did not find 'machinectl' in PATH")
|
||||
fmt.Println(sudoFallback)
|
||||
} else {
|
||||
toolPath = tp
|
||||
f = func() []string { return launchByMachineCtl(b) }
|
||||
}
|
||||
} else if tp, ok := which("sudo"); !ok {
|
||||
fatal("Did not find 'sudo' in PATH")
|
||||
} else {
|
||||
toolPath = tp
|
||||
}
|
||||
|
||||
if verbose {
|
||||
fmt.Printf("Selected launcher '%s' bare=%t\n", toolPath, b)
|
||||
}
|
||||
|
||||
cmd := exec.Command(toolPath, f()...)
|
||||
cmd.Env = env
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Dir = runDir
|
||||
|
||||
if verbose {
|
||||
fmt.Println("Executing:", cmd)
|
||||
}
|
||||
|
||||
if err := cmd.Start(); err != nil {
|
||||
fatal("Error starting process:", err)
|
||||
}
|
||||
|
||||
// Cleanup: need register
|
||||
|
||||
var r int
|
||||
if err := cmd.Wait(); err != nil {
|
||||
var exitError *exec.ExitError
|
||||
if !errors.As(err, &exitError) {
|
||||
fatal("Error running process:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleanup: deregister, call revert
|
||||
|
||||
if verbose {
|
||||
fmt.Println("Process exited with exit code", r)
|
||||
}
|
||||
os.Exit(r)
|
||||
}
|
||||
|
||||
func launchBySudo() (args []string) {
|
||||
args = make([]string, 0, 4+len(env)+len(command))
|
||||
|
||||
// -Hiu $USER
|
||||
args = append(args, "-Hiu", ego.Username)
|
||||
|
||||
// -A?
|
||||
if _, ok := os.LookupEnv(sudoAskPass); ok {
|
||||
if verbose {
|
||||
fmt.Printf("%s set, adding askpass flag\n", sudoAskPass)
|
||||
}
|
||||
args = append(args, "-A")
|
||||
}
|
||||
|
||||
// environ
|
||||
args = append(args, env...)
|
||||
|
||||
// -- $@
|
||||
args = append(args, "--")
|
||||
args = append(args, command...)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func launchByMachineCtl(bare bool) (args []string) {
|
||||
args = make([]string, 0, 9+len(env))
|
||||
|
||||
// shell --uid=$USER
|
||||
args = append(args, "shell", "--uid="+ego.Username)
|
||||
|
||||
// --quiet
|
||||
if !verbose {
|
||||
args = append(args, "--quiet")
|
||||
}
|
||||
|
||||
// environ
|
||||
envQ := make([]string, len(env)+1)
|
||||
for i, e := range env {
|
||||
envQ[i] = "-E" + e
|
||||
}
|
||||
envQ[len(env)] = "-E" + launcherPayloadEnv()
|
||||
args = append(args, envQ...)
|
||||
|
||||
// -- .host
|
||||
args = append(args, "--", ".host")
|
||||
|
||||
// /bin/sh -c
|
||||
if sh, ok := which("sh"); !ok {
|
||||
fatal("Did not find 'sh' in PATH")
|
||||
} else {
|
||||
args = append(args, sh, "-c")
|
||||
}
|
||||
|
||||
if len(command) == 0 { // execute shell if command is not provided
|
||||
command = []string{"$SHELL"}
|
||||
}
|
||||
|
||||
innerCommand := strings.Builder{}
|
||||
|
||||
if !bare {
|
||||
innerCommand.WriteString("dbus-update-activation-environment --systemd")
|
||||
for _, e := range env {
|
||||
innerCommand.WriteString(" " + strings.SplitN(e, "=", 2)[0])
|
||||
}
|
||||
innerCommand.WriteString("; systemctl --user start xdg-desktop-portal-gtk; ")
|
||||
}
|
||||
|
||||
if executable, err := os.Executable(); err != nil {
|
||||
fatal("Error reading executable path:", err)
|
||||
} else {
|
||||
innerCommand.WriteString("exec " + executable + " -V")
|
||||
}
|
||||
args = append(args, innerCommand.String())
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
func fatal(msg ...any) {
|
||||
// Cleanup: call revert
|
||||
fmt.Println(msg...)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user