aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app
diff options
context:
space:
mode:
Diffstat (limited to 'internal/app')
-rw-r--r--internal/app/builder.go13
-rw-r--r--internal/app/launch.go152
-rw-r--r--internal/app/setup.go124
3 files changed, 289 insertions, 0 deletions
diff --git a/internal/app/builder.go b/internal/app/builder.go
new file mode 100644
index 00000000..2bce43b4
--- /dev/null
+++ b/internal/app/builder.go
@@ -0,0 +1,13 @@
+package app
+
+func (a *App) Command() []string {
+ return a.command
+}
+
+func (a *App) UID() int {
+ return a.uid
+}
+
+func (a *App) AppendEnv(k, v string) {
+ a.env = append(a.env, k+"="+v)
+}
diff --git a/internal/app/launch.go b/internal/app/launch.go
new file mode 100644
index 00000000..c6f7b773
--- /dev/null
+++ b/internal/app/launch.go
@@ -0,0 +1,152 @@
+package app
+
+import (
+ "bytes"
+ "encoding/base64"
+ "encoding/gob"
+ "fmt"
+ "os"
+ "strings"
+ "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"
+)
+
+func (a *App) launcherPayloadEnv() string {
+ r := &bytes.Buffer{}
+ enc := base64.NewEncoder(base64.StdEncoding, r)
+
+ if err := gob.NewEncoder(enc).Encode(a.command); err != nil {
+ state.Fatal("Error encoding launcher payload:", err)
+ }
+
+ _ = enc.Close()
+ return launcherPayload + "=" + r.String()
+}
+
+// Early hidden launcher path
+func Early(printVersion bool) {
+ if printVersion {
+ if r, ok := os.LookupEnv(launcherPayload); ok {
+ dec := base64.NewDecoder(base64.StdEncoding, strings.NewReader(r))
+
+ var argv []string
+ if err := gob.NewDecoder(dec).Decode(&argv); err != nil {
+ fmt.Println("Error decoding launcher payload:", err)
+ os.Exit(1)
+ }
+
+ if err := os.Unsetenv(launcherPayload); err != nil {
+ fmt.Println("Error unsetting launcher payload:", err)
+ // not fatal, do not fail
+ }
+
+ var p string
+
+ if len(argv) > 0 {
+ if p, ok = util.Which(argv[0]); !ok {
+ fmt.Printf("Did not find '%s' in PATH\n", argv[0])
+ os.Exit(1)
+ }
+ } else {
+ if p, ok = os.LookupEnv("SHELL"); !ok {
+ fmt.Println("No command was specified and $SHELL was unset")
+ os.Exit(1)
+ }
+ }
+
+ if err := syscall.Exec(p, argv, os.Environ()); err != nil {
+ fmt.Println("Error executing launcher payload:", err)
+ os.Exit(1)
+ }
+
+ // unreachable
+ os.Exit(1)
+ return
+ }
+ }
+}
+
+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/setup.go b/internal/app/setup.go
new file mode 100644
index 00000000..7b5cd2ec
--- /dev/null
+++ b/internal/app/setup.go
@@ -0,0 +1,124 @@
+package app
+
+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 {
+ uid int
+ env []string
+ command []string
+
+ *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)
+ }
+
+ 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 New(userName string, args []string) *App {
+ a := &App{command: args}
+
+ if u, err := user.Lookup(userName); err != nil {
+ if errors.As(err, new(user.UnknownUserError)) {
+ fmt.Println("unknown user", userName)
+ } else {
+ // unreachable
+ panic(err)
+ }
+
+ // too early for fatal
+ os.Exit(1)
+ } else {
+ a.User = u
+ }
+
+ if u, err := strconv.Atoi(a.Uid); err != nil {
+ // usually unreachable
+ panic("uid parse")
+ } else {
+ a.uid = u
+ }
+
+ if system.V.Verbose {
+ fmt.Println("Running as user", a.Username, "("+a.Uid+"),", "command:", a.command)
+ }
+
+ return a
+}