internal/app: unexport outcome, remove app struct
All checks were successful
Test / Create distribution (push) Successful in 34s
Test / Sandbox (push) Successful in 2m14s
Test / Hakurei (race detector) (push) Successful in 5m20s
Test / Hpkg (push) Successful in 41s
Test / Hakurei (push) Successful in 2m20s
Test / Sandbox (race detector) (push) Successful in 2m9s
Test / Flake checks (push) Successful in 1m30s

The App struct no longer does anything, and the outcome struct is entirely opaque.

Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
Ophestra 2025-09-24 18:44:14 +09:00
parent b99c63337d
commit 1c4f593566
Signed by: cat
SSH Key Fingerprint: SHA256:gQ67O0enBZ7UdZypgtspB2FDM1g3GVw8nX0XSdcFw8Q
9 changed files with 59 additions and 149 deletions

View File

@ -6,11 +6,9 @@ import (
"io"
"log"
"os"
"os/signal"
"os/user"
"strconv"
"sync"
"syscall"
"time"
"hakurei.app/command"
@ -25,7 +23,7 @@ import (
"hakurei.app/system/dbus"
)
func buildCommand(out io.Writer) command.Command {
func buildCommand(ctx context.Context, out io.Writer) command.Command {
var (
flagVerbose bool
flagJSON bool
@ -45,7 +43,7 @@ func buildCommand(out io.Writer) command.Command {
config := tryPath(args[0])
config.Args = append(config.Args, args[1:]...)
runApp(config)
app.Main(ctx, std, config)
panic("unreachable")
})
@ -165,8 +163,7 @@ func buildCommand(out io.Writer) command.Command {
}
}
// invoke app
runApp(config)
app.Main(ctx, std, config)
panic("unreachable")
}).
Flag(&flagDBusConfigSession, "dbus-config", command.StringFlag("builtin"),
@ -249,22 +246,3 @@ func buildCommand(out io.Writer) command.Command {
return c
}
func runApp(config *hst.Config) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
a := app.MustNew(ctx, std)
if sa, err := a.Seal(config); err != nil {
hlog.BeforeExit()
if m, ok := container.GetErrorMessage(err); ok {
log.Fatal(m)
} else {
log.Fatalln("cannot seal app:", err)
}
} else {
sa.Main()
panic("unreachable")
}
}

View File

