aboutsummaryrefslogtreecommitdiffhomepage
path: root/internal/app/process.go
diff options
context:
space:
mode:
authorOphestra <cat@gensokyo.uk>2025-10-29 04:32:43 +0900
committerOphestra <cat@gensokyo.uk>2025-10-29 04:33:13 +0900
commita0b4e47acc228e48165966d8e139277b8f6f450e (patch)
treed363a6781536ce946aa445271ca1d3df587e6031 /internal/app/process.go
parenta52f7038e5a607dd0901244abb791c96deaf9c2d (diff)
internal/outcome: rename from app
This is less ambiguous, and more accurately describes the purpose of the package. Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/app/process.go')
-rw-r--r--internal/app/process.go321
1 files changed, 0 insertions, 321 deletions
diff --git a/internal/app/process.go b/internal/app/process.go
deleted file mode 100644
index 7873109a..00000000
--- a/internal/app/process.go
+++ /dev/null
@@ -1,321 +0,0 @@
-package app
-
-import (
- "context"
- "encoding/gob"
- "errors"
- "log"
- "os"
- "os/exec"
- "strconv"
- "strings"
- "syscall"
- "time"
-
- "hakurei.app/container"
- "hakurei.app/container/fhs"
- "hakurei.app/hst"
- "hakurei.app/internal"
- "hakurei.app/internal/app/state"
- "hakurei.app/message"
- "hakurei.app/system"
-)
-
-// Duration to wait for shim to exit on top of container WaitDelay.
-const shimWaitTimeout = 5 * time.Second
-
-// mainState holds persistent state bound to outcome.main.
-type mainState struct {
- // done is whether beforeExit has been called already.
- done bool
-
- // Time is the exact point in time where the process was created.
- // Location must be set to UTC.
- //
- // Time is nil if no process was ever created.
- Time *time.Time
-
- store state.Store
- cancel context.CancelFunc
- cmd *exec.Cmd
- cmdWait chan error
-
- k *outcome
- message.Msg
- uintptr
-}
-
-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
-)
-
-// 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()
-
- if isFault && ms.cancel != nil {
- ms.cancel()
- }
-
- var hasErr bool
- // updates hasErr but does not terminate
- perror := func(err error, message string) {
- hasErr = true
- printMessageError("cannot "+message+":", err)
- }
- exitCode := 1
- defer func() {
- if hasErr {
- os.Exit(exitCode)
- }
- }()
-
- // this also handles wait for a non-fault termination
- if ms.cmd != nil && ms.cmdWait != nil {
- waitDone := 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(waitDone) }()
-
- select {
- case err := <-ms.cmdWait:
- wstatus, ok := ms.cmd.ProcessState.Sys().(syscall.WaitStatus)
- if ok {
- if v := wstatus.ExitStatus(); v != 0 {
- hasErr = true
- exitCode = v
- }
- }
-
- if ms.IsVerbose() {
- if !ok {
- if err != nil {
- ms.Verbosef("wait: %v", err)
- }
- } else {
- switch {
- case wstatus.Exited():
- ms.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus())
-
- case wstatus.CoreDump():
- ms.Verbosef("process %d dumped core", ms.cmd.Process.Pid)
-
- case wstatus.Signaled():
- ms.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal())
-
- default:
- ms.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus)
- }
- }
- }
-
- case <-waitDone:
- 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.
- log.Printf("process %d did not terminate", ms.cmd.Process.Pid)
- }
-
- ms.Resume()
- }
-
- if ms.uintptr&mainNeedsRevert != 0 {
- if ok, err := ms.store.Do(ms.k.state.identity.unwrap(), func(c state.Cursor) {
- if ms.uintptr&mainNeedsDestroy != 0 {
- if err := c.Destroy(ms.k.state.id.unwrap()); 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
- ec := system.Process
- if revertErr := ms.k.sys.Revert((*system.Criteria)(&ec)); revertErr != nil {
- var joinError interface {
- Unwrap() []error
- error
- }
- if !errors.As(revertErr, &joinError) || joinError == nil {
- perror(revertErr, "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 {
- log.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")
- }
- }
- } else if ms.uintptr&mainNeedsDestroy != 0 {
- panic("unreachable")
- }
-}
-
-// fatal calls printMessageError, performs necessary cleanup, followed by a call to [os.Exit](1).
-func (ms mainState) fatal(fallback string, ferr error) {
- printMessageError(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.active.CompareAndSwap(false, true) {
- panic("outcome: attempted to run twice")
- }
-
- 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 = state.NewMulti(msg, k.state.sc.RunDirPath)
-
- ctx, cancel := context.WithCancel(k.ctx)
- defer cancel()
- ms.cancel = cancel
-
- ms.cmd = exec.CommandContext(ctx, hsuPath.String())
- ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr
- ms.cmd.Dir = fhs.Root // container init enters final working directory
- // shim runs in the same session as monitor; see shim.go for behaviour
- ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) }
-
- var e *gob.Encoder
- if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil {
- ms.fatal("cannot create shim setup pipe:", err)
- } else {
- e = encoder
- ms.cmd.Env = []string{
- // passed through to shim by hsu
- shimEnv + "=" + strconv.Itoa(fd),
- // interpreted by hsu
- "HAKUREI_IDENTITY=" + k.state.identity.String(),
- }
- }
-
- if len(k.supp) > 0 {
- msg.Verbosef("attaching supplementary group ids %s", k.supp)
- // interpreted by hsu
- ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(k.supp, " "))
- }
-
- msg.Verbosef("setuid helper at %s", hsuPath)
- msg.Suspend()
- if err := ms.cmd.Start(); err != nil {
- ms.fatal("cannot start setuid wrapper:", err)
- }
- startTime := time.Now().UTC()
- ms.cmdWait = make(chan error, 1)
- // this ties context back to the life of the process
- go func() { ms.cmdWait <- ms.cmd.Wait(); cancel() }()
- ms.Time = &startTime
-
- // unfortunately the I/O here cannot be directly canceled;
- // the cancellation path leads to fatal in this case so that is fine
- select {
- case err := <-func() (setupErr chan error) {
- setupErr = make(chan error, 1)
- go func() { setupErr <- e.Encode(k.state) }()
- return
- }():
- if err != nil {
- msg.Resume()
- ms.fatal("cannot transmit shim config:", err)
- }
-
- case <-ctx.Done():
- msg.Resume()
- ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err()))
- }
-
- // shim accepted setup payload, create process state
- if ok, err := ms.store.Do(k.state.identity.unwrap(), func(c state.Cursor) {
- if err := c.Save(&hst.State{
- ID: k.state.id.unwrap(),
- PID: os.Getpid(),
- ShimPID: ms.cmd.Process.Pid,
- Config: k.config,
- Time: *ms.Time,
- }); 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)
- }
- }
- // 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)
-}
-
-// printMessageError prints the error message according to [message.GetMessage],
-// or fallback prepended to err if an error message is not available.
-func printMessageError(fallback string, err error) {
- m, ok := message.GetMessage(err)
- if !ok {
- log.Println(fallback, err)
- return
- }
-
- log.Print(m)
-}