diff --git a/internal/app/finalise.go b/internal/app/finalise.go index a5286bd..a2a8f30 100644 --- a/internal/app/finalise.go +++ b/internal/app/finalise.go @@ -1,12 +1,9 @@ package app import ( - "bytes" "context" - "encoding/gob" "errors" "fmt" - "io" "os" "os/user" "sync/atomic" @@ -24,16 +21,14 @@ func newWithMessageError(msg string, err error) error { // An outcome is the runnable state of a hakurei container via [hst.Config]. type outcome struct { - // initial [hst.Config] gob stream for state data; - // this is prepared ahead of time as config is clobbered during seal creation - ct io.WriterTo - // Supplementary group ids. Populated during finalise. supp []string // Resolved priv side operating system interactions. Populated during finalise. sys *system.I // Transmitted to shim. Populated during finalise. state *outcomeState + // Kept for saving to [state]. + config *hst.Config // Whether the current process is in outcome.main. active atomic.Bool @@ -57,16 +52,6 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID, return err } - // TODO(ophestra): do not clobber during finalise - { - // encode initial configuration for state tracking - ct := new(bytes.Buffer) - if err := gob.NewEncoder(ct).Encode(config); err != nil { - return &hst.AppError{Step: "encode initial config", Err: err} - } - k.ct = ct - } - // hsu expects numerical group ids supp := make([]string, len(config.Groups)) for i, name := range config.Groups { @@ -106,5 +91,6 @@ func (k *outcome) finalise(ctx context.Context, msg container.Msg, id *state.ID, k.sys = sys k.supp = supp k.state = &s + k.config = config return nil } diff --git a/internal/app/process.go b/internal/app/process.go index a868512..3225496 100644 --- a/internal/app/process.go +++ b/internal/app/process.go @@ -289,10 +289,11 @@ func (k *outcome) main(msg container.Msg) { // 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(&state.State{ - ID: k.state.id.unwrap(), - PID: ms.cmd.Process.Pid, - Time: *ms.Time, - }, k.ct); err != nil { + ID: k.state.id.unwrap(), + PID: ms.cmd.Process.Pid, + Config: k.config, + Time: *ms.Time, + }); err != nil { ms.fatal("cannot save state entry:", err) } }); err != nil { diff --git a/internal/app/state/multi.go b/internal/app/state/multi.go index 822cddc..244b5c5 100644 --- a/internal/app/state/multi.go +++ b/internal/app/state/multi.go @@ -1,11 +1,9 @@ package state import ( - "encoding/binary" "encoding/gob" "errors" "fmt" - "io" "io/fs" "os" "path" @@ -43,13 +41,13 @@ func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) { // ensure directory if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) { s.backends.CompareAndDelete(identity, b) - return false, err + return false, &hst.AppError{Step: "create store segment directory", Err: err} } // open locker file if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil { s.backends.CompareAndDelete(identity, b) - return false, err + return false, &hst.AppError{Step: "open store segment lock file", Err: err} } else { b.lockfile = l } @@ -58,7 +56,7 @@ func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) { // lock backend if err := b.lockFile(); err != nil { - return false, err + return false, &hst.AppError{Step: "lock store segment", Err: err} } // expose backend methods without exporting the pointer @@ -69,15 +67,18 @@ func (s *multiStore) Do(identity int, f func(c Cursor)) (bool, error) { c.multiBackend = nil // unlock backend - return true, b.unlockFile() + if err := b.unlockFile(); err != nil { + return true, &hst.AppError{Step: "unlock store segment", Err: err} + } + return true, nil } func (s *multiStore) List() ([]int, error) { var entries []os.DirEntry - // read base directory to get all aids + // read base directory to get all identities if v, err := os.ReadDir(s.base); err != nil && !errors.Is(err, os.ErrNotExist) { - return nil, err + return nil, &hst.AppError{Step: "read store directory", Err: err} } else { entries = v } @@ -95,7 +96,7 @@ func (s *multiStore) List() ([]int, error) { s.msg.Verbosef("skipped non-aid entry %q", e.Name()) continue } else { - if v < 0 || v > 9999 { + if v < hst.IdentityMin || v > hst.IdentityMax { s.msg.Verbosef("skipped out of bounds entry %q", e.Name()) continue } @@ -130,9 +131,7 @@ type multiBackend struct { mu sync.RWMutex } -func (b *multiBackend) filename(id *ID) string { - return path.Join(b.path, id.String()) -} +func (b *multiBackend) filename(id *ID) string { return path.Join(b.path, id.String()) } func (b *multiBackend) lockFileAct(lt int) (err error) { op := "LockAct" @@ -159,13 +158,8 @@ func (b *multiBackend) lockFileAct(lt int) (err error) { return nil } -func (b *multiBackend) lockFile() error { - return b.lockFileAct(syscall.LOCK_EX) -} - -func (b *multiBackend) unlockFile() error { - return b.lockFileAct(syscall.LOCK_UN) -} +func (b *multiBackend) lockFile() error { return b.lockFileAct(syscall.LOCK_EX) } +func (b *multiBackend) unlockFile() error { return b.lockFileAct(syscall.LOCK_UN) } // reads all launchers in simpleBackend // file contents are ignored if decode is false @@ -176,7 +170,7 @@ func (b *multiBackend) load(decode bool) (Entries, error) { // read directory contents, should only contain files named after ids var entries []os.DirEntry if pl, err := os.ReadDir(b.path); err != nil { - return nil, err + return nil, &hst.AppError{Step: "read store segment directory", Err: err} } else { entries = pl } @@ -190,34 +184,34 @@ func (b *multiBackend) load(decode bool) (Entries, error) { return nil, fmt.Errorf("unexpected directory %q in store", e.Name()) } - id := new(ID) - if err := ParseAppID(id, e.Name()); err != nil { - return nil, err + var id ID + if err := ParseAppID(&id, e.Name()); err != nil { + return nil, &hst.AppError{Step: "parse state key", Err: err} } // run in a function to better handle file closing if err := func() error { // open state file for reading if f, err := os.Open(path.Join(b.path, e.Name())); err != nil { - return err + return &hst.AppError{Step: "open state file", Err: err} } else { - defer func() { - if f.Close() != nil { - // unreachable - panic("foreign state file closed prematurely") - } - }() - - s := new(State) - r[*id] = s + var s State + r[id] = &s // append regardless, but only parse if required, implements Len if decode { - if err = b.decodeState(f, s); err != nil { - return err - } - if s.ID != *id { + if err = gob.NewDecoder(f).Decode(&s); err != nil { + _ = f.Close() + return &hst.AppError{Step: "decode state data", Err: err} + } else if s.ID != id { + _ = f.Close() return fmt.Errorf("state entry %s has unexpected id %s", id, &s.ID) + } else if err = f.Close(); err != nil { + return &hst.AppError{Step: "close state file", Err: err} + } + + if s.Config == nil { + return ErrNoConfig } } @@ -231,126 +225,47 @@ func (b *multiBackend) load(decode bool) (Entries, error) { return r, nil } -// state file consists of an eight byte header, followed by concatenated gobs -// of [hst.Config] and [State], if [State.Config] is not nil or offset < 0, -// the first gob is skipped -func (b *multiBackend) decodeState(r io.ReadSeeker, state *State) error { - offset := make([]byte, 8) - if l, err := r.Read(offset); err != nil { - if errors.Is(err, io.EOF) { - return fmt.Errorf("state file too short: %d bytes", l) - } - return err - } - - // decode volatile state first - var skipConfig bool - { - o := int64(binary.LittleEndian.Uint64(offset)) - skipConfig = o < 0 - - if !skipConfig { - if l, err := r.Seek(o, io.SeekCurrent); err != nil { - return err - } else if l != 8+o { - return fmt.Errorf("invalid seek offset %d", l) - } - } - } - if err := gob.NewDecoder(r).Decode(state); err != nil { - return err - } - - // decode sealed config - if state.Config == nil { - // config must be provided either as part of volatile state, - // or in the config segment - if skipConfig { - return ErrNoConfig - } - - state.Config = new(hst.Config) - if _, err := r.Seek(8, io.SeekStart); err != nil { - return err - } - return gob.NewDecoder(r).Decode(state.Config) - } else { - return nil - } -} - // Save writes process state to filesystem -func (b *multiBackend) Save(state *State, configWriter io.WriterTo) error { +func (b *multiBackend) Save(state *State) error { b.mu.Lock() defer b.mu.Unlock() - if configWriter == nil && state.Config == nil { + if state.Config == nil { return ErrNoConfig } statePath := b.filename(&state.ID) if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil { - return err - } else { - defer func() { - if f.Close() != nil { - // unreachable - panic("state file closed prematurely") - } - }() - return b.encodeState(f, state, configWriter) + return &hst.AppError{Step: "create state file", Err: err} + } else if err = gob.NewEncoder(f).Encode(state); err != nil { + _ = f.Close() + return &hst.AppError{Step: "encode state data", Err: err} + } else if err = f.Close(); err != nil { + return &hst.AppError{Step: "close state file", Err: err} } -} - -func (b *multiBackend) encodeState(w io.WriteSeeker, state *State, configWriter io.WriterTo) error { - offset := make([]byte, 8) - - // skip header bytes - if _, err := w.Seek(8, io.SeekStart); err != nil { - return err - } - - if configWriter != nil { - // write config gob and encode header - if l, err := configWriter.WriteTo(w); err != nil { - return err - } else { - binary.LittleEndian.PutUint64(offset, uint64(l)) - } - } else { - // offset == -1 indicates absence of config gob - binary.LittleEndian.PutUint64(offset, 0xffffffffffffffff) - } - - // encode volatile state - if err := gob.NewEncoder(w).Encode(state); err != nil { - return err - } - - // write header - if _, err := w.Seek(0, io.SeekStart); err != nil { - return err - } - _, err := w.Write(offset) - return err + return nil } func (b *multiBackend) Destroy(id ID) error { b.mu.Lock() defer b.mu.Unlock() - return os.Remove(b.filename(&id)) + if err := os.Remove(b.filename(&id)); err != nil { + return &hst.AppError{Step: "destroy state entry", Err: err} + } + return nil } -func (b *multiBackend) Load() (Entries, error) { - return b.load(true) -} +func (b *multiBackend) Load() (Entries, error) { return b.load(true) } func (b *multiBackend) Len() (int, error) { // rn consists of only nil entries but has the correct length rn, err := b.load(false) - return len(rn), err + if err != nil { + return -1, &hst.AppError{Step: "count state entries", Err: err} + } + return len(rn), nil } func (b *multiBackend) close() error { @@ -361,7 +276,7 @@ func (b *multiBackend) close() error { if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) { return nil } - return err + return &hst.AppError{Step: "close lock file", Err: err} } // NewMulti returns an instance of the multi-file store. diff --git a/internal/app/state/state.go b/internal/app/state/state.go index 59fbc8c..b8d8d25 100644 --- a/internal/app/state/state.go +++ b/internal/app/state/state.go @@ -3,12 +3,12 @@ package state import ( "errors" - "io" "time" "hakurei.app/hst" ) +// ErrNoConfig is returned by [Cursor] when used with a nil [hst.Config]. var ErrNoConfig = errors.New("state does not contain config") type Entries map[ID]*State @@ -27,23 +27,23 @@ type Store interface { Close() error } -// Cursor provides access to the store +// Cursor provides access to the store of an identity. type Cursor interface { - Save(state *State, configWriter io.WriterTo) error + Save(state *State) error Destroy(id ID) error Load() (Entries, error) Len() (int, error) } -// State is an instance state +// State is the on-disk state of a container instance. type State struct { - // hakurei instance id + // Unique instance id, generated by internal/app. ID ID `json:"instance"` - // child process PID value + // Shim process pid. This runs as the target user. PID int `json:"pid"` - // sealed app configuration + // Configuration value used to start the container. Config *hst.Config `json:"config"` - // process start time + // Exact point in time that the shim process was created. Time time.Time `json:"time"` } diff --git a/internal/app/state/state_test.go b/internal/app/state/state_test.go index cc57865..baada60 100644 --- a/internal/app/state/state_test.go +++ b/internal/app/state/state_test.go @@ -1,9 +1,6 @@ package state_test import ( - "bytes" - "encoding/gob" - "io" "math/rand/v2" "reflect" "slices" @@ -31,12 +28,9 @@ func testStore(t *testing.T, s state.Store) { tl ) - var tc [tl]struct { - state state.State - ct bytes.Buffer - } + var tc [tl]state.State for i := 0; i < tl; i++ { - makeState(t, &tc[i].state, &tc[i].ct) + makeState(t, &tc[i]) } do := func(identity int, f func(c state.Cursor)) { @@ -47,8 +41,8 @@ func testStore(t *testing.T, s state.Store) { insert := func(i, identity int) { do(identity, func(c state.Cursor) { - if err := c.Save(&tc[i].state, &tc[i].ct); err != nil { - t.Fatalf("Save(&tc[%v]): error = %v", i, err) + if err := c.Save(&tc[i]); err != nil { + t.Fatalf("Save: error = %v", err) } }) } @@ -57,17 +51,13 @@ func testStore(t *testing.T, s state.Store) { do(identity, func(c state.Cursor) { if entries, err := c.Load(); err != nil { t.Fatalf("Load: error = %v", err) - } else if got, ok := entries[tc[i].state.ID]; !ok { - t.Fatalf("Load: entry %s missing", - &tc[i].state.ID) + } else if got, ok := entries[tc[i].ID]; !ok { + t.Fatalf("Load: entry %s missing", &tc[i].ID) } else { - got.Time = tc[i].state.Time - tc[i].state.Config = hst.Template() - if !reflect.DeepEqual(got, &tc[i].state) { - t.Fatalf("Load: entry %s got %#v, want %#v", - &tc[i].state.ID, got, &tc[i].state) + got.Time = tc[i].Time + if !reflect.DeepEqual(got, &tc[i]) { + t.Fatalf("Load: entry %s got %#v, want %#v", &tc[i].ID, got, &tc[i]) } - tc[i].state.Config = nil } }) } @@ -112,7 +102,7 @@ func testStore(t *testing.T, s state.Store) { t.Run("clear identity 1", func(t *testing.T) { do(1, func(c state.Cursor) { - if err := c.Destroy(tc[insertEntryOtherApp].state.ID); err != nil { + if err := c.Destroy(tc[insertEntryOtherApp].ID); err != nil { t.Fatalf("Destroy: error = %v", err) } }) @@ -120,7 +110,7 @@ func testStore(t *testing.T, s state.Store) { if l, err := c.Len(); err != nil { t.Fatalf("Len: error = %v", err) } else if l != 0 { - t.Fatalf("Len() = %d, want 0", l) + t.Fatalf("Len: %d, want 0", l) } }) }) @@ -132,13 +122,11 @@ func testStore(t *testing.T, s state.Store) { }) } -func makeState(t *testing.T, s *state.State, ct io.Writer) { +func makeState(t *testing.T, s *state.State) { if err := state.NewAppID(&s.ID); err != nil { t.Fatalf("cannot create dummy state: %v", err) } - if err := gob.NewEncoder(ct).Encode(hst.Template()); err != nil { - t.Fatalf("cannot encode dummy config: %v", err) - } s.PID = rand.Int() + s.Config = hst.Template() s.Time = time.Now() }