diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-09-24 13:26:30 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-09-24 13:37:38 +0900 |
| commit | b99c63337df4068b36d7d8de2ae79fd274172977 (patch) | |
| tree | 81d2453857b7d6328db6545f096181b771c24cc3 /internal | |
| parent | f09133a224fabf2d16c7790654e39068cb59450f (diff) | |
internal/app: do not return from shim start
The whole RunState ugliness and the other horrendous error handling conditions for internal/app come from an old design proposal for maintaining all app containers under the same daemon process for a user. The proposal was ultimately rejected but the implementation remained. It is removed here to alleviate internal/app from much of its ugliness and unreadability.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal')
| -rw-r--r-- | internal/app/errors.go | 184 | ||||
| -rw-r--r-- | internal/app/process.go | 389 | ||||
| -rw-r--r-- | internal/app/seal.go | 44 | ||||
| -rw-r--r-- | internal/app/state/multi.go | 10 | ||||
| -rw-r--r-- | internal/app/state/state.go | 2 | ||||
| -rw-r--r-- | internal/sys/hsu.go | 23 | ||||
| -rw-r--r-- | internal/sys/interface.go | 10 | ||||
| -rw-r--r-- | internal/sys/std.go | 28 |
8 files changed, 297 insertions, 393 deletions
diff --git a/internal/app/errors.go b/internal/app/errors.go deleted file mode 100644 index f7fa2b1c..00000000 --- a/internal/app/errors.go +++ /dev/null @@ -1,184 +0,0 @@ -package app - -import ( - "errors" - "log" - - "hakurei.app/container" - "hakurei.app/hst" - "hakurei.app/internal/hlog" - "hakurei.app/internal/sys" -) - -// PrintRunStateErr prints an error message via [log] if runErr is not nil, and returns an appropriate exit code. -// -// TODO(ophestra): remove this function once RunState has been replaced -func PrintRunStateErr(rs *RunState, runErr error) (code int) { - code = rs.ExitStatus() - - if runErr != nil { - if rs.Time == nil { - // no process has been created - printMessageError("cannot start app:", runErr) - } else { - if m, ok := container.GetErrorMessage(runErr); !ok { - // catch-all for unexpected errors - log.Println("run returned error:", runErr) - } else { - var se *StateStoreError - if !errors.As(runErr, &se) { - // this could only be returned from a shim setup failure path - log.Print(m) - } else { - // InnerErr is returned by c.Save(&sd, seal.ct), and are always unwrapped - printMessageError("error returned during revert:", - &hst.AppError{Step: "save process state", Err: se.InnerErr}) - } - } - } - - if code == 0 { - code = 126 - } - } - - if rs.RevertErr != nil { - var stateStoreError *StateStoreError - if !errors.As(rs.RevertErr, &stateStoreError) || stateStoreError == nil { - printMessageError("cannot clean up:", rs.RevertErr) - goto out - } - - if stateStoreError.Errs != nil { - if len(stateStoreError.Errs) == 2 { // storeErr.save(revertErr, store.Close()) - if stateStoreError.Errs[0] != nil { // revertErr is MessageError joined by errors.Join - var joinedErrors interface { - Unwrap() []error - error - } - if !errors.As(stateStoreError.Errs[0], &joinedErrors) { - printMessageError("cannot revert:", stateStoreError.Errs[0]) - } else { - for _, err := range joinedErrors.Unwrap() { - if err != nil { - printMessageError("cannot revert:", err) - } - } - } - } - if stateStoreError.Errs[1] != nil { // store.Close() is joined by errors.Join - log.Printf("cannot close store: %v", stateStoreError.Errs[1]) - } - } else { - log.Printf("fault during cleanup: %v", errors.Join(stateStoreError.Errs...)) - } - } - - if stateStoreError.OpErr != nil { - log.Printf("blind revert due to store fault: %v", stateStoreError.OpErr) - } - - if stateStoreError.DoErr != nil { - printMessageError("state store operation unsuccessful:", stateStoreError.DoErr) - } - - if stateStoreError.Inner && stateStoreError.InnerErr != nil { - printMessageError("cannot destroy state entry:", stateStoreError.InnerErr) - } - - out: - if code == 0 { - code = 128 - } - } - if rs.WaitErr != nil { - hlog.Verbosef("wait: %v", rs.WaitErr) - } - return -} - -// TODO(ophestra): this duplicates code in cmd/hakurei/command.go, keep this up to date until removal -func printMessageError(fallback string, err error) { - // this indicates the error message has already reached stderr, outside the current process's control; - // this is only reached when hsu fails for any reason, as a second error message following hsu is confusing - if errors.Is(err, sys.ErrHsuAccess) { - hlog.Verbose("*"+fallback, err) - return - } - - m, ok := container.GetErrorMessage(err) - if !ok { - log.Println(fallback, err) - return - } - - log.Print(m) -} - -// StateStoreError is returned for a failed state save. -type StateStoreError struct { - // whether inner function was called - Inner bool - // returned by the Save/Destroy method of [state.Cursor] - InnerErr error - // returned by the Do method of [state.Store] - DoErr error - // stores an arbitrary store operation error - OpErr error - // stores arbitrary errors - Errs []error -} - -// save saves arbitrary errors in [StateStoreError.Errs] once. -func (e *StateStoreError) save(errs ...error) { - if len(errs) == 0 || e.Errs != nil { - panic("invalid call to save") - } - e.Errs = errs -} - -// equiv returns an error that [StateStoreError] is equivalent to, including nil. -func (e *StateStoreError) equiv(step string) error { - if e.Inner && e.InnerErr == nil && e.DoErr == nil && e.OpErr == nil && errors.Join(e.Errs...) == nil { - return nil - } else { - return &hst.AppError{Step: step, Err: e} - } -} - -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.OpErr != nil { - return e.OpErr.Error() - } - if err := errors.Join(e.Errs...); err != nil { - return err.Error() - } - - // equiv nullifies e for values where this is reached - panic("unreachable") -} - -func (e *StateStoreError) Unwrap() (errs []error) { - errs = make([]error, 0, 3+len(e.Errs)) - if e.InnerErr != nil { - errs = append(errs, e.InnerErr) - } - if e.DoErr != nil { - errs = append(errs, e.DoErr) - } - if e.OpErr != nil { - errs = append(errs, e.OpErr) - } - for _, err := range e.Errs { - if err != nil { - errs = append(errs, err) - } - } - return -} diff --git a/internal/app/process.go b/internal/app/process.go index daa7832a..0ee123a1 100644 --- a/internal/app/process.go +++ b/internal/app/process.go @@ -13,7 +13,6 @@ import ( "time" "hakurei.app/container" - "hakurei.app/hst" "hakurei.app/internal" "hakurei.app/internal/app/state" "hakurei.app/internal/hlog" @@ -23,85 +22,166 @@ import ( // duration to wait for shim to exit, after container WaitDelay has elapsed. const shimWaitTimeout = 5 * time.Second -// ErrShimTimeout is returned 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 error is instead returned when there is a lockup in shim preventing it from completing. -var ErrShimTimeout = errors.New("shim did not exit") +// mainState holds persistent state bound to [Outcome.Main]. +type mainState struct { + // done is whether beforeExit has been called already. + done bool -// RunState stores the outcome of a call to [Outcome.Run]. -type RunState struct { // 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 - // RevertErr is stored by the deferred revert call. - RevertErr error - // WaitErr is the generic error value created by the standard library. - WaitErr error - syscall.WaitStatus + seal *Outcome + store state.Store + cancel context.CancelFunc + cmd *exec.Cmd + cmdWait chan error + + uintptr } -// setStart stores the current time in [RunState] once. -func (rs *RunState) setStart() { - if rs.Time != nil { - panic("attempted to store time twice") +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") } - now := time.Now().UTC() - rs.Time = &now -} + ms.done = true + defer hlog.BeforeExit() -// Run commits deferred system setup and starts the container. -func (seal *Outcome) Run(rs *RunState) error { - if !seal.f.CompareAndSwap(false, true) { - // Run does much more than just starting a process; calling it twice, even if the first call fails, will result - // in inconsistent state that is impossible to clean up; return here to limit damage and hopefully give the - // other Run a chance to return - return errors.New("outcome: attempted to run twice") + if isFault && ms.cancel != nil { + ms.cancel() } - if rs == nil { - panic("invalid state") + 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) + } + }() - // read comp value early to allow for early failure - hsuPath := internal.MustHsuPath() + // this also handles wait for a non-fault termination + if ms.cmd != nil && ms.cmdWait != nil { + waitDone := make(chan struct{}) + // TODO(ophestra): enforce this limit early so it does not have to be done twice + shimTimeoutCompensated := shimWaitTimeout + if ms.seal.waitDelay > MaxShimWaitDelay { + shimTimeoutCompensated += MaxShimWaitDelay + } else { + shimTimeoutCompensated += ms.seal.waitDelay + } + // this ties waitDone to ctx with the additional compensated timeout duration + go func() { <-ms.seal.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitDone) }() - if err := seal.sys.Commit(); err != nil { - return err + 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 hlog.Load() { + if !ok { + if err != nil { + hlog.Verbosef("wait: %v", err) + } + } else { + switch { + case wstatus.Exited(): + hlog.Verbosef("process %d exited with code %d", ms.cmd.Process.Pid, wstatus.ExitStatus()) + + case wstatus.CoreDump(): + hlog.Verbosef("process %d dumped core", ms.cmd.Process.Pid) + + case wstatus.Signaled(): + hlog.Verbosef("process %d got %s", ms.cmd.Process.Pid, wstatus.Signal()) + + default: + hlog.Verbosef("process %d exited with status %#x", ms.cmd.Process.Pid, wstatus) + } + } + } + + case <-waitDone: + hlog.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) + } + + hlog.Resume() + if ms.seal.sync != nil { + if err := ms.seal.sync.Close(); err != nil { + perror(err, "close wayland security context") + } + } + if ms.seal.dbusMsg != nil { + ms.seal.dbusMsg() + } } - store := state.NewMulti(seal.runDirPath.String()) - deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store - defer func() { - var revertErr error - storeErr := new(StateStoreError) - storeErr.Inner, storeErr.DoErr = store.Do(seal.user.identity.unwrap(), func(c state.Cursor) { - revertErr = func() error { - storeErr.InnerErr = deferredStoreFunc(c) - var rt system.Enablement + if ms.uintptr&mainNeedsRevert != 0 { + if ok, err := ms.store.Do(ms.seal.user.identity.unwrap(), func(c state.Cursor) { + if ms.uintptr&mainNeedsDestroy != 0 { + if err := c.Destroy(ms.seal.id.unwrap()); err != nil { + perror(err, "destroy state entry") + } + } + + var rt system.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 states, err := c.Load(); err != nil { - // revert per-process state here to limit damage - storeErr.OpErr = err - return seal.sys.Revert((*system.Criteria)(&ec)) - } else { - if l := len(states); l == 0 { - ec |= system.User + if revertErr := ms.seal.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 { - hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l) + 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 { + hlog.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) - } + // 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 ^ (system.EWayland | system.EX11 | system.EDBus | system.EPulse) if hlog.Load() { if ec > 0 { @@ -109,27 +189,70 @@ func (seal *Outcome) Run(rs *RunState) error { } } - return seal.sys.Revert((*system.Criteria)(&ec)) - }() - }) - storeErr.save(revertErr, store.Close()) - rs.RevertErr = storeErr.equiv("clean up") - }() + if err = ms.seal.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") + } + + if ms.store != nil { + if err := ms.store.Close(); err != nil { + perror(err, "close state store") + } + } +} + +// 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 commits deferred system setup, runs the container, reverts changes to the system, and terminates the program. +// Main does not return. +func (seal *Outcome) Main() { + if !seal.f.CompareAndSwap(false, true) { + panic("outcome: attempted to run twice") + } + + // read comp value early for early failure + hsuPath := internal.MustHsuPath() + + // ms.beforeExit required beyond this point + ms := &mainState{seal: seal} + + if err := seal.sys.Commit(); err != nil { + ms.fatal("cannot commit system setup:", err) + } + ms.uintptr |= mainNeedsRevert + ms.store = state.NewMulti(seal.runDirPath.String()) ctx, cancel := context.WithCancel(seal.ctx) defer cancel() - cmd := exec.CommandContext(ctx, hsuPath) - cmd.Stdin, cmd.Stdout, cmd.Stderr = os.Stdin, os.Stdout, os.Stderr - cmd.Dir = container.FHSRoot // container init enters final working directory + ms.cancel = cancel + + ms.cmd = exec.CommandContext(ctx, hsuPath) + ms.cmd.Stdin, ms.cmd.Stdout, ms.cmd.Stderr = os.Stdin, os.Stdout, os.Stderr + ms.cmd.Dir = container.FHSRoot // container init enters final working directory // shim runs in the same session as monitor; see shim.go for behaviour - cmd.Cancel = func() error { return cmd.Process.Signal(syscall.SIGCONT) } + ms.cmd.Cancel = func() error { return ms.cmd.Process.Signal(syscall.SIGCONT) } var e *gob.Encoder - if fd, encoder, err := container.Setup(&cmd.ExtraFiles); err != nil { - return &hst.AppError{Step: "create shim setup pipe", Err: err} + if fd, encoder, err := container.Setup(&ms.cmd.ExtraFiles); err != nil { + ms.fatal("cannot create shim setup pipe:", err) } else { e = encoder - cmd.Env = []string{ + ms.cmd.Env = []string{ // passed through to shim by hsu shimEnv + "=" + strconv.Itoa(fd), // interpreted by hsu @@ -140,102 +263,78 @@ func (seal *Outcome) Run(rs *RunState) error { if len(seal.user.supp) > 0 { hlog.Verbosef("attaching supplementary group ids %s", seal.user.supp) // interpreted by hsu - cmd.Env = append(cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " ")) + ms.cmd.Env = append(ms.cmd.Env, "HAKUREI_GROUPS="+strings.Join(seal.user.supp, " ")) } hlog.Verbosef("setuid helper at %s", hsuPath) hlog.Suspend() - if err := cmd.Start(); err != nil { - return &hst.AppError{Step: "start setuid wrapper", Err: err} + if err := ms.cmd.Start(); err != nil { + ms.fatal("cannot start setuid wrapper:", err) } - rs.setStart() - - // this prevents blocking forever on an early failure - waitErr, setupErr := make(chan error, 1), make(chan error, 1) - go func() { waitErr <- cmd.Wait(); cancel() }() - go func() { - setupErr <- e.Encode(&shimParams{ - os.Getpid(), - seal.waitDelay, - seal.container, - hlog.Load(), - }) - }() + 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 := <-setupErr: + case err := <-func() (setupErr chan error) { + setupErr = make(chan error, 1) + go func() { + setupErr <- e.Encode(&shimParams{ + os.Getpid(), + seal.waitDelay, + seal.container, + hlog.Load(), + }) + }() + return + }(): if err != nil { hlog.Resume() - return &hst.AppError{Step: "transmit shim config", Err: err} + ms.fatal("cannot transmit shim config:", err) } case <-ctx.Done(): hlog.Resume() - return newWithMessageError("shim setup canceled", syscall.ECANCELED) + ms.fatal("shim context canceled:", newWithMessageError("shim setup canceled", ctx.Err())) } - // returned after blocking on waitErr - var earlyStoreErr = new(StateStoreError) - { - // shim accepted setup payload, create process state - sd := state.State{ + // shim accepted setup payload, create process state + if ok, err := ms.store.Do(seal.user.identity.unwrap(), func(c state.Cursor) { + if err := c.Save(&state.State{ ID: seal.id.unwrap(), - PID: cmd.Process.Pid, - Time: *rs.Time, + PID: ms.cmd.Process.Pid, + Time: *ms.Time, + }, seal.ct); err != nil { + ms.fatal("cannot save state entry:", err) } - earlyStoreErr.Inner, earlyStoreErr.DoErr = store.Do(seal.user.identity.unwrap(), func(c state.Cursor) { - earlyStoreErr.InnerErr = c.Save(&sd, seal.ct) - }) - } - - // state in store at this point, destroy defunct state entry on return - deferredStoreFunc = func(c state.Cursor) error { return c.Destroy(seal.id.unwrap()) } - - waitTimeout := make(chan struct{}) - // TODO(ophestra): enforce this limit early so it does not have to be done twice - shimTimeoutCompensated := shimWaitTimeout - if seal.waitDelay > MaxShimWaitDelay { - shimTimeoutCompensated += MaxShimWaitDelay - } else { - shimTimeoutCompensated += seal.waitDelay - } - go func() { <-seal.ctx.Done(); time.Sleep(shimTimeoutCompensated); close(waitTimeout) }() - - select { - case rs.WaitErr = <-waitErr: - rs.WaitStatus = cmd.ProcessState.Sys().(syscall.WaitStatus) - if hlog.Load() { - switch { - case rs.Exited(): - hlog.Verbosef("process %d exited with code %d", cmd.Process.Pid, rs.ExitStatus()) - - case rs.CoreDump(): - hlog.Verbosef("process %d dumped core", cmd.Process.Pid) - - case rs.Signaled(): - hlog.Verbosef("process %d got %s", cmd.Process.Pid, rs.Signal()) - - default: - hlog.Verbosef("process %d exited with status %#x", cmd.Process.Pid, rs.WaitStatus) - } + }); err != nil { + if ok { + ms.uintptr |= mainNeedsDestroy + ms.fatal("cannot unlock state store:", err) + } else { + ms.fatal("cannot open state store:", err) } - - case <-waitTimeout: - rs.WaitErr = ErrShimTimeout - hlog.Resume() - // TODO(ophestra): verify this behaviour in vm tests - log.Printf("process %d did not terminate", cmd.Process.Pid) } + // state in store at this point, destroy defunct state entry on termination + ms.uintptr |= mainNeedsDestroy - hlog.Resume() - if seal.sync != nil { - if err := seal.sync.Close(); err != nil { - log.Printf("cannot close wayland security context: %v", err) - } - } - if seal.dbusMsg != nil { - seal.dbusMsg() + // beforeExit ties shim process to context + ms.beforeExit(false) + os.Exit(0) +} + +// printMessageError prints the error message according to [container.GetErrorMessage], +// or fallback prepended to err if an error message is not available. +func printMessageError(fallback string, err error) { + m, ok := container.GetErrorMessage(err) + if !ok { + log.Println(fallback, err) + return } - return earlyStoreErr.equiv("save process state") + log.Print(m) } diff --git a/internal/app/seal.go b/internal/app/seal.go index e6cccecc..cafd6915 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -119,7 +119,7 @@ type hsuUser struct { username string } -func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Config) error { +func (seal *Outcome) finalise(ctx context.Context, k sys.State, config *hst.Config) error { const ( home = "HOME" shell = "SHELL" @@ -176,19 +176,17 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co home: config.Home, username: config.Username, } + if seal.user.username == "" { seal.user.username = "chronos" } else if !isValidUsername(seal.user.username) { return newWithMessage(fmt.Sprintf("invalid user name %q", seal.user.username)) } - if u, err := sys.Uid(seal.user.identity.unwrap()); err != nil { - return err - } else { - seal.user.uid = newInt(u) - } + seal.user.uid = newInt(sys.MustUid(k, seal.user.identity.unwrap())) + seal.user.supp = make([]string, len(config.Groups)) for i, name := range config.Groups { - if g, err := sys.LookupGroup(name); err != nil { + if g, err := k.LookupGroup(name); err != nil { return newWithMessageError(fmt.Sprintf("unknown group %q", name), err) } else { seal.user.supp[i] = g.Gid @@ -201,7 +199,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co if config.Shell == nil { config.Shell = container.AbsFHSRoot.Append("bin", "sh") - s, _ := sys.LookupEnv(shell) + s, _ := k.LookupEnv(shell) if a, err := container.NewAbs(s); err == nil { config.Shell = a } @@ -210,7 +208,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // hsu clears the environment so resolve paths early if config.Path == nil { if len(config.Args) > 0 { - if p, err := sys.LookPath(config.Args[0]); err != nil { + if p, err := k.LookPath(config.Args[0]); err != nil { return &hst.AppError{Step: "look up executable file", Err: err} } else if config.Path, err = container.NewAbs(p); err != nil { return newWithMessageError(err.Error(), err) @@ -246,7 +244,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // hide nscd from container if present nscd := container.AbsFHSVar.Append("run/nscd") - if _, err := sys.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) { + if _, err := k.Stat(nscd.String()); !errors.Is(err, fs.ErrNotExist) { conf.Filesystem = append(conf.Filesystem, hst.FilesystemConfigJSON{FilesystemConfig: &hst.FSEphemeral{Target: nscd}}) } @@ -274,7 +272,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co { var uid, gid int var err error - seal.container, seal.env, err = newContainer(config.Container, sys, seal.id.String(), &uid, &gid) + seal.container, seal.env, err = newContainer(config.Container, k, seal.id.String(), &uid, &gid) seal.waitDelay = config.Container.WaitDelay if err != nil { return &hst.AppError{Step: "initialise container configuration", Err: err} @@ -298,7 +296,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co seal.env[xdgSessionClass] = "user" seal.env[xdgSessionType] = "tty" - share := &shareHost{seal: seal, sc: sys.Paths()} + share := &shareHost{seal: seal, sc: k.Paths()} seal.runDirPath = share.sc.RunDirPath seal.sys = system.New(seal.ctx, seal.user.uid.unwrap()) seal.sys.Ensure(share.sc.SharePath.String(), 0711) @@ -342,14 +340,14 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } // pass TERM for proper terminal I/O in initial process - if t, ok := sys.LookupEnv(term); ok { + if t, ok := k.LookupEnv(term); ok { seal.env[term] = t } if config.Enablements.Unwrap()&system.EWayland != 0 { // outer wayland socket (usually `/run/user/%d/wayland-%d`) var socketPath *container.Absolute - if name, ok := sys.LookupEnv(wayland.WaylandDisplay); !ok { + if name, ok := k.LookupEnv(wayland.WaylandDisplay); !ok { hlog.Verbose(wayland.WaylandDisplay + " is not set, assuming " + wayland.FallbackName) socketPath = share.sc.RuntimePath.Append(wayland.FallbackName) } else if a, err := container.NewAbs(name); err != nil { @@ -380,7 +378,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } if config.Enablements.Unwrap()&system.EX11 != 0 { - if d, ok := sys.LookupEnv(display); !ok { + if d, ok := k.LookupEnv(display); !ok { return newWithMessage("DISPLAY is not set") } else { socketDir := container.AbsFHSTmp.Append(".X11-unix") @@ -398,7 +396,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } } if socketPath != nil { - if _, err := sys.Stat(socketPath.String()); err != nil { + if _, err := k.Stat(socketPath.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: fmt.Sprintf("access X11 socket %q", socketPath), Err: err} } @@ -422,14 +420,14 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co // PulseAudio socket (usually `/run/user/%d/pulse/native`) pulseSocket := pulseRuntimeDir.Append("native") - if _, err := sys.Stat(pulseRuntimeDir.String()); err != nil { + if _, err := k.Stat(pulseRuntimeDir.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: fmt.Sprintf("access PulseAudio directory %q", pulseRuntimeDir), Err: err} } return newWithMessage(fmt.Sprintf("PulseAudio directory %q not found", pulseRuntimeDir)) } - if s, err := sys.Stat(pulseSocket.String()); err != nil { + if s, err := k.Stat(pulseSocket.String()); err != nil { if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: fmt.Sprintf("access PulseAudio socket %q", pulseSocket), Err: err} } @@ -453,7 +451,7 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co const paLocateStep = "locate PulseAudio cookie" // from environment - if p, ok := sys.LookupEnv(pulseCookie); ok { + if p, ok := k.LookupEnv(pulseCookie); ok { if a, err := container.NewAbs(p); err != nil { return &hst.AppError{Step: paLocateStep, Err: err} } else { @@ -464,14 +462,14 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } // $HOME/.pulse-cookie - if p, ok := sys.LookupEnv(home); ok { + if p, ok := k.LookupEnv(home); ok { if a, err := container.NewAbs(p); err != nil { return &hst.AppError{Step: paLocateStep, Err: err} } else { paCookiePath = a.Append(".pulse-cookie") } - if s, err := sys.Stat(paCookiePath.String()); err != nil { + if s, err := k.Stat(paCookiePath.String()); err != nil { paCookiePath = nil if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: "access PulseAudio cookie", Err: err} @@ -485,13 +483,13 @@ func (seal *Outcome) finalise(ctx context.Context, sys sys.State, config *hst.Co } // $XDG_CONFIG_HOME/pulse/cookie - if p, ok := sys.LookupEnv(xdgConfigHome); ok { + if p, ok := k.LookupEnv(xdgConfigHome); ok { if a, err := container.NewAbs(p); err != nil { return &hst.AppError{Step: paLocateStep, Err: err} } else { paCookiePath = a.Append("pulse", "cookie") } - if s, err := sys.Stat(paCookiePath.String()); err != nil { + if s, err := k.Stat(paCookiePath.String()); err != nil { paCookiePath = nil if !errors.Is(err, fs.ErrNotExist) { return &hst.AppError{Step: "access PulseAudio cookie", Err: err} diff --git a/internal/app/state/multi.go b/internal/app/state/multi.go index 80a3abff..9a400ac6 100644 --- a/internal/app/state/multi.go +++ b/internal/app/state/multi.go @@ -27,27 +27,27 @@ type multiStore struct { lock sync.RWMutex } -func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) { +func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) { s.lock.RLock() defer s.lock.RUnlock() // load or initialise new backend b := new(multiBackend) b.lock.Lock() - if v, ok := s.backends.LoadOrStore(aid, b); ok { + if v, ok := s.backends.LoadOrStore(identity, b); ok { b = v.(*multiBackend) } else { - b.path = path.Join(s.base, strconv.Itoa(aid)) + b.path = path.Join(s.base, strconv.Itoa(identity)) // ensure directory if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) { - s.backends.CompareAndDelete(aid, b) + s.backends.CompareAndDelete(identity, b) return false, err } // open locker file if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil { - s.backends.CompareAndDelete(aid, b) + s.backends.CompareAndDelete(identity, b) return false, err } else { b.lockfile = l diff --git a/internal/app/state/state.go b/internal/app/state/state.go index 42750ec6..6a3a82db 100644 --- a/internal/app/state/state.go +++ b/internal/app/state/state.go @@ -17,7 +17,7 @@ type Store interface { // Do calls f exactly once and ensures store exclusivity until f returns. // Returns whether f is called and any errors during the locking process. // Cursor provided to f becomes invalid as soon as f returns. - Do(aid int, f func(c Cursor)) (ok bool, err error) + Do(identity int, f func(c Cursor)) (ok bool, err error) // List queries the store and returns a list of aids known to the store. // Note that some or all returned aids might not have any active apps. diff --git a/internal/sys/hsu.go b/internal/sys/hsu.go index 84bb0731..2be12d1e 100644 --- a/internal/sys/hsu.go +++ b/internal/sys/hsu.go @@ -3,6 +3,7 @@ package sys import ( "errors" "fmt" + "log" "os" "os/exec" "strconv" @@ -11,6 +12,7 @@ import ( "hakurei.app/container" "hakurei.app/hst" "hakurei.app/internal" + "hakurei.app/internal/hlog" ) // Hsu caches responses from cmd/hsu. @@ -79,3 +81,24 @@ func (h *Hsu) Uid(identity int) (int, error) { } return u.uid, u.err } + +// MustUid calls [State.Uid] and terminates on error. +func MustUid(s State, identity int) int { + uid, err := s.Uid(identity) + if err == nil { + return uid + } + + const fallback = "cannot obtain uid from setuid wrapper:" + if errors.Is(err, ErrHsuAccess) { + hlog.Verbose("*"+fallback, err) + os.Exit(1) + return -0xdeadbeef + } else if m, ok := container.GetErrorMessage(err); ok { + log.Fatal(m) + return -0xdeadbeef + } else { + log.Fatalln(fallback, err) + return -0xdeadbeef + } +} diff --git a/internal/sys/interface.go b/internal/sys/interface.go index ed765ece..f2465e62 100644 --- a/internal/sys/interface.go +++ b/internal/sys/interface.go @@ -49,14 +49,8 @@ type State interface { Uid(identity int) (int, error) } -// GetUserID obtains user id from hsu by querying uid of identity 0. -func GetUserID(os State) (int, error) { - if uid, err := os.Uid(0); err != nil { - return -1, err - } else { - return (uid / 10000) - 100, nil - } -} +// MustGetUserID obtains user id from hsu by querying uid of identity 0. +func MustGetUserID(os State) int { return (MustUid(os, 0) / 10000) - 100 } // CopyPaths is a generic implementation of [hst.Paths]. func CopyPaths(os State, v *hst.Paths, userid int) { diff --git a/internal/sys/std.go b/internal/sys/std.go index 24b36fa2..11334c72 100644 --- a/internal/sys/std.go +++ b/internal/sys/std.go @@ -1,9 +1,7 @@ package sys import ( - "errors" "io/fs" - "log" "os" "os/exec" "os/user" @@ -41,30 +39,6 @@ func (s *Std) Printf(format string, v ...any) { hlog.Verbosef(form const xdgRuntimeDir = "XDG_RUNTIME_DIR" func (s *Std) Paths() hst.Paths { - s.pathsOnce.Do(func() { - if userid, err := GetUserID(s); err != nil { - // TODO(ophestra): this duplicates code in cmd/hakurei/command.go, keep this up to date until removal - hlog.BeforeExit() - const fallback = "cannot obtain user id from hsu:" - - // this indicates the error message has already reached stderr, outside the current process's control; - // this is only reached when hsu fails for any reason, as a second error message following hsu is confusing - if errors.Is(err, ErrHsuAccess) { - hlog.Verbose("*"+fallback, err) - os.Exit(1) - return - } - - m, ok := container.GetErrorMessage(err) - if !ok { - log.Fatalln(fallback, err) - return - } - - log.Fatal(m) - } else { - CopyPaths(s, &s.paths, userid) - } - }) + s.pathsOnce.Do(func() { CopyPaths(s, &s.paths, MustGetUserID(s)) }) return s.paths } |
