forked from security/hakurei
internal/store: rename from state
This reduces collision with local variable names, and generally makes sense for the new store package, since it no longer specifies the state struct. Signed-off-by: Ophestra <cat@gensokyo.uk>
This commit is contained in:
161
internal/store/segment.go
Normal file
161
internal/store/segment.go
Normal file
@@ -0,0 +1,161 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"iter"
|
||||
"os"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"hakurei.app/container/check"
|
||||
"hakurei.app/hst"
|
||||
"hakurei.app/internal/lockedfile"
|
||||
)
|
||||
|
||||
// stateEntryHandle is a handle on a state entry retrieved from a storeHandle.
|
||||
// Must only be used while its parent storeHandle.fileMu is held.
|
||||
type stateEntryHandle struct {
|
||||
// Error returned while decoding pathname.
|
||||
// A non-nil value disables stateEntryHandle.
|
||||
decodeErr error
|
||||
|
||||
// Checked path to entry file.
|
||||
pathname *check.Absolute
|
||||
|
||||
hst.ID
|
||||
}
|
||||
|
||||
// open opens the underlying state entry file, returning [hst.AppError] for a non-nil error.
|
||||
func (eh *stateEntryHandle) open(flag int, perm os.FileMode) (*os.File, error) {
|
||||
if eh.decodeErr != nil {
|
||||
return nil, eh.decodeErr
|
||||
}
|
||||
|
||||
if f, err := os.OpenFile(eh.pathname.String(), flag, perm); err != nil {
|
||||
return nil, &hst.AppError{Step: "open state entry", Err: err}
|
||||
} else {
|
||||
return f, nil
|
||||
}
|
||||
}
|
||||
|
||||
// destroy removes the underlying state entry file, returning [hst.AppError] for a non-nil error.
|
||||
func (eh *stateEntryHandle) destroy() error {
|
||||
// destroy does not go through open
|
||||
if eh.decodeErr != nil {
|
||||
return eh.decodeErr
|
||||
}
|
||||
|
||||
if err := os.Remove(eh.pathname.String()); err != nil {
|
||||
return &hst.AppError{Step: "destroy state entry", Err: err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// save encodes [hst.State] and writes it to the underlying file.
|
||||
// An error is returned if a file already exists with the same identifier.
|
||||
// save does not validate the embedded [hst.Config].
|
||||
func (eh *stateEntryHandle) save(state *hst.State) error {
|
||||
f, err := eh.open(os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = entryEncode(f, state)
|
||||
if closeErr := f.Close(); closeErr != nil && err == nil {
|
||||
err = &hst.AppError{Step: "close state file", Err: closeErr}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// load loads and validates the state entry header, and returns the [hst.Enablement] byte.
|
||||
// for a non-nil v, the full state payload is decoded and stored in the value pointed to by v.
|
||||
// load validates the embedded hst.Config value.
|
||||
func (eh *stateEntryHandle) load(v *hst.State) (hst.Enablement, error) {
|
||||
f, err := eh.open(os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
|
||||
var et hst.Enablement
|
||||
if v != nil {
|
||||
et, err = entryDecode(f, v)
|
||||
if err == nil && v.ID != eh.ID {
|
||||
err = &hst.AppError{Step: "validate state identifier", Err: os.ErrInvalid,
|
||||
Msg: fmt.Sprintf("state entry %s has unexpected id %s", eh.ID.String(), v.ID.String())}
|
||||
}
|
||||
} else {
|
||||
et, err = entryDecodeHeader(f)
|
||||
}
|
||||
|
||||
if closeErr := f.Close(); closeErr != nil && err == nil {
|
||||
err = &hst.AppError{Step: "close state file", Err: closeErr}
|
||||
}
|
||||
return et, err
|
||||
}
|
||||
|
||||
// storeHandle is a handle on a stateStore segment.
|
||||
// Initialised by stateStore.identityHandle.
|
||||
type storeHandle struct {
|
||||
// Identity of instances tracked by this segment.
|
||||
identity int
|
||||
// Pathname of directory that the segment referred to by storeHandle is rooted in.
|
||||
path *check.Absolute
|
||||
// Inter-process mutex to synchronise operations against resources in this segment.
|
||||
fileMu *lockedfile.Mutex
|
||||
|
||||
// Must be held alongside fileMu.
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
// entries returns an iterator over all stateEntryHandle held in this segment.
|
||||
// Must be called while holding a lock on mu and fileMu.
|
||||
// A non-nil error attached to a stateEntryHandle indicates a malformed identifier and is of type [hst.AppError].
|
||||
// A non-nil error returned by entries is of type [hst.AppError].
|
||||
func (h *storeHandle) entries() (iter.Seq[*stateEntryHandle], int, error) {
|
||||
// for error reporting
|
||||
const step = "read store segment entries"
|
||||
|
||||
// read directory contents, should only contain storeMutexName and identifier
|
||||
var entries []os.DirEntry
|
||||
if pl, err := os.ReadDir(h.path.String()); err != nil {
|
||||
return nil, -1, &hst.AppError{Step: step, Err: err}
|
||||
} else {
|
||||
entries = pl
|
||||
}
|
||||
|
||||
// expects lock file
|
||||
l := len(entries)
|
||||
if l > 0 {
|
||||
l--
|
||||
}
|
||||
|
||||
return func(yield func(*stateEntryHandle) bool) {
|
||||
for _, ent := range entries {
|
||||
var eh = stateEntryHandle{pathname: h.path.Append(ent.Name())}
|
||||
|
||||
// this should never happen
|
||||
if ent.IsDir() {
|
||||
eh.decodeErr = &hst.AppError{Step: step,
|
||||
Err: errors.New("unexpected directory " + strconv.Quote(ent.Name()) + " in store")}
|
||||
goto out
|
||||
}
|
||||
|
||||
// silently skip lock file
|
||||
if ent.Name() == storeMutexName {
|
||||
continue
|
||||
}
|
||||
|
||||
// this either indicates a serious bug or external interference
|
||||
if err := eh.ID.UnmarshalText([]byte(ent.Name())); err != nil {
|
||||
eh.decodeErr = &hst.AppError{Step: "decode store segment entry", Err: err}
|
||||
goto out
|
||||
}
|
||||
|
||||
out:
|
||||
if !yield(&eh) {
|
||||
break
|
||||
}
|
||||
}
|
||||
}, l, nil
|
||||
}
|
||||
Reference in New Issue
Block a user