diff --git a/.gitignore b/.gitignore
index ae08513..4b84d41 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,7 +4,7 @@
*.dll
*.so
*.dylib
-/ego
+/fortify
# Test binary, built with `go test -c`
*.test
diff --git a/README.md b/README.md
deleted file mode 100644
index 055e48b..0000000
--- a/README.md
+++ /dev/null
@@ -1,83 +0,0 @@
-ego (the Go side)
-=================
-
-[![Go Reference](https://pkg.go.dev/badge/git.ophivana.moe/cat/ego.svg)](https://pkg.go.dev/git.ophivana.moe/cat/ego)
-
-> Do all your games need access to your documents, browser history, SSH private keys?
->
-> ... No? Just run `ego steam`!
-
-**Ego** is a tool to run Linux desktop applications under a different local user. Currently
-integrates with Wayland, Xorg, PulseAudio and xdg-desktop-portal. You may think of it as `xhost`
-for Wayland and PulseAudio. This is done using filesystem ACLs and X11 host access control.
-
-Disclaimer: **DO NOT RUN UNTRUSTED PROGRAMS VIA EGO.** However, using ego is more secure than
-running applications directly under your primary user.
-
-Differences
------------
-* Written in Go
-* Tracks process states
-* Cleans up after last process exits
-* Argv preservation in machinectl mode
-* Has no dependencies other than the two C libraries
-
-Manual setup
-------------
-Ego aims to come with sane defaults and be easy to set up.
-
-**Requirements:**
-* Sudo
-* A C compiler
-* [Go](https://go.dev/doc/install)
-* `libacl.so` library (Debian/Ubuntu: libacl1-dev; Fedora: libacl-devel; Arch: acl)
-* `libxcb.so` library (Debian/Ubuntu: libxcb1-dev; Fedora: libxcb-devel; Arch: libxcb)
-
-**Recommended:** (Not needed when using `--sudo` mode, but some desktop functionality may not work).
-* `machinectl` command (Debian/Ubuntu/Fedora: systemd-container; Arch: systemd)
-* `xdg-desktop-portal-gtk` (Debian/Ubuntu/Fedora/Arch: xdg-desktop-portal-gtk)
-
-**Installation:**
-
-1. Run in repository worktree:
-
- go build -v -ldflags '-s -w'
- sudo cp ego /usr/local/bin/
-
-2. Create local user named "ego": [1]
-
- sudo useradd ego --uid 155 --create-home
-
-3. That's all, try it:
-
- ego xdg-open .
-
-[1] No extra groups are needed by the ego user.
-UID below 1000 hides this user on the login screen.
-
-### Avoid password prompt
-If using "machinectl" mode (default if available), you need the rather new systemd version >=247
-and polkit >=0.106 to do this securely.
-
-Create file `/etc/polkit-1/rules.d/50-ego-machinectl.rules`, polkit will automatically load it
-(replace `$USER` with your own username):
-
-```js
-polkit.addRule(function(action, subject) {
- if (action.id == "org.freedesktop.machine1.host-shell" &&
- action.lookup("user") == "ego" &&
- subject.user == "$USER") {
- return polkit.Result.YES;
- }
-});
-```
-
-##### sudo mode
-For sudo, add the following to `/etc/sudoers` (replace `$USER` with your own username):
-
- $USER ALL=(ego) NOPASSWD:ALL
-
-Appendix
---------
-Ego is licensed under the MIT License (see the `LICENSE` file).
-The original Ego was created by Marti Raudsepp under the repository https://github.com/intgr/ego
\ No newline at end of file
diff --git a/cli.go b/cli.go
index ca1b41b..e5466f0 100644
--- a/cli.go
+++ b/cli.go
@@ -1,50 +1,23 @@
package main
import (
- "errors"
"flag"
- "fmt"
- "os"
- "os/user"
+
+ "git.ophivana.moe/cat/fortify/internal/system"
)
var (
userName string
- methodFlags [2]bool
printVersion bool
mustPulse bool
+ flagVerbose 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.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(&verbose, "v", false, "Verbose output")
+ flag.BoolVar(&flagVerbose, "v", false, "Verbose output")
flag.BoolVar(&printVersion, "V", false, "Print version")
}
-
-func copyArgs() {
- tryLauncher()
- tryVersion()
- tryLicense()
-
- command = flag.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)
- }
-
- os.Exit(1)
- } else {
- ego = u
- }
-
- if verbose {
- fmt.Println("Running as user", ego.Username, "("+ego.Uid+"),", "command:", command)
- }
-}
diff --git a/flake.nix b/flake.nix
index 8c8e419..1e31f3b 100644
--- a/flake.nix
+++ b/flake.nix
@@ -1,5 +1,5 @@
{
- description = "ego development environment";
+ description = "fortify development environment";
inputs = {
nixpkgs.url = "github:NixOS/nixpkgs/24.05";
@@ -30,11 +30,11 @@
mkShell {
packages = [
(buildGoModule rec {
- pname = "ego";
+ pname = "fortify";
version = "0.0.0-flake";
src = ./.;
- vendorHash = null; # we have no dependencies :3
+ vendorHash = null; # we have no Go dependencies :3
ldflags = [
"-s"
diff --git a/go.mod b/go.mod
index c3d7971..e5903ee 100644
--- a/go.mod
+++ b/go.mod
@@ -1,3 +1,3 @@
-module git.ophivana.moe/cat/ego
+module git.ophivana.moe/cat/fortify
go 1.22
diff --git a/acl.go b/internal/acl/c.go
similarity index 50%
rename from acl.go
rename to internal/acl/c.go
index 34e51a4..89bc046 100644
--- a/acl.go
+++ b/internal/acl/c.go
@@ -1,4 +1,4 @@
-package main
+package acl
import (
"errors"
@@ -13,88 +13,11 @@ import (
//#cgo linux LDFLAGS: -lacl
import "C"
-const (
- aclRead = C.ACL_READ
- aclWrite = C.ACL_WRITE
- aclExecute = C.ACL_EXECUTE
-
- aclTypeDefault = C.ACL_TYPE_DEFAULT
- aclTypeAccess = C.ACL_TYPE_ACCESS
-
- aclUndefinedTag = C.ACL_UNDEFINED_TAG
- aclUserObj = C.ACL_USER_OBJ
- aclUser = C.ACL_USER
- aclGroupObj = C.ACL_GROUP_OBJ
- aclGroup = C.ACL_GROUP
- aclMask = C.ACL_MASK
- aclOther = C.ACL_OTHER
-)
-
type acl struct {
val C.acl_t
freed bool
}
-func aclUpdatePerm(path string, uid int, perms ...C.acl_perm_t) error {
- // read acl from file
- a, err := aclGetFile(path, aclTypeAccess)
- if err != nil {
- return err
- }
- // free acl on return if get is successful
- defer a.free()
-
- // remove existing entry
- if err = a.removeEntry(aclUser, uid); err != nil {
- return err
- }
-
- // create new entry if perms are passed
- if len(perms) > 0 {
- // create new acl entry
- var e C.acl_entry_t
- if _, err = C.acl_create_entry(&a.val, &e); err != nil {
- return err
- }
-
- // get perm set of new entry
- var p C.acl_permset_t
- if _, err = C.acl_get_permset(e, &p); err != nil {
- return err
- }
-
- // add target perms
- for _, perm := range perms {
- if _, err = C.acl_add_perm(p, perm); err != nil {
- return err
- }
- }
-
- // set perm set to new entry
- if _, err = C.acl_set_permset(e, p); err != nil {
- return err
- }
-
- // set user tag to new entry
- if _, err = C.acl_set_tag_type(e, aclUser); err != nil {
- return err
- }
-
- // set qualifier (uid) to new entry
- if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
- return err
- }
- }
-
- // calculate mask after update
- if _, err = C.acl_calc_mask(&a.val); err != nil {
- return err
- }
-
- // write acl to file
- return a.setFile(path, aclTypeAccess)
-}
-
func aclGetFile(path string, t C.acl_type_t) (*acl, error) {
p := C.CString(path)
a, err := C.acl_get_file(p, t)
diff --git a/internal/acl/export.go b/internal/acl/export.go
new file mode 100644
index 0000000..aeb8ed9
--- /dev/null
+++ b/internal/acl/export.go
@@ -0,0 +1,86 @@
+package acl
+
+import "unsafe"
+
+//#include
+//#include
+//#include
+//#cgo linux LDFLAGS: -lacl
+import "C"
+
+const (
+ Read = C.ACL_READ
+ Write = C.ACL_WRITE
+ Execute = C.ACL_EXECUTE
+
+ TypeDefault = C.ACL_TYPE_DEFAULT
+ TypeAccess = C.ACL_TYPE_ACCESS
+
+ UndefinedTag = C.ACL_UNDEFINED_TAG
+ UserObj = C.ACL_USER_OBJ
+ User = C.ACL_USER
+ GroupObj = C.ACL_GROUP_OBJ
+ Group = C.ACL_GROUP
+ Mask = C.ACL_MASK
+ Other = C.ACL_OTHER
+)
+
+func UpdatePerm(path string, uid int, perms ...C.acl_perm_t) error {
+ // read acl from file
+ a, err := aclGetFile(path, TypeAccess)
+ if err != nil {
+ return err
+ }
+ // free acl on return if get is successful
+ defer a.free()
+
+ // remove existing entry
+ if err = a.removeEntry(User, uid); err != nil {
+ return err
+ }
+
+ // create new entry if perms are passed
+ if len(perms) > 0 {
+ // create new acl entry
+ var e C.acl_entry_t
+ if _, err = C.acl_create_entry(&a.val, &e); err != nil {
+ return err
+ }
+
+ // get perm set of new entry
+ var p C.acl_permset_t
+ if _, err = C.acl_get_permset(e, &p); err != nil {
+ return err
+ }
+
+ // add target perms
+ for _, perm := range perms {
+ if _, err = C.acl_add_perm(p, perm); err != nil {
+ return err
+ }
+ }
+
+ // set perm set to new entry
+ if _, err = C.acl_set_permset(e, p); err != nil {
+ return err
+ }
+
+ // set user tag to new entry
+ if _, err = C.acl_set_tag_type(e, User); err != nil {
+ return err
+ }
+
+ // set qualifier (uid) to new entry
+ if _, err = C.acl_set_qualifier(e, unsafe.Pointer(&uid)); err != nil {
+ return err
+ }
+ }
+
+ // calculate mask after update
+ if _, err = C.acl_calc_mask(&a.val); err != nil {
+ return err
+ }
+
+ // write acl to file
+ return a.setFile(path, TypeAccess)
+}
diff --git a/internal/app/builder.go b/internal/app/builder.go
new file mode 100644
index 0000000..2bce43b
--- /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 0000000..c6f7b77
--- /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 0000000..7b5cd2e
--- /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
+}
diff --git a/internal/state/exit.go b/internal/state/exit.go
new file mode 100644
index 0000000..e0e2c04
--- /dev/null
+++ b/internal/state/exit.go
@@ -0,0 +1,68 @@
+package state
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+
+ "git.ophivana.moe/cat/fortify/internal/acl"
+ "git.ophivana.moe/cat/fortify/internal/system"
+ "git.ophivana.moe/cat/fortify/internal/xcb"
+)
+
+func Fatal(msg ...any) {
+ fmt.Println(msg...)
+ BeforeExit()
+ os.Exit(1)
+}
+
+func BeforeExit() {
+ if u == nil {
+ fmt.Println("warn: beforeExit called before app init")
+ return
+ }
+
+ if statePath == "" {
+ if system.V.Verbose {
+ fmt.Println("State path is unset")
+ }
+ } else {
+ if err := os.Remove(statePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
+ fmt.Println("Error removing state file:", err)
+ }
+ }
+
+ if d, err := readLaunchers(); err != nil {
+ fmt.Println("Error reading active launchers:", err)
+ os.Exit(1)
+ } else if len(d) > 0 {
+ // other launchers are still active
+ if system.V.Verbose {
+ fmt.Printf("Found %d active launchers, exiting without cleaning up\n", len(d))
+ }
+ return
+ }
+
+ if system.V.Verbose {
+ fmt.Println("No other launchers active, will clean up")
+ }
+
+ if xcbActionComplete {
+ if system.V.Verbose {
+ fmt.Printf("X11: Removing XHost entry SI:localuser:%s\n", u.Username)
+ }
+ if err := xcb.ChangeHosts(xcb.HostModeDelete, xcb.FamilyServerInterpreted, "localuser\x00"+u.Username); err != nil {
+ fmt.Println("Error removing XHost entry:", err)
+ }
+ }
+
+ for _, candidate := range cleanupCandidate {
+ if err := acl.UpdatePerm(candidate, uid); err != nil {
+ fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err)
+ }
+ if system.V.Verbose {
+ fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", u.Username, candidate)
+ }
+ }
+}
diff --git a/internal/state/register.go b/internal/state/register.go
new file mode 100644
index 0000000..2fff565
--- /dev/null
+++ b/internal/state/register.go
@@ -0,0 +1,12 @@
+package state
+
+func RegisterRevertPath(p string) {
+ cleanupCandidate = append(cleanupCandidate, p)
+}
+
+func XcbActionComplete() {
+ if xcbActionComplete {
+ Fatal("xcb inserted twice")
+ }
+ xcbActionComplete = true
+}
diff --git a/state.go b/internal/state/track.go
similarity index 55%
rename from state.go
rename to internal/state/track.go
index 2204708..913d684 100644
--- a/state.go
+++ b/internal/state/track.go
@@ -1,4 +1,4 @@
-package main
+package state
import (
"encoding/gob"
@@ -10,6 +10,8 @@ import (
"os/exec"
"path"
"strconv"
+
+ "git.ophivana.moe/cat/fortify/internal/system"
)
// we unfortunately have to assume there are never races between processes
@@ -33,7 +35,7 @@ func init() {
flag.BoolVar(&stateActionEarly, "state", false, "query state value of current active launchers")
}
-func tryState() {
+func Early() {
if !stateActionEarly {
return
}
@@ -52,13 +54,9 @@ func tryState() {
os.Exit(0)
}
-func registerRevertPath(p string) {
- cleanupCandidate = append(cleanupCandidate, p)
-}
-
-// called after process start, before wait
-func registerProcess(uid string, cmd *exec.Cmd) error {
- statePath = path.Join(runDir, uid, strconv.Itoa(cmd.Process.Pid))
+// SaveProcess called after process start, before wait
+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,
@@ -66,7 +64,7 @@ func registerProcess(uid string, cmd *exec.Cmd) error {
Command: command,
}
- if err := os.Mkdir(path.Join(runDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
+ if err := os.Mkdir(path.Join(system.V.RunDir, uid), 0700); err != nil && !errors.Is(err, fs.ErrExist) {
return err
}
@@ -86,7 +84,7 @@ func registerProcess(uid string, cmd *exec.Cmd) error {
func readLaunchers() ([]*launcherState, error) {
var f *os.File
var r []*launcherState
- launcherPrefix := path.Join(runDir, ego.Uid)
+ launcherPrefix := path.Join(system.V.RunDir, u.Uid)
if pl, err := os.ReadDir(launcherPrefix); err != nil {
return nil, err
@@ -115,48 +113,3 @@ func readLaunchers() ([]*launcherState, error) {
return r, nil
}
-
-func beforeExit() {
- if err := os.Remove(statePath); err != nil && !errors.Is(err, fs.ErrNotExist) {
- fmt.Println("Error removing state file:", err)
- }
-
- if a, err := readLaunchers(); err != nil {
- fmt.Println("Error reading active launchers:", err)
- os.Exit(1)
- } else if len(a) > 0 {
- // other launchers are still active
- if verbose {
- fmt.Printf("Found %d active launchers, exiting without cleaning up\n", len(a))
- }
- return
- }
-
- if verbose {
- fmt.Println("No other launchers active, will clean up")
- }
-
- if xcbActionComplete {
- if verbose {
- fmt.Printf("X11: Removing XHost entry SI:localuser:%s\n", ego.Username)
- }
- if err := changeHosts(xcbHostModeDelete, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
- fmt.Println("Error removing XHost entry:", err)
- }
- }
-
- for _, candidate := range cleanupCandidate {
- if err := aclUpdatePerm(candidate, uid); err != nil {
- fmt.Printf("Error stripping ACL entry from '%s': %s\n", candidate, err)
- }
- if verbose {
- fmt.Printf("Stripped ACL entry for user '%s' from '%s'\n", ego.Username, candidate)
- }
- }
-}
-
-func fatal(msg ...any) {
- fmt.Println(msg...)
- beforeExit()
- os.Exit(1)
-}
diff --git a/internal/state/value.go b/internal/state/value.go
new file mode 100644
index 0000000..cb20818
--- /dev/null
+++ b/internal/state/value.go
@@ -0,0 +1,21 @@
+package state
+
+import (
+ "os/user"
+)
+
+var (
+ u *user.User
+ uid int
+ command []string
+)
+
+func Set(val user.User, c []string, d int) {
+ if u != nil {
+ panic("state set twice")
+ }
+
+ u = &val
+ command = c
+ uid = d
+}
diff --git a/internal/system/retrieve.go b/internal/system/retrieve.go
new file mode 100644
index 0000000..d9f0e3d
--- /dev/null
+++ b/internal/system/retrieve.go
@@ -0,0 +1,28 @@
+package system
+
+import (
+ "fmt"
+ "os"
+ "path"
+ "strconv"
+)
+
+func Retrieve(verbose bool) {
+ if V != nil {
+ panic("system info retrieved twice")
+ }
+
+ v := &Values{Share: path.Join(os.TempDir(), "fortify."+strconv.Itoa(os.Geteuid())), Verbose: verbose}
+
+ if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
+ fmt.Println("Env variable", xdgRuntimeDir, "unset")
+
+ // too early for fatal
+ os.Exit(1)
+ } else {
+ v.Runtime = r
+ v.RunDir = path.Join(v.Runtime, "fortify")
+ }
+
+ V = v
+}
diff --git a/internal/system/value.go b/internal/system/value.go
new file mode 100644
index 0000000..aa4da51
--- /dev/null
+++ b/internal/system/value.go
@@ -0,0 +1,17 @@
+package system
+
+const (
+ xdgRuntimeDir = "XDG_RUNTIME_DIR"
+)
+
+type Values struct {
+ Share string
+ Runtime string
+ RunDir string
+ Verbose bool
+}
+
+var (
+ V *Values
+ MethodFlags [2]bool
+)
diff --git a/internal/util/simple.go b/internal/util/simple.go
new file mode 100644
index 0000000..08ce23f
--- /dev/null
+++ b/internal/util/simple.go
@@ -0,0 +1,39 @@
+package util
+
+import (
+ "io"
+ "os"
+ "os/exec"
+)
+
+func Which(file string) (string, bool) {
+ p, err := exec.LookPath(file)
+ return p, err == nil
+}
+
+func CopyFile(dst, src string) error {
+ srcD, err := os.Open(src)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if srcD.Close() != nil {
+ // unreachable
+ panic("src file closed prematurely")
+ }
+ }()
+
+ dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
+ if err != nil {
+ return err
+ }
+ defer func() {
+ if dstD.Close() != nil {
+ // unreachable
+ panic("dst file closed prematurely")
+ }
+ }()
+
+ _, err = io.Copy(dstD, srcD)
+ return err
+}
diff --git a/internal/util/std.go b/internal/util/std.go
new file mode 100644
index 0000000..5c37a0d
--- /dev/null
+++ b/internal/util/std.go
@@ -0,0 +1,75 @@
+package util
+
+import (
+ "errors"
+ "fmt"
+ "io/fs"
+ "os"
+ "path"
+
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/system"
+)
+
+const (
+ systemdCheckPath = "/run/systemd/system"
+
+ home = "HOME"
+ xdgConfigHome = "XDG_CONFIG_HOME"
+
+ PulseServer = "PULSE_SERVER"
+ PulseCookie = "PULSE_COOKIE"
+)
+
+// SdBooted implements https://www.freedesktop.org/software/systemd/man/sd_booted.html
+func SdBooted() bool {
+ _, err := os.Stat(systemdCheckPath)
+ if err != nil {
+ if system.V.Verbose {
+ if errors.Is(err, fs.ErrNotExist) {
+ fmt.Println("System not booted through systemd")
+ } else {
+ fmt.Println("Error accessing", systemdCheckPath+":", err.Error())
+ }
+ }
+ return false
+ }
+ return true
+}
+
+// DiscoverPulseCookie try various standard methods to discover the current user's PulseAudio authentication cookie
+func DiscoverPulseCookie() string {
+ if p, ok := os.LookupEnv(PulseCookie); ok {
+ return p
+ }
+
+ if p, ok := os.LookupEnv(home); ok {
+ p = path.Join(p, ".pulse-cookie")
+ if s, err := os.Stat(p); err != nil {
+ if !errors.Is(err, fs.ErrNotExist) {
+ state.Fatal("Error accessing PulseAudio cookie:", err)
+ // unreachable
+ return p
+ }
+ } else if !s.IsDir() {
+ return p
+ }
+ }
+
+ if p, ok := os.LookupEnv(xdgConfigHome); ok {
+ p = path.Join(p, "pulse", "cookie")
+ if s, err := os.Stat(p); err != nil {
+ if !errors.Is(err, fs.ErrNotExist) {
+ state.Fatal("Error accessing PulseAudio cookie:", err)
+ // unreachable
+ return p
+ }
+ } else if !s.IsDir() {
+ return p
+ }
+ }
+
+ state.Fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
+ PulseCookie, xdgConfigHome, home))
+ return ""
+}
diff --git a/internal/xcb/c.go b/internal/xcb/c.go
new file mode 100644
index 0000000..8fe59d8
--- /dev/null
+++ b/internal/xcb/c.go
@@ -0,0 +1,33 @@
+package xcb
+
+import (
+ "errors"
+)
+
+//#include
+//#include
+//#cgo linux LDFLAGS: -lxcb
+import "C"
+
+func xcbHandleConnectionError(c *C.xcb_connection_t) error {
+ if errno := C.xcb_connection_has_error(c); errno != 0 {
+ switch errno {
+ case C.XCB_CONN_ERROR:
+ return errors.New("connection error")
+ case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED:
+ return errors.New("extension not supported")
+ case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT:
+ return errors.New("memory not available")
+ case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED:
+ return errors.New("request length exceeded")
+ case C.XCB_CONN_CLOSED_PARSE_ERR:
+ return errors.New("invalid display string")
+ case C.XCB_CONN_CLOSED_INVALID_SCREEN:
+ return errors.New("server has no screen matching display")
+ default:
+ return errors.New("generic X11 failure")
+ }
+ } else {
+ return nil
+ }
+}
diff --git a/internal/xcb/export.go b/internal/xcb/export.go
new file mode 100644
index 0000000..72a7413
--- /dev/null
+++ b/internal/xcb/export.go
@@ -0,0 +1,47 @@
+package xcb
+
+//#include
+//#include
+//#cgo linux LDFLAGS: -lxcb
+import "C"
+import (
+ "errors"
+ "unsafe"
+)
+
+const (
+ HostModeInsert = C.XCB_HOST_MODE_INSERT
+ HostModeDelete = C.XCB_HOST_MODE_DELETE
+
+ FamilyInternet = C.XCB_FAMILY_INTERNET
+ FamilyDecnet = C.XCB_FAMILY_DECNET
+ FamilyChaos = C.XCB_FAMILY_CHAOS
+ FamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED
+ FamilyInternet6 = C.XCB_FAMILY_INTERNET_6
+)
+
+func ChangeHosts(mode, family C.uint8_t, address string) error {
+ var c *C.xcb_connection_t
+ c = C.xcb_connect(nil, nil)
+ defer C.xcb_disconnect(c)
+
+ if err := xcbHandleConnectionError(c); err != nil {
+ return err
+ }
+
+ addr := C.CString(address)
+ cookie := C.xcb_change_hosts_checked(c, mode, family, C.ushort(len(address)), (*C.uchar)(unsafe.Pointer(addr)))
+ C.free(unsafe.Pointer(addr))
+
+ if err := xcbHandleConnectionError(c); err != nil {
+ return err
+ }
+
+ e := C.xcb_request_check(c, cookie)
+ if e != nil {
+ defer C.free(unsafe.Pointer(e))
+ return errors.New("xcb_change_hosts() failed")
+ }
+
+ return nil
+}
diff --git a/launcher.go b/launcher.go
deleted file mode 100644
index 68d2cb8..0000000
--- a/launcher.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package main
-
-import (
- "bytes"
- "encoding/base64"
- "encoding/gob"
- "fmt"
- "os"
- "strings"
- "syscall"
-)
-
-const (
- // hidden path for main to act as a launcher
- egoLauncher = "EGO_LAUNCHER"
-)
-
-// hidden launcher path
-func tryLauncher() {
- if printVersion {
- if r, ok := os.LookupEnv(egoLauncher); ok {
- // egoLauncher variable contains launcher payload
- 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(egoLauncher); err != nil {
- fmt.Println("Error unsetting launcher payload:", err)
- // not fatal, do not fail
- }
-
- var p string
-
- if len(argv) > 0 {
- if p, ok = 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 launcherPayloadEnv() string {
- r := &bytes.Buffer{}
- enc := base64.NewEncoder(base64.StdEncoding, r)
-
- if err := gob.NewEncoder(enc).Encode(command); err != nil {
- fatal("Error encoding launcher payload:", err)
- }
-
- _ = enc.Close()
- return egoLauncher + "=" + r.String()
-}
diff --git a/main.go b/main.go
index 4a719d8..102252b 100644
--- a/main.go
+++ b/main.go
@@ -6,15 +6,22 @@ import (
"fmt"
"io/fs"
"os"
- "os/exec"
- "os/user"
"path"
"strconv"
- "strings"
"syscall"
+
+ "git.ophivana.moe/cat/fortify/internal/acl"
+ "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 Version = "impure"
+var (
+ Version = "impure"
+ a *app.App
+)
func tryVersion() {
if printVersion {
@@ -23,25 +30,9 @@ func tryVersion() {
}
}
-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"
+ term = "TERM"
+ display = "DISPLAY"
// https://manpages.debian.org/experimental/libwayland-doc/wl_display_connect.3.en.html
waylandDisplay = "WAYLAND_DISPLAY"
@@ -49,315 +40,168 @@ const (
func main() {
flag.Parse()
- copyArgs()
- if u, err := strconv.Atoi(ego.Uid); err != nil {
- // usually unreachable
- panic("ego uid parse")
- } else {
- uid = u
+ // launcher payload early exit
+ app.Early(printVersion)
+
+ // version/license command early exit
+ tryVersion()
+ tryLicense()
+
+ system.Retrieve(flagVerbose)
+ a = app.New(userName, flag.Args())
+ state.Set(*a.User, a.Command(), a.UID())
+
+ // ensure RunDir (e.g. `/run/user/%d/fortify`)
+ if err := os.Mkdir(system.V.RunDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
+ state.Fatal("Error creating runtime directory:", err)
}
- if r, ok := os.LookupEnv(xdgRuntimeDir); !ok {
- fatal("Env variable", xdgRuntimeDir, "unset")
- } else {
- runtime = r
- runDir = path.Join(runtime, "ego")
+ // state query command early exit
+ state.Early()
+
+ // ensure Share (e.g. `/tmp/fortify.%d`)
+ // acl is unnecessary as this directory is world executable
+ if err := os.Mkdir(system.V.Share, 0701); err != nil && !errors.Is(err, fs.ErrExist) {
+ state.Fatal("Error creating shared directory:", err)
}
- // state query command
- tryState()
-
- // Report warning if user home directory does not exist or has wrong ownership
- if stat, err := os.Stat(ego.HomeDir); err != nil {
- if verbose {
+ // warn about target user home directory ownership
+ if stat, err := os.Stat(a.HomeDir); err != nil {
+ if system.V.Verbose {
switch {
case errors.Is(err, fs.ErrPermission):
- fmt.Printf("User %s home directory %s is not accessible", ego.Username, ego.HomeDir)
+ fmt.Printf("User %s home directory %s is not accessible", a.Username, a.HomeDir)
case errors.Is(err, fs.ErrNotExist):
- fmt.Printf("User %s home directory %s does not exist", ego.Username, ego.HomeDir)
+ fmt.Printf("User %s home directory %s does not exist", a.Username, a.HomeDir)
default:
- fmt.Printf("Error stat user %s home directory %s: %s", ego.Username, ego.HomeDir, err)
+ fmt.Printf("Error stat user %s home directory %s: %s", a.Username, a.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)
+ if u := strconv.Itoa(int(stat.Sys().(*syscall.Stat_t).Uid)); u != a.Uid {
+ fmt.Printf("User %s home directory %s has incorrect ownership (expected UID %s, found %s)", a.Username, a.HomeDir, a.Uid, u)
}
}
- // Add execute perm to runtime dir, e.g. `/run/user/%d`
- if s, err := os.Stat(runtime); err != nil {
+ // ensure runtime directory ACL (e.g. `/run/user/%d`)
+ if s, err := os.Stat(system.V.Runtime); err != nil {
if errors.Is(err, fs.ErrNotExist) {
- fatal("Runtime directory does not exist")
+ state.Fatal("Runtime directory does not exist")
}
- fatal("Error accessing runtime directory:", err)
+ state.Fatal("Error accessing runtime directory:", err)
} else if !s.IsDir() {
- fatal(fmt.Sprintf("Path '%s' is not a directory", runtime))
+ state.Fatal(fmt.Sprintf("Path '%s' is not a directory", system.V.Runtime))
} else {
- if err = aclUpdatePerm(runtime, uid, aclExecute); err != nil {
- fatal("Error preparing runtime dir:", err)
+ if err = acl.UpdatePerm(system.V.Runtime, a.UID(), acl.Execute); err != nil {
+ state.Fatal("Error preparing runtime dir:", err)
} else {
- registerRevertPath(runtime)
+ state.RegisterRevertPath(system.V.Runtime)
}
- if verbose {
- fmt.Printf("Runtime data dir '%s' configured\n", runtime)
+ if system.V.Verbose {
+ fmt.Printf("Runtime data dir '%s' configured\n", system.V.Runtime)
}
}
- // Create runtime dir for Ego itself (e.g. `/run/user/%d/ego`) and make it readable for target
- if err := os.Mkdir(runDir, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
- fatal("Error creating Ego runtime dir:", err)
- }
- if err := aclUpdatePerm(runDir, uid, aclExecute); err != nil {
- fatal("Error preparing Ego runtime dir:", err)
- } else {
- registerRevertPath(runDir)
- }
-
- // Add rwx permissions to Wayland socket (e.g. `/run/user/%d/wayland-0`)
+ // ensure Wayland socket ACL (e.g. `/run/user/%d/wayland-%d`)
if w, ok := os.LookupEnv(waylandDisplay); !ok {
- if verbose {
+ if system.V.Verbose {
fmt.Println("Wayland: WAYLAND_DISPLAY not set, skipping")
}
} else {
// add environment variable for new process
- env = append(env, waylandDisplay+"="+path.Join(runtime, w))
- wp := path.Join(runtime, w)
- if err := aclUpdatePerm(wp, uid, aclRead, aclWrite, aclExecute); err != nil {
- fatal(fmt.Sprintf("Error preparing Wayland '%s':", w), err)
+ 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 {
- registerRevertPath(wp)
+ state.RegisterRevertPath(wp)
}
- if verbose {
+ if system.V.Verbose {
fmt.Printf("Wayland socket '%s' configured\n", w)
}
}
- // Detect `DISPLAY` and grant permissions via X11 protocol `ChangeHosts` command
+ // discovery X11 and grant user permission via the `ChangeHosts` command
if d, ok := os.LookupEnv(display); !ok {
- if verbose {
+ if system.V.Verbose {
fmt.Println("X11: DISPLAY not set, skipping")
}
} else {
// add environment variable for new process
- env = append(env, display+"="+d)
+ a.AppendEnv(display, d)
- if verbose {
- fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", ego.Username, d)
+ if system.V.Verbose {
+ fmt.Printf("X11: Adding XHost entry SI:localuser:%s to display '%s'\n", a.Username, d)
}
- if err := changeHosts(xcbHostModeInsert, xcbFamilyServerInterpreted, "localuser\x00"+ego.Username); err != nil {
- fatal(fmt.Sprintf("Error adding XHost entry to '%s':", d), err)
+ 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 {
- xcbActionComplete = true
+ state.XcbActionComplete()
}
}
- // Add execute permissions to PulseAudio directory (e.g. `/run/user/%d/pulse`)
- pulse := path.Join(runtime, "pulse")
+ // 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) {
- fatal("Error accessing PulseAudio directory:", err)
+ state.Fatal("Error accessing PulseAudio directory:", err)
}
if mustPulse {
- fatal("PulseAudio is unavailable")
+ state.Fatal("PulseAudio is unavailable")
}
- if verbose {
+ if system.V.Verbose {
fmt.Printf("PulseAudio dir '%s' not found, skipping\n", pulse)
}
} else {
// add environment variable for new process
- env = append(env, pulseServer+"=unix:"+pulseS)
- if err = aclUpdatePerm(pulse, uid, aclExecute); err != nil {
- fatal("Error preparing PulseAudio:", err)
+ a.AppendEnv(util.PulseServer, "unix:"+pulseS)
+ if err = acl.UpdatePerm(pulse, a.UID(), acl.Execute); err != nil {
+ state.Fatal("Error preparing PulseAudio:", err)
} else {
- registerRevertPath(pulse)
+ state.RegisterRevertPath(pulse)
}
- // Ensure permissions of PulseAudio socket `/run/user/%d/pulse/native`
+ // 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) {
- fatal("PulseAudio directory found but socket does not exist")
+ state.Fatal("PulseAudio directory found but socket does not exist")
}
- fatal("Error accessing PulseAudio socket:", err)
+ state.Fatal("Error accessing PulseAudio socket:", err)
} else {
if m := s.Mode(); m&0o006 != 0o006 {
- fatal(fmt.Sprintf("Unexpected permissions on '%s':", pulseS), m)
+ state.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 {
+ 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 = copyFile(pulseCookieFinal, pulseCookieSource); err != nil {
- fatal("Error copying PulseAudio cookie:", err)
+ if err = util.CopyFile(pulseCookieFinal, pulseCookieSource); err != nil {
+ state.Fatal("Error copying PulseAudio cookie:", err)
}
- if err = aclUpdatePerm(pulseCookieFinal, uid, aclRead); err != nil {
- fatal("Error publishing PulseAudio cookie:", err)
+ if err = acl.UpdatePerm(pulseCookieFinal, a.UID(), acl.Read); err != nil {
+ state.Fatal("Error publishing PulseAudio cookie:", err)
} else {
- registerRevertPath(pulseCookieFinal)
+ state.RegisterRevertPath(pulseCookieFinal)
}
- if verbose {
+ if system.V.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)
+ a.AppendEnv(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)
- }
-
- if err := registerProcess(ego.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) {
- fatal("Error running process:", err)
- }
- }
-
- if verbose {
- fmt.Println("Process exited with exit code", r)
- }
- beforeExit()
- 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
+ a.Run()
}
diff --git a/util.go b/util.go
deleted file mode 100644
index 1a50743..0000000
--- a/util.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package main
-
-import (
- "errors"
- "fmt"
- "io"
- "io/fs"
- "os"
- "os/exec"
- "path"
-)
-
-const (
- systemdCheckPath = "/run/systemd/system"
-)
-
-// https://www.freedesktop.org/software/systemd/man/sd_booted.html
-func sdBooted() bool {
- _, err := os.Stat(systemdCheckPath)
- if err != nil {
- if verbose {
- if errors.Is(err, fs.ErrNotExist) {
- fmt.Println("System not booted through systemd")
- } else {
- fmt.Println("Error accessing", systemdCheckPath+":", err.Error())
- }
- }
- return false
- }
- return true
-}
-
-// Try various ways to discover the current user's PulseAudio authentication cookie.
-func discoverPulseCookie() string {
- if p, ok := os.LookupEnv(pulseCookie); ok {
- return p
- }
-
- if p, ok := os.LookupEnv(home); ok {
- p = path.Join(p, ".pulse-cookie")
- if s, err := os.Stat(p); err != nil {
- if !errors.Is(err, fs.ErrNotExist) {
- fatal("Error accessing PulseAudio cookie:", err)
- // unreachable
- return p
- }
- } else if !s.IsDir() {
- return p
- }
- }
-
- if p, ok := os.LookupEnv(xdgConfigHome); ok {
- p = path.Join(p, "pulse", "cookie")
- if s, err := os.Stat(p); err != nil {
- if !errors.Is(err, fs.ErrNotExist) {
- fatal("Error accessing PulseAudio cookie:", err)
- // unreachable
- return p
- }
- } else if !s.IsDir() {
- return p
- }
- }
-
- fatal(fmt.Sprintf("Cannot locate PulseAudio cookie (tried $%s, $%s/pulse/cookie, $%s/.pulse-cookie)",
- pulseCookie, xdgConfigHome, home))
- return ""
-}
-
-func which(file string) (string, bool) {
- p, err := exec.LookPath(file)
- return p, err == nil
-}
-
-func copyFile(dst, src string) error {
- srcD, err := os.Open(src)
- if err != nil {
- return err
- }
- defer func() {
- if srcD.Close() != nil {
- // unreachable
- panic("src file closed prematurely")
- }
- }()
-
- dstD, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0600)
- if err != nil {
- return err
- }
- defer func() {
- if dstD.Close() != nil {
- // unreachable
- panic("dst file closed prematurely")
- }
- }()
-
- _, err = io.Copy(dstD, srcD)
- return err
-}
diff --git a/x11.go b/x11.go
deleted file mode 100644
index d25155e..0000000
--- a/x11.go
+++ /dev/null
@@ -1,72 +0,0 @@
-package main
-
-import "C"
-import (
- "errors"
- "unsafe"
-)
-
-//#include
-//#include
-//#cgo linux LDFLAGS: -lxcb
-import "C"
-
-const (
- xcbHostModeInsert = C.XCB_HOST_MODE_INSERT
- xcbHostModeDelete = C.XCB_HOST_MODE_DELETE
-
- xcbFamilyInternet = C.XCB_FAMILY_INTERNET
- xcbFamilyDecnet = C.XCB_FAMILY_DECNET
- xcbFamilyChaos = C.XCB_FAMILY_CHAOS
- xcbFamilyServerInterpreted = C.XCB_FAMILY_SERVER_INTERPRETED
- xcbFamilyInternet6 = C.XCB_FAMILY_INTERNET_6
-)
-
-func changeHosts(mode, family C.uint8_t, address string) error {
- var c *C.xcb_connection_t
- c = C.xcb_connect(nil, nil)
- defer C.xcb_disconnect(c)
-
- if err := xcbHandleConnectionError(c); err != nil {
- return err
- }
-
- addr := C.CString(address)
- cookie := C.xcb_change_hosts_checked(c, mode, family, C.ushort(len(address)), (*C.uchar)(unsafe.Pointer(addr)))
- C.free(unsafe.Pointer(addr))
-
- if err := xcbHandleConnectionError(c); err != nil {
- return err
- }
-
- e := C.xcb_request_check(c, cookie)
- if e != nil {
- defer C.free(unsafe.Pointer(e))
- return errors.New("xcb_change_hosts() failed")
- }
-
- return nil
-}
-
-func xcbHandleConnectionError(c *C.xcb_connection_t) error {
- if errno := C.xcb_connection_has_error(c); errno != 0 {
- switch errno {
- case C.XCB_CONN_ERROR:
- return errors.New("connection error")
- case C.XCB_CONN_CLOSED_EXT_NOTSUPPORTED:
- return errors.New("extension not supported")
- case C.XCB_CONN_CLOSED_MEM_INSUFFICIENT:
- return errors.New("memory not available")
- case C.XCB_CONN_CLOSED_REQ_LEN_EXCEED:
- return errors.New("request length exceeded")
- case C.XCB_CONN_CLOSED_PARSE_ERR:
- return errors.New("invalid display string")
- case C.XCB_CONN_CLOSED_INVALID_SCREEN:
- return errors.New("server has no screen matching display")
- default:
- return errors.New("generic X11 failure")
- }
- } else {
- return nil
- }
-}