From 1c4f593566943d8a0ce16eb3cae77f8c50238202 Mon Sep 17 00:00:00 2001 From: Ophestra Date: Wed, 24 Sep 2025 18:44:14 +0900 Subject: [PATCH] internal/app: unexport outcome, remove app struct The App struct no longer does anything, and the outcome struct is entirely opaque. Signed-off-by: Ophestra --- cmd/hakurei/command.go | 28 ++----------- cmd/hakurei/command_test.go | 2 +- cmd/hakurei/main.go | 9 ++++- internal/app/app.go | 81 ++++++------------------------------- internal/app/app_test.go | 53 ++++++++++-------------- internal/app/export_test.go | 13 ++---- internal/app/process.go | 9 ++--- internal/app/seal.go | 8 ++-- internal/app/strings.go | 5 +-- 9 files changed, 59 insertions(+), 149 deletions(-) diff --git a/cmd/hakurei/command.go b/cmd/hakurei/command.go index f1032f9..0600a55 100644 --- a/cmd/hakurei/command.go +++ b/cmd/hakurei/command.go @@ -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") - } -} diff --git a/cmd/hakurei/command_test.go b/cmd/hakurei/command_test.go index dbbcea9..8ce9a23 100644 --- a/cmd/hakurei/command_test.go +++ b/cmd/hakurei/command_test.go @@ -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) diff --git a/cmd/hakurei/main.go b/cmd/hakurei/main.go index a636cfc..b71f50c 100644 --- a/cmd/hakurei/main.go +++ b/cmd/hakurei/main.go @@ -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() diff --git a/internal/app/app.go b/internal/app/app.go index dd34cf6..4ecf1cc 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -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 "" +// 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 "" - } - 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") } diff --git a/internal/app/app_test.go b/internal/app/app_test.go index 8b54b4e..edd8c9f 100644 --- a/internal/app/app_test.go +++ b/internal/app/app_test.go @@ -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)) + } + }) }) }) } diff --git a/internal/app/export_test.go b/internal/app/export_test.go index 3e5db9f..1bfba48 100644 --- a/internal/app/export_test.go +++ b/internal/app/export_test.go @@ -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) } diff --git a/internal/app/process.go b/internal/app/process.go index 0ee123a..a182fa6 100644 --- a/internal/app/process.go +++ b/internal/app/process.go @@ -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") } diff --git a/internal/app/seal.go b/internal/app/seal.go index cafd691..2d7fea3 100644 --- a/internal/app/seal.go +++ b/internal/app/seal.go @@ -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" diff --git a/internal/app/strings.go b/internal/app/strings.go index e4465ce..a7fdcd4 100644 --- a/internal/app/strings.go +++ b/internal/app/strings.go @@ -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 {