internal/app: do not return from shim start
All checks were successful
Test / Create distribution (push) Successful in 49s
Test / Sandbox (push) Successful in 2m37s
Test / Hakurei (push) Successful in 3m32s
Test / Hpkg (push) Successful in 4m21s
Test / Hakurei (race detector) (push) Successful in 5m37s
Test / Sandbox (race detector) (push) Successful in 2m7s
Test / Flake checks (push) Successful in 1m20s

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>
This commit is contained in:
2025-09-24 13:26:30 +09:00
parent f09133a224
commit b99c63337d
10 changed files with 388 additions and 509 deletions

View File

@@ -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
}

View File

@@ -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")
}
now := time.Now().UTC()
rs.Time = &now
}
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
)
// 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")
// 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 hlog.BeforeExit()
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)
}
// read comp value early to allow for early failure
hsuPath := internal.MustHsuPath()
if err := seal.sys.Commit(); err != nil {
return err
}
store := state.NewMulti(seal.runDirPath.String())
deferredStoreFunc := func(c state.Cursor) error { return nil } // noop until state in store
exitCode := 1
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)
if hasErr {
os.Exit(exitCode)
}
}()
var rt system.Enablement
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
} else {
hlog.Verbosef("found %d instances, cleaning up without user-scoped operations", l)
// 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) }()
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())
// 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)
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()
}
}
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 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 {
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)
}
}
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)
}
}
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)
}
hlog.Resume()
if seal.sync != nil {
if err := seal.sync.Close(); err != nil {
log.Printf("cannot close wayland security context: %v", err)
}); err != nil {
if ok {
ms.uintptr |= mainNeedsDestroy
ms.fatal("cannot unlock state store:", err)
} else {
ms.fatal("cannot open state store:", err)
}
}
if seal.dbusMsg != nil {
seal.dbusMsg()
}
// state in store at this point, destroy defunct state entry on termination
ms.uintptr |= mainNeedsDestroy
return earlyStoreErr.equiv("save process state")
// 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
}
log.Print(m)
}

View File

@@ -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}

View File

@@ -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

View File

@@ -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.

View File

@@ -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
}
}

View File

@@ -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) {

View File

@@ -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
}