state: expose aids and use instance id as key
Fortify state store instances was specific to aids due to outdated design decisions carried over from the ego rewrite. That no longer makes sense in the current application, so the interface now enables a single store object to manage all transient state. Signed-off-by: Ophestra Umiker <cat@ophivana.moe>
This commit is contained in:
parent
5ea7333431
commit
eae3034260
@ -218,7 +218,7 @@ func (a *app) Seal(config *fst.Config) error {
|
|||||||
// open process state store
|
// open process state store
|
||||||
// the simple store only starts holding an open file after first action
|
// the simple store only starts holding an open file after first action
|
||||||
// store activity begins after Start is called and must end before Wait
|
// store activity begins after Start is called and must end before Wait
|
||||||
seal.store = state.NewSimple(seal.RunDirPath, seal.sys.user.as)
|
seal.store = state.NewMulti(seal.RunDirPath)
|
||||||
|
|
||||||
// initialise system interface with full uid
|
// initialise system interface with full uid
|
||||||
seal.sys.I = system.New(seal.sys.user.uid)
|
seal.sys.I = system.New(seal.sys.user.uid)
|
||||||
|
@ -76,8 +76,8 @@ func (a *app) Start() error {
|
|||||||
|
|
||||||
// register process state
|
// register process state
|
||||||
var err0 = new(StateStoreError)
|
var err0 = new(StateStoreError)
|
||||||
err0.Inner, err0.DoErr = a.seal.store.Do(func(b state.Backend) {
|
err0.Inner, err0.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(c state.Cursor) {
|
||||||
err0.InnerErr = b.Save(&sd)
|
err0.InnerErr = c.Save(&sd)
|
||||||
})
|
})
|
||||||
a.seal.sys.saveState = true
|
a.seal.sys.saveState = true
|
||||||
return err0.equiv("cannot save process state:")
|
return err0.equiv("cannot save process state:")
|
||||||
@ -199,11 +199,11 @@ func (a *app) Wait() (int, error) {
|
|||||||
|
|
||||||
// update store and revert app setup transaction
|
// update store and revert app setup transaction
|
||||||
e := new(StateStoreError)
|
e := new(StateStoreError)
|
||||||
e.Inner, e.DoErr = a.seal.store.Do(func(b state.Backend) {
|
e.Inner, e.DoErr = a.seal.store.Do(a.seal.sys.user.aid, func(b state.Cursor) {
|
||||||
e.InnerErr = func() error {
|
e.InnerErr = func() error {
|
||||||
// destroy defunct state entry
|
// destroy defunct state entry
|
||||||
if cmd := a.shim.Unwrap(); cmd != nil && a.seal.sys.saveState {
|
if cmd := a.shim.Unwrap(); cmd != nil && a.seal.sys.saveState {
|
||||||
if err := b.Destroy(cmd.Process.Pid); err != nil {
|
if err := b.Destroy(*a.id); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,54 +3,135 @@ package state
|
|||||||
import (
|
import (
|
||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"errors"
|
"errors"
|
||||||
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
|
|
||||||
|
"git.ophivana.moe/security/fortify/fst"
|
||||||
|
"git.ophivana.moe/security/fortify/internal/fmsg"
|
||||||
)
|
)
|
||||||
|
|
||||||
// fine-grained locking and access
|
// fine-grained locking and access
|
||||||
type multiStore struct {
|
type multiStore struct {
|
||||||
path []string
|
base string
|
||||||
|
|
||||||
// created/opened by prepare
|
// initialised backends
|
||||||
lockfile *os.File
|
backends *sync.Map
|
||||||
// enforce prepare method
|
|
||||||
init sync.Once
|
|
||||||
// error returned by prepare
|
|
||||||
initErr error
|
|
||||||
|
|
||||||
lock sync.Mutex
|
lock sync.RWMutex
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *multiStore) Do(f func(b Backend)) (bool, error) {
|
func (s *multiStore) Do(aid int, f func(c Cursor)) (bool, error) {
|
||||||
s.init.Do(s.prepare)
|
s.lock.RLock()
|
||||||
if s.initErr != nil {
|
defer s.lock.RUnlock()
|
||||||
return false, s.initErr
|
|
||||||
}
|
|
||||||
|
|
||||||
s.lock.Lock()
|
// load or initialise new backend
|
||||||
defer s.lock.Unlock()
|
b := new(multiBackend)
|
||||||
|
if v, ok := s.backends.LoadOrStore(aid, b); ok {
|
||||||
|
b = v.(*multiBackend)
|
||||||
|
} else {
|
||||||
|
b.lock.Lock()
|
||||||
|
b.path = path.Join(s.base, strconv.Itoa(aid))
|
||||||
|
|
||||||
// lock store
|
// ensure directory
|
||||||
if err := s.lockFile(); err != nil {
|
if err := os.MkdirAll(b.path, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
||||||
|
s.backends.CompareAndDelete(aid, b)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// initialise new backend for caller
|
// open locker file
|
||||||
b := new(multiBackend)
|
if l, err := os.OpenFile(b.path+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
|
||||||
b.path = path.Join(s.path...)
|
s.backends.CompareAndDelete(aid, b)
|
||||||
f(b)
|
return false, err
|
||||||
// disable backend
|
} else {
|
||||||
b.lock.Lock()
|
b.lockfile = l
|
||||||
|
}
|
||||||
|
b.lock.Unlock()
|
||||||
|
}
|
||||||
|
|
||||||
// unlock store
|
// lock backend
|
||||||
return true, s.unlockFile()
|
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) lockFileAct(lt int) (err error) {
|
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() {
|
||||||
|
fmsg.VPrintf("skipped non-directory entry %q", e.Name())
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// skip non-numerical names
|
||||||
|
if v, err := strconv.Atoi(e.Name()); err != nil {
|
||||||
|
fmsg.VPrintf("skipped non-aid entry %q", e.Name())
|
||||||
|
continue
|
||||||
|
} else {
|
||||||
|
if v < 0 || v > 9999 {
|
||||||
|
fmsg.VPrintf("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 *fst.ID) string {
|
||||||
|
return path.Join(b.path, id.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (b *multiBackend) lockFileAct(lt int) (err error) {
|
||||||
op := "LockAct"
|
op := "LockAct"
|
||||||
switch lt {
|
switch lt {
|
||||||
case syscall.LOCK_EX:
|
case syscall.LOCK_EX:
|
||||||
@ -60,7 +141,7 @@ func (s *multiStore) lockFileAct(lt int) (err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
err = syscall.Flock(int(s.lockfile.Fd()), lt)
|
err = syscall.Flock(int(b.lockfile.Fd()), lt)
|
||||||
if !errors.Is(err, syscall.EINTR) {
|
if !errors.Is(err, syscall.EINTR) {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
@ -68,81 +149,53 @@ func (s *multiStore) lockFileAct(lt int) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return &fs.PathError{
|
return &fs.PathError{
|
||||||
Op: op,
|
Op: op,
|
||||||
Path: s.lockfile.Name(),
|
Path: b.lockfile.Name(),
|
||||||
Err: err,
|
Err: err,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *multiStore) lockFile() error {
|
func (b *multiBackend) lockFile() error {
|
||||||
return s.lockFileAct(syscall.LOCK_EX)
|
return b.lockFileAct(syscall.LOCK_EX)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *multiStore) unlockFile() error {
|
func (b *multiBackend) unlockFile() error {
|
||||||
return s.lockFileAct(syscall.LOCK_UN)
|
return b.lockFileAct(syscall.LOCK_UN)
|
||||||
}
|
|
||||||
|
|
||||||
func (s *multiStore) prepare() {
|
|
||||||
s.initErr = func() error {
|
|
||||||
prefix := path.Join(s.path...)
|
|
||||||
// ensure directory
|
|
||||||
if err := os.MkdirAll(prefix, 0700); err != nil && !errors.Is(err, fs.ErrExist) {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// open locker file
|
|
||||||
if f, err := os.OpenFile(prefix+".lock", os.O_RDWR|os.O_CREATE, 0600); err != nil {
|
|
||||||
return err
|
|
||||||
} else {
|
|
||||||
s.lockfile = f
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *multiStore) Close() error {
|
|
||||||
s.lock.Lock()
|
|
||||||
defer s.lock.Unlock()
|
|
||||||
|
|
||||||
err := s.lockfile.Close()
|
|
||||||
if err == nil || errors.Is(err, os.ErrInvalid) || errors.Is(err, os.ErrClosed) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type multiBackend struct {
|
|
||||||
path string
|
|
||||||
|
|
||||||
lock sync.RWMutex
|
|
||||||
}
|
|
||||||
|
|
||||||
func (b *multiBackend) filename(pid int) string {
|
|
||||||
return path.Join(b.path, strconv.Itoa(pid))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// reads all launchers in simpleBackend
|
// reads all launchers in simpleBackend
|
||||||
// file contents are ignored if decode is false
|
// file contents are ignored if decode is false
|
||||||
func (b *multiBackend) load(decode bool) ([]*State, error) {
|
func (b *multiBackend) load(decode bool) (Entries, error) {
|
||||||
b.lock.RLock()
|
b.lock.RLock()
|
||||||
defer b.lock.RUnlock()
|
defer b.lock.RUnlock()
|
||||||
|
|
||||||
var (
|
// read directory contents, should only contain files named after ids
|
||||||
r []*State
|
var entries []os.DirEntry
|
||||||
f *os.File
|
|
||||||
)
|
|
||||||
|
|
||||||
// read directory contents, should only contain files named after PIDs
|
|
||||||
if pl, err := os.ReadDir(b.path); err != nil {
|
if pl, err := os.ReadDir(b.path); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
} else {
|
} else {
|
||||||
for _, e := range pl {
|
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(fst.ID)
|
||||||
|
if err := fst.ParseAppID(id, e.Name()); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// run in a function to better handle file closing
|
// run in a function to better handle file closing
|
||||||
if err = func() error {
|
if err := func() error {
|
||||||
// open state file for reading
|
// open state file for reading
|
||||||
if f, err = os.Open(path.Join(b.path, e.Name())); err != nil {
|
if f, err := os.Open(path.Join(b.path, e.Name())); err != nil {
|
||||||
return err
|
return err
|
||||||
} else {
|
} else {
|
||||||
defer func() {
|
defer func() {
|
||||||
@ -152,21 +205,26 @@ func (b *multiBackend) load(decode bool) ([]*State, error) {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
var s State
|
s := new(State)
|
||||||
r = append(r, &s)
|
r[*id] = s
|
||||||
|
|
||||||
// append regardless, but only parse if required, used to implement Len
|
// append regardless, but only parse if required, used to implement Len
|
||||||
if decode {
|
if decode {
|
||||||
return gob.NewDecoder(f).Decode(&s)
|
if err = gob.NewDecoder(f).Decode(s); err != nil {
|
||||||
} else {
|
return err
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if s.ID != *id {
|
||||||
|
return fmt.Errorf("state entry %s has unexpected id %s", id, &s.ID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
}(); err != nil {
|
}(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
return r, nil
|
return r, nil
|
||||||
}
|
}
|
||||||
@ -180,7 +238,7 @@ func (b *multiBackend) Save(state *State) error {
|
|||||||
return errors.New("state does not contain config")
|
return errors.New("state does not contain config")
|
||||||
}
|
}
|
||||||
|
|
||||||
statePath := b.filename(state.PID)
|
statePath := b.filename(&state.ID)
|
||||||
|
|
||||||
// create and open state data file
|
// create and open state data file
|
||||||
if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
|
if f, err := os.OpenFile(statePath, os.O_RDWR|os.O_CREATE|os.O_EXCL, 0600); err != nil {
|
||||||
@ -197,14 +255,14 @@ func (b *multiBackend) Save(state *State) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *multiBackend) Destroy(pid int) error {
|
func (b *multiBackend) Destroy(id fst.ID) error {
|
||||||
b.lock.Lock()
|
b.lock.Lock()
|
||||||
defer b.lock.Unlock()
|
defer b.lock.Unlock()
|
||||||
|
|
||||||
return os.Remove(b.filename(pid))
|
return os.Remove(b.filename(&id))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *multiBackend) Load() ([]*State, error) {
|
func (b *multiBackend) Load() (Entries, error) {
|
||||||
return b.load(true)
|
return b.load(true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -214,9 +272,21 @@ func (b *multiBackend) Len() (int, error) {
|
|||||||
return len(rn), err
|
return len(rn), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewSimple returns an instance of a file-based store.
|
func (b *multiBackend) close() error {
|
||||||
func NewSimple(runDir string, prefix ...string) Store {
|
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 := new(multiStore)
|
||||||
b.path = append([]string{runDir, "state"}, prefix...)
|
b.base = path.Join(runDir, "state")
|
||||||
|
b.backends = new(sync.Map)
|
||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
11
internal/state/multi_test.go
Normal file
11
internal/state/multi_test.go
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
package state_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"git.ophivana.moe/security/fortify/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestMulti(t *testing.T) {
|
||||||
|
testStore(t, state.NewMulti(t.TempDir()))
|
||||||
|
}
|
@ -1,11 +1,8 @@
|
|||||||
package state
|
package state
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
"strings"
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"time"
|
"time"
|
||||||
@ -18,45 +15,31 @@ import (
|
|||||||
// in an implementation-specific way.
|
// in an implementation-specific way.
|
||||||
func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) {
|
func MustPrintLauncherStateSimpleGlobal(w **tabwriter.Writer, runDir string) {
|
||||||
now := time.Now().UTC()
|
now := time.Now().UTC()
|
||||||
|
s := NewMulti(runDir)
|
||||||
|
|
||||||
// read runtime directory to get all UIDs
|
// read runtime directory to get all UIDs
|
||||||
if dirs, err := os.ReadDir(path.Join(runDir, "state")); err != nil && !errors.Is(err, os.ErrNotExist) {
|
if aids, err := s.List(); err != nil {
|
||||||
fmsg.Fatal("cannot read runtime directory:", err)
|
fmsg.Fatal("cannot list store:", err)
|
||||||
} else {
|
} else {
|
||||||
for _, e := range dirs {
|
for _, aid := range aids {
|
||||||
// skip non-directories
|
|
||||||
if !e.IsDir() {
|
|
||||||
fmsg.VPrintf("skipped non-directory entry %q", e.Name())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// skip non-numerical names
|
|
||||||
if _, err = strconv.Atoi(e.Name()); err != nil {
|
|
||||||
fmsg.VPrintf("skipped non-uid entry %q", e.Name())
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
// obtain temporary store
|
|
||||||
s := NewSimple(runDir, e.Name()).(*multiStore)
|
|
||||||
|
|
||||||
// print states belonging to this store
|
// print states belonging to this store
|
||||||
s.mustPrintLauncherState(w, now)
|
s.(*multiStore).mustPrintLauncherState(aid, w, now)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// mustPrintLauncherState causes store activity so store needs to be closed
|
// mustPrintLauncherState causes store activity so store needs to be closed
|
||||||
if err = s.Close(); err != nil {
|
if err := s.Close(); err != nil {
|
||||||
fmsg.Printf("cannot close store for user %q: %s", e.Name(), err)
|
fmsg.Printf("cannot close store: %v", err)
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *multiStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time) {
|
func (s *multiStore) mustPrintLauncherState(aid int, w **tabwriter.Writer, now time.Time) {
|
||||||
var innerErr error
|
var innerErr error
|
||||||
|
|
||||||
if ok, err := s.Do(func(b Backend) {
|
if ok, err := s.Do(aid, func(c Cursor) {
|
||||||
innerErr = func() error {
|
innerErr = func() error {
|
||||||
// read launcher states
|
// read launcher states
|
||||||
states, err := b.Load()
|
states, err := c.Load()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -111,25 +94,25 @@ func (s *multiStore) mustPrintLauncherState(w **tabwriter.Writer, now time.Time)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !fmsg.Verbose() {
|
if !fmsg.Verbose() {
|
||||||
_, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\t%s\t%s\n",
|
_, _ = fmt.Fprintf(*w, "\t%d\t%d\t%s\t%s\t%s\n",
|
||||||
state.PID, s.path[len(s.path)-1], now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), cs)
|
state.PID, aid, now.Sub(state.Time).Round(time.Second).String(), strings.TrimPrefix(ets.String(), ", "), cs)
|
||||||
} else {
|
} else {
|
||||||
// emit argv instead when verbose
|
// emit argv instead when verbose
|
||||||
_, _ = fmt.Fprintf(*w, "\t%d\t%s\t%s\n",
|
_, _ = fmt.Fprintf(*w, "\t%d\t%d\t%s\n",
|
||||||
state.PID, s.path[len(s.path)-1], state.ID)
|
state.PID, aid, state.ID)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}()
|
}()
|
||||||
}); err != nil {
|
}); err != nil {
|
||||||
fmsg.Printf("cannot perform action on store %q: %s", path.Join(s.path...), err)
|
fmsg.Printf("cannot perform action on app %d: %v", aid, err)
|
||||||
if !ok {
|
if !ok {
|
||||||
fmsg.Fatal("store faulted before printing")
|
fmsg.Fatal("store faulted before printing")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if innerErr != nil {
|
if innerErr != nil {
|
||||||
fmsg.Fatalf("cannot print launcher state for store %q: %s", path.Join(s.path...), innerErr)
|
fmsg.Fatalf("cannot print launcher state of app %d: %s", aid, innerErr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -6,21 +6,27 @@ import (
|
|||||||
"git.ophivana.moe/security/fortify/fst"
|
"git.ophivana.moe/security/fortify/fst"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type Entries map[fst.ID]*State
|
||||||
|
|
||||||
type Store interface {
|
type Store interface {
|
||||||
// Do calls f exactly once and ensures store exclusivity until f returns.
|
// Do calls f exactly once and ensures store exclusivity until f returns.
|
||||||
// Returns whether f is called and any errors during the locking process.
|
// Returns whether f is called and any errors during the locking process.
|
||||||
// Backend provided to f becomes invalid as soon as f returns.
|
// Cursor provided to f becomes invalid as soon as f returns.
|
||||||
Do(f func(b Backend)) (bool, error)
|
Do(aid int, f func(c Cursor)) (ok bool, err error)
|
||||||
|
|
||||||
|
// List queries the store and returns a list of aids known to the store.
|
||||||
|
// Note that some or all returned aids might not have any active apps.
|
||||||
|
List() (aids []int, err error)
|
||||||
|
|
||||||
// Close releases any resources held by Store.
|
// Close releases any resources held by Store.
|
||||||
Close() error
|
Close() error
|
||||||
}
|
}
|
||||||
|
|
||||||
// Backend provides access to the store
|
// Cursor provides access to the store
|
||||||
type Backend interface {
|
type Cursor interface {
|
||||||
Save(state *State) error
|
Save(state *State) error
|
||||||
Destroy(pid int) error
|
Destroy(id fst.ID) error
|
||||||
Load() ([]*State, error)
|
Load() (Entries, error)
|
||||||
Len() (int, error)
|
Len() (int, error)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
126
internal/state/state_test.go
Normal file
126
internal/state/state_test.go
Normal file
@ -0,0 +1,126 @@
|
|||||||
|
package state_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"math/rand/v2"
|
||||||
|
"reflect"
|
||||||
|
"slices"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"git.ophivana.moe/security/fortify/fst"
|
||||||
|
"git.ophivana.moe/security/fortify/internal/state"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testStore(t *testing.T, s state.Store) {
|
||||||
|
t.Run("list empty store", func(t *testing.T) {
|
||||||
|
if aids, err := s.List(); err != nil {
|
||||||
|
t.Fatalf("List: error = %v", err)
|
||||||
|
} else if len(aids) != 0 {
|
||||||
|
t.Fatalf("List: aids = %#v", aids)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const (
|
||||||
|
insertEntryChecked = iota
|
||||||
|
insertEntryNoCheck
|
||||||
|
insertEntryOtherApp
|
||||||
|
|
||||||
|
tl
|
||||||
|
)
|
||||||
|
|
||||||
|
var tc [tl]state.State
|
||||||
|
for i := 0; i < tl; i++ {
|
||||||
|
makeState(t, &tc[i])
|
||||||
|
}
|
||||||
|
|
||||||
|
do := func(aid int, f func(c state.Cursor)) {
|
||||||
|
if ok, err := s.Do(aid, f); err != nil {
|
||||||
|
t.Fatalf("Do: ok = %v, error = %v", ok, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
insert := func(i, aid int) {
|
||||||
|
do(aid, func(c state.Cursor) {
|
||||||
|
if err := c.Save(&tc[i]); err != nil {
|
||||||
|
t.Fatalf("Save(&tc[%v]): error = %v", i, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
check := func(i, aid int) {
|
||||||
|
do(aid, func(c state.Cursor) {
|
||||||
|
if entries, err := c.Load(); err != nil {
|
||||||
|
t.Fatalf("Load: error = %v", err)
|
||||||
|
} else if got, ok := entries[tc[i].ID]; !ok {
|
||||||
|
t.Fatalf("Load: entry %s missing",
|
||||||
|
&tc[i].ID)
|
||||||
|
} else {
|
||||||
|
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])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
t.Run("insert entry checked", func(t *testing.T) {
|
||||||
|
insert(insertEntryChecked, 0)
|
||||||
|
check(insertEntryChecked, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("insert entry unchecked", func(t *testing.T) {
|
||||||
|
insert(insertEntryNoCheck, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("insert entry different aid", func(t *testing.T) {
|
||||||
|
insert(insertEntryOtherApp, 1)
|
||||||
|
check(insertEntryOtherApp, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("check previous insertion", func(t *testing.T) {
|
||||||
|
check(insertEntryNoCheck, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("list aids", func(t *testing.T) {
|
||||||
|
if aids, err := s.List(); err != nil {
|
||||||
|
t.Fatalf("List: error = %v", err)
|
||||||
|
} else {
|
||||||
|
slices.Sort(aids)
|
||||||
|
want := []int{0, 1}
|
||||||
|
if slices.Compare(aids, want) != 0 {
|
||||||
|
t.Fatalf("List() = %#v, want %#v", aids, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("clear aid 1", func(t *testing.T) {
|
||||||
|
do(1, func(c state.Cursor) {
|
||||||
|
if err := c.Destroy(tc[insertEntryOtherApp].ID); err != nil {
|
||||||
|
t.Fatalf("Destroy: error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
do(1, func(c state.Cursor) {
|
||||||
|
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.Run("close store", func(t *testing.T) {
|
||||||
|
if err := s.Close(); err != nil {
|
||||||
|
t.Fatalf("Close: error = %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func makeState(t *testing.T, s *state.State) {
|
||||||
|
if err := fst.NewAppID(&s.ID); err != nil {
|
||||||
|
t.Fatalf("cannot create dummy state: %v", err)
|
||||||
|
}
|
||||||
|
s.Config = fst.Template()
|
||||||
|
s.PID = rand.Int()
|
||||||
|
s.Time = time.Now()
|
||||||
|
}
|
3
test.nix
3
test.nix
@ -214,5 +214,8 @@ nixosTest {
|
|||||||
swaymsg("exit", succeed=False)
|
swaymsg("exit", succeed=False)
|
||||||
machine.wait_until_fails("pgrep -x sway")
|
machine.wait_until_fails("pgrep -x sway")
|
||||||
machine.wait_for_file("/tmp/sway-exit-ok")
|
machine.wait_for_file("/tmp/sway-exit-ok")
|
||||||
|
|
||||||
|
# Print fortify runDir contents:
|
||||||
|
print(machine.succeed("find /run/user/1000/fortify"))
|
||||||
'';
|
'';
|
||||||
}
|
}
|
||||||
|
Loading…
Reference in New Issue
Block a user