aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-11-02 03:19:18 +0900
committerOphestra <cat@gensokyo.uk>2025-11-02 04:25:45 +0900
commit2ba599b3997f9d4270e533ee0d0e7c235414b31a (patch)
treeb777ba5c1080e81e5ba28aa33d41c8e5b0f9a665
parentd3d341712560e7590997cc04ee48098e81381d90 (diff)
internal/outcome/process: use new store interface
This change also spawns shim before committing system state, leaving it blocking on the setup pipe. The internal/outcome/process structure is also entirely reworked to be much more readable and less error-prone, while enabling basic performance measurements. A long-standing bug where segment lock is not held during Commit is also resolved. Closes #19. Signed-off-by: Ophestra <cat@gensokyo.uk>
-rw-r--r--internal/outcome/process.go446
1 files changed, 249 insertions, 197 deletions
diff --git a/internal/outcome/process.go b/internal/outcome/process.go
index ff2fcaf3..399e7cb1 100644
--- a/internal/outcome/process.go
+++ b/internal/outcome/process.go
@@ -4,6 +4,8 @@ import (
"context"
"encoding/gob"
"errors"
+ "iter"
+ "math"
"os"
"os/exec"
"strconv"
@@ -28,243 +30,290 @@ const (
shimSetupTimeout = 5 * time.Second
)
-// mainState holds persistent state bound to outcome.main.
-type mainState struct {
- // done is whether beforeExit has been called already.
- done bool
+// main carries out outcome and terminates. main does not return.
+func (k *outcome) main(msg message.Msg) {
+ if k.ctx == nil || k.sys == nil || k.state == nil {
+ panic("outcome: did not finalise")
+ }
- // Populated on successful hsu startup.
- cmd *exec.Cmd
- // Cancels cmd, must be populated before cmd is populated.
- cancel context.CancelFunc
+ // read comp value early for early failure
+ hsuPath := internal.MustHsuPath()
- store store.Compat
+ const (
+ // transitions to processCommit, or processFinal on failure
+ processStart = iota
+ // transitions to processServe, or processLifecycle on failure
+ processCommit
+ // transitions to processLifecycle only
+ processServe
+ // transitions to processCleanup only
+ processLifecycle
+ // transitions to processFinal only
+ processCleanup
+ // execution terminates, must be the final state
+ processFinal
+ )
- k *outcome
- message.Msg
- uintptr
-}
+ // for the shim process
+ ctx, cancel := context.WithCancel(k.ctx)
+ defer cancel()
-const (
- // mainNeedsRevert indicates the call to Commit has succeeded.
- mainNeedsRevert uintptr = 1 << iota
- // mainNeedsDestroy indicates the instance state entry is present in the store.
- mainNeedsDestroy
-)
+ var (
+ // state for next iteration
+ processState uintptr = processStart
+ // current state, must not be mutated directly
+ processStateCur uintptr = math.MaxUint
+ // point in time the current iteration began
+ processTime time.Time
-// beforeExit must be called immediately before a call to [os.Exit].
-func (ms mainState) beforeExit(isFault bool) {
- if ms.done {
- panic("attempting to call beforeExit twice")
- }
- ms.done = true
- defer ms.BeforeExit()
+ // whether sys is currently in between a call to Commit and Revert
+ isBeforeRevert bool
- if isFault && ms.cancel != nil {
- ms.cancel()
- }
+ // initialised during processStart if successful
+ handle *store.Handle
+ // initialised during processServe if state is saved
+ entryHandle *store.EntryHandle
- var hasErr bool
- // updates hasErr but does not terminate
- perror := func(err error, message string) {
- hasErr = true
- printMessageError(ms.GetLogger().Println, "cannot "+message+":", err)
- }
- exitCode := 1
- defer func() {
- if hasErr {
- os.Exit(exitCode)
- }
- }()
+ // can be set in any state, used in processFinal
+ exitCode int
- // this also handles wait for a non-fault termination
- if ms.cmd != nil {
- select {
- case err := <-func() chan error { w := make(chan error, 1); go func() { w <- ms.cmd.Wait(); ms.cancel() }(); return w }():
- wstatus, ok := ms.cmd.ProcessState.Sys().(syscall.WaitStatus)
- if ok {
- if v := wstatus.ExitStatus(); v != 0 {
- hasErr = true
- exitCode = v
+ // shim process startup time,
+ // populated in processStart, accessed by processServe
+ startTime time.Time
+ // shim process as target uid,
+ // populated in processStart, accessed by processServe
+ shimCmd *exec.Cmd
+ // write end of shim setup pipe,
+ // populated in processStart, accessed by processServe
+ shimPipe *os.File
+
+ // perror cancels ctx and prints an error message
+ perror = func(err error, message string) {
+ cancel()
+ if shimPipe != nil {
+ if closeErr := shimPipe.Close(); closeErr != nil {
+ msg.Verbose(closeErr.Error())
}
+ shimPipe = nil
}
+ if exitCode == 0 {
+ exitCode = 1
+ }
+ printMessageError(msg.GetLogger().Println, "cannot "+message+":", err)
+ }
- if ms.IsVerbose() {
- if !ok {
- if err != nil {
- ms.Verbosef("wait: %v", err)
+ // perrorFatal cancels ctx, prints an error message, and sets the next state
+ perrorFatal = func(err error, message string, newState uintptr) {
+ perror(err, message)
+ processState = newState
+ }
+ )
+
+ for {
+ var processTimePrev time.Time
+ processTimePrev, processTime = processTime, time.Now()
+ var processStatePrev uintptr
+ processStatePrev, processStateCur = processStateCur, processState
+
+ if !processTimePrev.IsZero() && processStatePrev != processLifecycle {
+ msg.Verbosef("state %d took %d ms", processStatePrev, processTime.Sub(processTimePrev).Milliseconds())
+ }
+
+ switch processState {
+ case processStart:
+ if h, err := store.New(k.state.sc.RunDirPath.Append("state")).Handle(k.state.identity.unwrap()); err != nil {
+ perrorFatal(err, "obtain store segment handle", processFinal)
+ continue
+ } else {
+ handle = h
+ }
+
+ cmd, f, err := k.start(ctx, msg, hsuPath, &startTime)
+ if err != nil {
+ perrorFatal(err, "start shim", processFinal)
+ continue
+ } else {
+ shimCmd, shimPipe = cmd, f
+ }
+
+ processState = processCommit
+
+ case processCommit:
+ if isBeforeRevert {
+ perrorFatal(newWithMessage("invalid transition to commit state"), "commit", processLifecycle)
+ continue
+ }
+
+ unlock, err := handle.Lock()
+ if err != nil {
+ perrorFatal(err, "acquire lock on store segment", processLifecycle)
+ continue
+ }
+ if entryHandle, err = handle.Save(&hst.State{
+ ID: k.state.id.unwrap(),
+ PID: os.Getpid(),
+ ShimPID: shimCmd.Process.Pid,
+ Config: k.config,
+ Time: startTime,
+ }); err != nil {
+ unlock()
+ // transition here to avoid the commit/revert cycle on the doomed instance
+ perrorFatal(err, "save instance state", processLifecycle)
+ continue
+ }
+
+ err = k.sys.Commit()
+ unlock()
+ if err != nil {
+ perrorFatal(err, "commit system setup", processLifecycle)
+ continue
+ }
+ isBeforeRevert = true
+
+ processState = processServe
+
+ case processServe:
+ // this state transition to processLifecycle only
+ processState = processLifecycle
+
+ // this starts the container, system setup must complete before this point
+ if err := serveShim(msg, shimPipe, k.state); err != nil {
+ perror(err, "serve shim payload")
+ continue
+ } else {
+ shimPipe = nil // this is already closed by serveShim
+ }
+
+ case processLifecycle:
+ // this state transition to processCleanup only
+ processState = processCleanup
+
+ msg.Suspend()
+ select {
+ case err := <-func() chan error { w := make(chan error, 1); go func() { w <- shimCmd.Wait(); cancel() }(); return w }():
+ wstatus, ok := shimCmd.ProcessState.Sys().(syscall.WaitStatus)
+ if ok {
+ if v := wstatus.ExitStatus(); v != 0 {
+ exitCode = v
}
- } else {
- switch {
- case wstatus.Exited():
- ms.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
+ }
+
+ if msg.IsVerbose() {
+ if !ok {
+ if err != nil {
+ msg.Verbosef("wait: %v", err)
+ }
+ } else {
+ switch {
+ case wstatus.Exited():
+ msg.Verbosef("process %d exited with code %d", shimCmd.Process.Pid, wstatus.ExitStatus())
- case wstatus.CoreDump():
- ms.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
+ case wstatus.CoreDump():
+ msg.Verbosef("process %d dumped core", shimCmd.Process.Pid)
- case wstatus.Signaled():
- ms.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
+ case wstatus.Signaled():
+ msg.Verbosef("process %d got %s", shimCmd.Process.Pid, wstatus.Signal())
- default:
- ms.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
+ default:
+ msg.Verbosef("process %d exited with status %#x", shimCmd.Process.Pid, wstatus)
+ }
}
}
+
+ case <-func() chan struct{} {
+ w := make(chan struct{})
+ // this ties processLifecycle to ctx with the additional compensated timeout duration
+ // to allow transition to the next state on a locked up shim
+ go func() { <-ctx.Done(); time.Sleep(k.state.Shim.WaitDelay + shimWaitTimeout); close(w) }()
+ return w
+ }():
+ // this is only reachable when wait did not return within shimWaitTimeout, after its WaitDelay has elapsed.
+ // This is different from the container failing to terminate within its timeout period, as that is enforced
+ // by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
+ msg.GetLogger().Printf("process %d did not terminate", shimCmd.Process.Pid)
}
+ msg.Resume()
- case <-func() chan struct{} {
- w := make(chan struct{})
- // this ties waitDone to ctx with the additional compensated timeout duration
- go func() { <-ms.k.ctx.Done(); time.Sleep(ms.k.state.Shim.WaitDelay + shimWaitTimeout); close(w) }()
- return w
- }():
- ms.Resume()
- // this is only reachable when shim did not exit within shimWaitTimeout, after its WaitDelay has elapsed.
- // This is different from the container failing to terminate within its timeout period, as that is enforced
- // by the shim. This path is instead reached when there is a lockup in shim preventing it from completing.
- ms.GetLogger().Printf("process %d did not terminate", ms.cmd.Process.Pid)
- }
+ case processCleanup:
+ // this state transition to processFinal only
+ processState = processFinal
- ms.Resume()
- }
+ unlock, err := handle.Lock()
+ if err != nil {
+ perror(err, "acquire lock on store segment")
+ }
- if ms.uintptr&mainNeedsRevert != 0 {
- if ok, err := ms.store.Do(ms.k.state.identity.unwrap(), func(c store.Cursor) {
- if ms.uintptr&mainNeedsDestroy != 0 {
- if err := c.Destroy(ms.k.state.id.unwrap()); err != nil {
+ if entryHandle != nil {
+ if err = entryHandle.Destroy(); err != nil {
perror(err, "destroy state entry")
}
}
- var rt hst.Enablement
- if states, err := c.Load(); err != nil {
- // it is impossible to continue from this point;
- // revert per-process state here to limit damage
+ if isBeforeRevert {
ec := system.Process
- if revertErr := ms.k.sys.Revert((*system.Criteria)(&ec)); revertErr != nil {
+
+ var entries iter.Seq[*store.EntryHandle]
+ if entries, _, err = handle.Entries(); err != nil {
+ // it is impossible to continue from this point,
+ // per-process state will be reverted to limit damage
+ perror(err, "read store segment entries")
+ } else {
+ // accumulate enablements of remaining instances
+ var (
+ // alive enablement bits
+ rt hst.Enablement
+ // alive instance count
+ n int
+ )
+ for eh := range entries {
+ var et hst.Enablement
+ if et, err = eh.Load(nil); err != nil {
+ perror(err, "read state header of instance "+eh.ID.String())
+ } else {
+ rt |= et
+ n++
+ }
+ }
+
+ if n == 0 {
+ ec |= system.User
+ } else {
+ msg.Verbosef("found %d instances, cleaning up without user-scoped operations", n)
+ }
+ ec |= rt ^ (hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse)
+ if msg.IsVerbose() {
+ if ec > 0 {
+ msg.Verbose("reverting operations scope", system.TypeString(ec))
+ }
+ }
+ }
+
+ if err = k.sys.Revert((*system.Criteria)(&ec)); err != nil {
var joinError interface {
Unwrap() []error
error
}
- if !errors.As(revertErr, &joinError) || joinError == nil {
- perror(revertErr, "revert system setup")
+ if !errors.As(err, &joinError) || joinError == nil {
+ perror(err, "revert system setup")
} else {
for _, v := range joinError.Unwrap() {
perror(v, "revert system setup step")
}
}
}
- perror(err, "load instance states")
- } else {
- ec := system.Process
- if l := len(states); l == 0 {
- ec |= system.User
- } else {
- ms.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
- }
-
- // accumulate enablements of remaining launchers
- for i, s := range states {
- if s.Config != nil {
- rt |= s.Config.Enablements.Unwrap()
- } else {
- ms.GetLogger().Printf("state entry %d does not contain config", i)
- }
- }
-
- ec |= rt ^ (hst.EWayland | hst.EX11 | hst.EDBus | hst.EPulse)
- if ms.IsVerbose() {
- if ec > 0 {
- ms.Verbose("reverting operations scope", system.TypeString(ec))
- }
- }
-
- if err = ms.k.sys.Revert((*system.Criteria)(&ec)); err != nil {
- perror(err, "revert system setup")
- }
- }
- }); err != nil {
- if ok {
- perror(err, "unlock state store")
- } else {
- perror(err, "open state store")
+ isBeforeRevert = false
}
- }
- } else if ms.uintptr&mainNeedsDestroy != 0 {
- panic("unreachable")
- }
-}
+ unlock()
-// fatal calls printMessageError, performs necessary cleanup, followed by a call to [os.Exit](1).
-func (ms mainState) fatal(fallback string, ferr error) {
- printMessageError(ms.GetLogger().Println, fallback, ferr)
- ms.beforeExit(true)
- os.Exit(1)
-}
-
-// main carries out outcome and terminates. main does not return.
-func (k *outcome) main(msg message.Msg) {
- if k.ctx == nil || k.sys == nil || k.state == nil {
- panic("outcome: did not finalise")
- }
-
- // read comp value early for early failure
- hsuPath := internal.MustHsuPath()
-
- // ms.beforeExit required beyond this point
- ms := mainState{Msg: msg, k: k}
-
- if err := k.sys.Commit(); err != nil {
- ms.fatal("cannot commit system setup:", err)
- }
- ms.uintptr |= mainNeedsRevert
- ms.store = store.NewMulti(msg, k.state.sc.RunDirPath)
-
- ctx, cancel := context.WithCancel(k.ctx)
- defer cancel()
- ms.cancel = cancel
-
- // shim starts and blocks on setup payload before container is started
- var (
- startTime time.Time
- shimPipe *os.File
- )
- if cmd, f, err := k.start(ctx, msg, hsuPath, &startTime); err != nil {
- ms.fatal("cannot start shim:", err)
- panic("unreachable")
- } else {
- ms.cmd, shimPipe = cmd, f
- }
-
- // this starts the container, system setup must complete before this point
- if err := serveShim(msg, shimPipe, k.state); err != nil {
- ms.fatal("cannot serve shim payload:", err)
- }
+ case processFinal:
+ msg.BeforeExit()
+ os.Exit(exitCode)
- // shim accepted setup payload, create process state
- if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c store.Cursor) {
- if err := c.Save(&hst.State{
- ID: k.state.id.unwrap(),
- PID: os.Getpid(),
- ShimPID: ms.cmd.Process.Pid,
- Config: k.config,
- Time: startTime,
- }); err != nil {
- ms.fatal("cannot save state entry:", err)
- }
- }); err != nil {
- if ok {
- ms.uintptr |= mainNeedsDestroy
- ms.fatal("cannot unlock state store:", err)
- } else {
- ms.fatal("cannot open state store:", err)
+ default: // not reached
+ k.fatalf("invalid transition from state %d to %d", processStatePrev, processState)
+ panic("unreachable")
}
}
- // state in store at this point, destroy defunct state entry on termination
- ms.uintptr |= mainNeedsDestroy
-
- // beforeExit ties shim process to context
- ms.beforeExit(false)
- os.Exit(0)
}
// start starts the shim via cmd/hsu.
@@ -301,7 +350,6 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
}
msg.Verbosef("setuid helper at %s", hsuPath)
- msg.Suspend()
if err := cmd.Start(); err != nil {
msg.Resume()
return cmd, shimPipe, &hst.AppError{Step: "start setuid wrapper", Err: err}
@@ -313,6 +361,10 @@ func (k *outcome) start(ctx context.Context, msg message.Msg,
// serveShim serves outcomeState through the shim setup pipe.
func serveShim(msg message.Msg, shimPipe *os.File, state *outcomeState) error {
+ if shimPipe == nil {
+ return newWithMessage("shim pipe not available")
+ }
+
if err := shimPipe.SetDeadline(time.Now().Add(shimSetupTimeout)); err != nil {
msg.Verbose(err.Error())
}