@ -68,7 +68,7 @@ Flags:
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
out := new(bytes.Buffer)
c := buildCommand(out)
c := buildCommand(t.Context(), out)
if err := c.Parse(tc.args); !errors.Is(err, command.ErrHelp) && !errors.Is(err, flag.ErrHelp) {
t.Errorf("Parse: error = %v; want %v",
err, command.ErrHelp)

View File

@ -4,10 +4,13 @@ package main
//go:generate cp ../../LICENSE .
import (
"context"
_ "embed"
"errors"
"log"
"os"
"os/signal"
"syscall"
"hakurei.app/container"
"hakurei.app/internal"
@ -44,7 +47,11 @@ func main() {
log.Fatal("this program must not run as root")
}
buildCommand(os.Stderr).MustParse(os.Args[1:], func(err error) {
ctx, stop := signal.NotifyContext(context.Background(),
syscall.SIGINT, syscall.SIGTERM)
defer stop() // unreachable
buildCommand(ctx, os.Stderr).MustParse(os.Args[1:], func(err error) {
hlog.Verbosef("command returned %v", err)
if errors.Is(err, errSuccess) {
hlog.BeforeExit()

View File

@ -3,83 +3,28 @@ package app
import (
"context"
"fmt"
"log"
"sync"
"os"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
)
// New returns the address of a newly initialised [App] struct.
func New(ctx context.Context, os sys.State) (*App, error) {
a := new(App)
a.sys = os
a.ctx = ctx
id := new(state.ID)
err := state.NewAppID(id)
a.id = newID(id)
return a, err
}
// MustNew calls [New] and panics if an error is returned.
func MustNew(ctx context.Context, os sys.State) *App {
a, err := New(ctx, os)
if err != nil {
log.Fatalf("cannot create app: %v", err)
}
return a
}
// An App keeps track of the hakurei container lifecycle.
type App struct {
outcome *Outcome
id *stringPair[state.ID]
sys sys.State
ctx context.Context
mu sync.RWMutex
}
// ID returns a copy of [state.ID] held by App.
func (a *App) ID() state.ID { a.mu.RLock(); defer a.mu.RUnlock(); return a.id.unwrap() }
func (a *App) String() string {
if a == nil {
return "<nil>"
// Main runs an app according to [hst.Config] and terminates. Main does not return.
func Main(ctx context.Context, k sys.State, config *hst.Config) {
var id state.ID
if err := state.NewAppID(&id); err != nil {
log.Fatal(err)
}
a.mu.RLock()
defer a.mu.RUnlock()
if a.outcome != nil {
if a.outcome.user.uid == nil {
return "<invalid>"
}
return fmt.Sprintf("sealed app %s as uid %s", a.id, a.outcome.user.uid)
var seal outcome
seal.id = &stringPair[state.ID]{id, id.String()}
if err := seal.finalise(ctx, k, config); err != nil {
printMessageError("cannot seal app:", err)
os.Exit(1)
}
return fmt.Sprintf("unsealed app %s", a.id)
}
// Seal determines the [Outcome] of [hst.Config].
// Values stored in and referred to by [hst.Config] might be overwritten and must not be used again.
func (a *App) Seal(config *hst.Config) (*Outcome, error) {
a.mu.Lock()
defer a.mu.Unlock()
if a.outcome != nil {
panic("attempting to seal app twice")
}
seal := new(Outcome)
seal.id = a.id
err := seal.finalise(a.ctx, a.sys, config)
if err == nil {
a.outcome = seal
}
return seal, err
seal.main()
panic("unreachable")
}

View File

@ -16,12 +16,12 @@ import (
)
type sealTestCase struct {
name string
os sys.State
config *hst.Config
id state.ID
wantSys *system.I
wantContainer *container.Params
name string
os sys.State
config *hst.Config
id state.ID
wantSys *system.I
wantParams *container.Params
}
func TestApp(t *testing.T) {
@ -29,38 +29,27 @@ func TestApp(t *testing.T) {
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
a := app.NewWithID(t.Context(), tc.id, tc.os)
var (
gotSys *system.I
gotContainer *container.Params
)
if !t.Run("seal", func(t *testing.T) {
if sa, err := a.Seal(tc.config); err != nil {
t.Run("finalise", func(t *testing.T) {
sys, params, err := app.FinaliseIParams(t.Context(), tc.os, tc.config, &tc.id)
if err != nil {
if s, ok := container.GetErrorMessage(err); !ok {
t.Errorf("Seal: error = %v", err)
t.Fatalf("Seal: error = %v", err)
} else {
t.Errorf("Seal: %s", s)
t.Fatalf("Seal: %s", s)
}
return
} else {
gotSys, gotContainer = app.AppIParams(a, sa)
}
}) {
return
}
t.Run("compare sys", func(t *testing.T) {
if !gotSys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v",
gotSys, tc.wantSys)
}
})
t.Run("sys", func(t *testing.T) {
if !sys.Equal(tc.wantSys) {
t.Errorf("Seal: sys = %#v, want %#v", sys, tc.wantSys)
}
})
t.Run("compare params", func(t *testing.T) {
if !reflect.DeepEqual(gotContainer, tc.wantContainer) {
t.Errorf("seal: params =\n%s\n, want\n%s",
mustMarshal(gotContainer), mustMarshal(tc.wantContainer))
}
t.Run("params", func(t *testing.T) {
if !reflect.DeepEqual(params, tc.wantParams) {
t.Errorf("seal: params =\n%s\n, want\n%s", mustMarshal(params), mustMarshal(tc.wantParams))
}
})
})
})
}

View File

@ -4,18 +4,13 @@ import (
"context"
"hakurei.app/container"
"hakurei.app/hst"
"hakurei.app/internal/app/state"
"hakurei.app/internal/sys"
"hakurei.app/system"
)
func NewWithID(ctx context.Context, id state.ID, os sys.State) *App {
return &App{id: newID(&id), sys: os, ctx: ctx}
}
func AppIParams(a *App, seal *Outcome) (*system.I, *container.Params) {
if a.outcome != seal || a.id != seal.id {
panic("broken app/outcome link")
}
return seal.sys, seal.container
func FinaliseIParams(ctx context.Context, k sys.State, config *hst.Config, id *state.ID) (*system.I, *container.Params, error) {
seal := outcome{id: &stringPair[state.ID]{*id, id.String()}}
return seal.sys, seal.container, seal.finalise(ctx, k, config)
}

View File

@ -22,7 +22,7 @@ import (
// duration to wait for shim to exit, after container WaitDelay has elapsed.
const shimWaitTimeout = 5 * time.Second
// mainState holds persistent state bound to [Outcome.Main].
// mainState holds persistent state bound to outcome.main.
type mainState struct {
// done is whether beforeExit has been called already.
done bool
@ -33,7 +33,7 @@ type mainState struct {
// Time is nil if no process was ever created.
Time *time.Time
seal *Outcome
seal *outcome
store state.Store
cancel context.CancelFunc
cmd *exec.Cmd
@ -218,9 +218,8 @@ func (ms mainState) fatal(fallback string, ferr error) {
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() {
// main carries out outcome and terminates. main does not return.
func (seal *outcome) main() {
if !seal.f.CompareAndSwap(false, true) {
panic("outcome: attempted to run twice")
}

View File

@ -32,8 +32,8 @@ func newWithMessageError(msg string, err error) error {
return &hst.AppError{Step: "finalise", Err: err, Msg: msg}
}
// An Outcome is the runnable state of a hakurei container via [hst.Config].
type Outcome struct {
// An outcome is the runnable state of a hakurei container via [hst.Config].
type outcome struct {
// copied from initialising [app]
id *stringPair[state.ID]
// copied from [sys.State]
@ -66,7 +66,7 @@ type shareHost struct {
// process-specific directory in XDG_RUNTIME_DIR, empty if unused
runtimeSharePath *container.Absolute
seal *Outcome
seal *outcome
sc hst.Paths
}
@ -119,7 +119,7 @@ type hsuUser struct {
username string
}
func (seal *Outcome) finalise(ctx context.Context, k 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"

View File

@ -2,12 +2,9 @@ package app
import (
"strconv"
"hakurei.app/internal/app/state"
)
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
func newID(id *state.ID) *stringPair[state.ID] { return &stringPair[state.ID]{*id, id.String()} }
func newInt(v int) *stringPair[int] { return &stringPair[int]{v, strconv.Itoa(v)} }
// stringPair stores a value and its string representation.
type stringPair[T comparable] struct {