aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/start.go
diff options
context:
space:
mode:
Diffstat (limited to 'internal/app/start.go')
-rw-r--r--internal/app/start.go187
1 files changed, 187 insertions, 0 deletions
diff --git a/internal/app/start.go b/internal/app/start.go
new file mode 100644
index 00000000..b335b68a
--- /dev/null
+++ b/internal/app/start.go
@@ -0,0 +1,187 @@
+package app
+
+import (
+ "errors"
+ "os"
+ "os/exec"
+ "strconv"
+ "time"
+
+ "git.ophivana.moe/cat/fortify/internal/state"
+ "git.ophivana.moe/cat/fortify/internal/verbose"
+)
+
+type (
+ // ProcessError encapsulates errors returned by starting *exec.Cmd
+ ProcessError BaseError
+)
+
+// Start starts the fortified child
+func (a *app) Start() error {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+
+ if err := a.seal.sys.commit(); err != nil {
+ return err
+ }
+
+ // select command builder
+ var commandBuilder func() (args []string)
+ switch a.seal.launchOption {
+ case LaunchMethodSudo:
+ commandBuilder = a.commandBuilderSudo
+ case LaunchMethodBwrap:
+ commandBuilder = a.commandBuilderBwrap
+ case LaunchMethodMachineCtl:
+ commandBuilder = a.commandBuilderMachineCtl
+ default:
+ panic("unreachable")
+ }
+
+ // configure child process
+ a.cmd = exec.Command(a.seal.toolPath, commandBuilder()...)
+ a.cmd.Env = []string{}
+ a.cmd.Stdin = os.Stdin
+ a.cmd.Stdout = os.Stdout
+ a.cmd.Stderr = os.Stderr
+ a.cmd.Dir = a.seal.RunDirPath
+
+ // start child process
+ verbose.Println("starting main process:", a.cmd)
+ if err := a.cmd.Start(); err != nil {
+ return (*ProcessError)(wrapError(err, "cannot start process:", err))
+ }
+ startTime := time.Now().UTC()
+
+ // create process state
+ sd := state.State{
+ PID: a.cmd.Process.Pid,
+ Command: a.seal.command,
+ Capability: a.seal.et,
+ Launcher: a.seal.toolPath,
+ Argv: a.cmd.Args,
+ Time: startTime,
+ }
+
+ // register process state
+ var e = new(StateStoreError)
+ e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
+ e.InnerErr = b.Save(&sd)
+ })
+ return e.equiv("cannot save process state:", e)
+}
+
+// StateStoreError is returned for a failed state save
+type StateStoreError struct {
+ // whether inner function was called
+ Inner bool
+ // error returned by state.Store Do method
+ DoErr error
+ // error returned by state.Backend Save method
+ InnerErr error
+ // any other errors needing to be tracked
+ Err error
+}
+
+func (e *StateStoreError) equiv(a ...any) error {
+ if e.Inner == true && e.DoErr == nil && e.InnerErr == nil && e.Err == nil {
+ return nil
+ } else {
+ return wrapError(e, a...)
+ }
+}
+
+func (e *StateStoreError) Error() string {
+ if e.Inner && e.InnerErr != nil {
+ return e.InnerErr.Error()
+ }
+
+ if e.DoErr != nil {
+ return e.DoErr.Error()
+ }
+
+ if e.Err != nil {
+ return e.Err.Error()
+ }
+
+ return "(nil)"
+}
+
+func (e *StateStoreError) Unwrap() (errs []error) {
+ errs = make([]error, 0, 3)
+ if e.DoErr != nil {
+ errs = append(errs, e.DoErr)
+ }
+ if e.InnerErr != nil {
+ errs = append(errs, e.InnerErr)
+ }
+ if e.Err != nil {
+ errs = append(errs, e.Err)
+ }
+ return
+}
+
+type RevertCompoundError interface {
+ Error() string
+ Unwrap() []error
+}
+
+func (a *app) Wait() (int, error) {
+ a.lock.Lock()
+ defer a.lock.Unlock()
+
+ var r int
+
+ // wait for process and resolve exit code
+ if err := a.cmd.Wait(); err != nil {
+ var exitError *exec.ExitError
+ if !errors.As(err, &exitError) {
+ // should be unreachable
+ a.wait = err
+ }
+
+ // store non-zero return code
+ r = exitError.ExitCode()
+ } else {
+ r = a.cmd.ProcessState.ExitCode()
+ }
+
+ verbose.Println("process", strconv.Itoa(a.cmd.Process.Pid), "exited with exit code", r)
+
+ // update store and revert app setup transaction
+ e := new(StateStoreError)
+ e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
+ e.InnerErr = func() error {
+ // destroy defunct state entry
+ if err := b.Destroy(a.cmd.Process.Pid); err != nil {
+ return err
+ }
+
+ var global bool
+
+ // measure remaining state entries
+ if l, err := b.Len(); err != nil {
+ return err
+ } else {
+ // clean up global modifications if we're the last launcher alive
+ global = l == 0
+
+ if !global {
+ verbose.Printf("found %d active launchers, cleaning up without globals\n", l)
+ } else {
+ verbose.Println("no other launchers active, will clean up globals")
+ }
+ }
+
+ // FIXME: depending on exit sequence, some parts of the transaction never gets reverted
+ if err := a.seal.sys.revert(global); err != nil {
+ return err.(RevertCompoundError)
+ }
+
+ return nil
+ }()
+ })
+
+ e.Err = a.seal.store.Close()
+ return r, e.equiv("error returned during cleanup:", e)
+}