diff options
| author | Ophestra <cat@gensokyo.uk> | 2025-07-02 20:42:51 +0900 |
|---|---|---|
| committer | Ophestra <cat@gensokyo.uk> | 2025-07-02 20:42:51 +0900 |
| commit | eb22a8bcc1ac03357d4073e5d3ed6d0ab5246a37 (patch) | |
| tree | d27d1f724a18bb47bf48816a444dccac28b5248c /internal/state/multi.go | |
| parent | 31aef905fa819310ee7694775a836c294ff742e4 (diff) | |
cmd/hakurei: move to cmd
Having it at the project root never made sense since the "ego" name was deprecated. This change finally addresses it.
Signed-off-by: Ophestra <cat@gensokyo.uk>
Diffstat (limited to 'internal/state/multi.go')
| -rw-r--r-- | internal/state/multi.go | 373 |
1 files changed, 0 insertions, 373 deletions
diff --git a/internal/state/multi.go b/internal/state/multi.go deleted file mode 100644 index fac5b389..00000000 --- a/internal/state/multi.go +++ /dev/null @@ -1,373 +0,0 @@ -package state - -import ( - "encoding/binary" - "encoding/gob" - "errors" - "fmt" - "io" - "io/fs" - "os" - "path" - "strconv" - "sync" - "syscall" - - "git.gensokyo.uk/security/hakurei/hst" - "git.gensokyo.uk/security/hakurei/internal/app" - "git.gensokyo.uk/security/hakurei/internal/hlog" -) - -// fine-grained locking and access -type multiStore struct { - base string - - // initialised backends - backends *sync.Map - - lock sync.RWMutex -} - -func (s *multiStore) Do(aid 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 { - b = v.(*multiBackend) - } else { - b.path = path.Join(s.base, strconv.Itoa(aid)) - - // ensure directory - if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) { - s.backends.CompareAndDelete(aid, 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) - return false, err - } else { - b.lockfile = l - } - b.lock.Unlock() - } - - // lock backend - if err := b.lockFile(); err != nil { - return false, err - } - - // expose backend methods without exporting the pointer - c := new(struct{ *multiBackend }) - c.multiBackend = b - f(b) - // disable access to the backend on a best-effort basis - c.multiBackend = nil - - // unlock backend - return true, b.unlockFile() -} - -func (s *multiStore) List() ([]int, error) { - var entries []os.DirEntry - - // read base directory to get all aids - if v, err := os.ReadDir(s.base); err != nil && !errors.Is(err, os.ErrNotExist) { - return nil, err - } else { - entries = v - } - - aidsBuf := make([]int, 0, len(entries)) - for _, e := range entries { - // skip non-directories - if !e.IsDir() { - hlog.Verbosef("skipped non-directory entry %q", e.Name()) - continue - } - - // skip non-numerical names - if v, err := strconv.Atoi(e.Name()); err != nil { - hlog.Verbosef("skipped non-aid entry %q", e.Name()) - continue - } else { - if v < 0 || v > 9999 { - hlog.Verbosef("skipped out of bounds entry %q", e.Name()) - continue - } - - aidsBuf = append(aidsBuf, v) - } - } - - return append([]int(nil), aidsBuf...), nil -} - -func (s *multiStore) Close() error { - s.lock.Lock() - defer s.lock.Unlock() - - var errs []error - s.backends.Range(func(_, value any) bool { - b := value.(*multiBackend) - errs = append(errs, b.close()) - return true - }) - - return errors.Join(errs...) -} - -type multiBackend struct { - path string - - // created/opened by prepare - lockfile *os.File - - lock sync.RWMutex -} - -func (b *multiBackend) filename(id *app.ID) string { - return path.Join(b.path, id.String()) -} - -func (b *multiBackend) lockFileAct(lt int) (err error) { - op := "LockAct" - switch lt { - case syscall.LOCK_EX: - op = "Lock" - case syscall.LOCK_UN: - op = "Unlock" - } - - for { - err = syscall.Flock(int(b.lockfile.Fd()), lt) - if !errors.Is(err, syscall.EINTR) { - break - } - } - if err != nil { - return &fs.PathError{ - Op: op, - Path: b.lockfile.Name(), - Err: err, - } - } - return nil -} - -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 -func (b *multiBackend) load(decode bool) (Entries, error) { - b.lock.RLock() - defer b.lock.RUnlock() - - // 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 - } else { - entries = pl - } - - // allocate as if every entry is valid - // since that should be the case assuming no external interference happens - r := make(Entries, len(entries)) - - for _, e := range entries { - if e.IsDir() { - return nil, fmt.Errorf("unexpected directory %q in store", e.Name()) - } - - id := new(app.ID) - if err := app.ParseAppID(id, e.Name()); err != nil { - return nil, 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 - } else { - defer func() { - if f.Close() != nil { - // unreachable - panic("foreign state file closed prematurely") - } - }() - - s := new(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 { - return fmt.Errorf("state entry %s has unexpected id %s", id, &s.ID) - } - } - - return nil - } - }(); err != nil { - return nil, err - } - } - - 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 { - b.lock.Lock() - defer b.lock.Unlock() - - if configWriter == nil && 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) - } -} - -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 -} - -func (b *multiBackend) Destroy(id app.ID) error { - b.lock.Lock() - defer b.lock.Unlock() - - return os.Remove(b.filename(&id)) -} - -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 -} - -func (b *multiBackend) close() error { - b.lock.Lock() - defer b.lock.Unlock() - - err := b.lockfile.Close() - if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) { - return nil - } - return err -} - -// NewMulti returns an instance of the multi-file store. -func NewMulti(runDir string) Store { - b := new(multiStore) - b.base = path.Join(runDir, "state") - b.backends = new(sync.Map) - return b -} |
