From a2d947e1069a2952eb402573d08cf8d3cd8ec5c9 Mon Sep 17 00:00:00 2001 From: lash Date: Thu, 13 Apr 2023 00:38:33 +0100 Subject: [PATCH] Add persisted state engine runner --- dev/interactive/main.go | 2 +- engine/default.go | 12 ++++++-- engine/engine.go | 18 ++++++++++- engine/engine_test.go | 10 ++++-- engine/loop.go | 9 ++---- engine/loop_test.go | 21 ++++++++----- engine/persist.go | 28 +++++++++++++++++ engine/persist_test.go | 68 +++++++++++++++++++++++++++++++++++++++++ persist/fs.go | 11 +++++++ persist/persist.go | 7 +++++ state/state.go | 24 +++++++-------- state/state_test.go | 16 +++++----- 12 files changed, 186 insertions(+), 40 deletions(-) create mode 100644 engine/persist.go create mode 100644 engine/persist_test.go diff --git a/dev/interactive/main.go b/dev/interactive/main.go index 8af1f08..a52e8a6 100644 --- a/dev/interactive/main.go +++ b/dev/interactive/main.go @@ -21,7 +21,7 @@ func main() { ctx := context.Background() en := engine.NewSizedEngine(dir, uint32(size)) - err := engine.Loop(&en, root, ctx, os.Stdin, os.Stdout) + err := engine.Loop(&en, os.Stdin, os.Stdout, ctx) if err != nil { fmt.Fprintf(os.Stderr, "loop exited with error: %v", err) os.Exit(1) diff --git a/engine/default.go b/engine/default.go index 88f8c2b..1192115 100644 --- a/engine/default.go +++ b/engine/default.go @@ -1,6 +1,8 @@ package engine import ( + "context" + "git.defalsify.org/festive/cache" "git.defalsify.org/festive/resource" "git.defalsify.org/festive/state" @@ -11,7 +13,11 @@ func NewDefaultEngine(dir string) Engine { st := state.NewState(0) rs := resource.NewFsResource(dir) ca := cache.NewCache() - return NewEngine(Config{}, &st, &rs, ca) + cfg := Config{ + Root: "root", + } + ctx := context.TODO() + return NewEngine(cfg, &st, &rs, ca, ctx) } // NewSizedEngine is a convenience function to instantiate a filesystem-backed engine with a specified output constraint. @@ -21,6 +27,8 @@ func NewSizedEngine(dir string, size uint32) Engine { ca := cache.NewCache() cfg := Config{ OutputSize: size, + Root: "root", } - return NewEngine(cfg, &st, &rs, ca) + ctx := context.TODO() + return NewEngine(cfg, &st, &rs, ca, ctx) } diff --git a/engine/engine.go b/engine/engine.go index 883ea7a..5e8bc4f 100644 --- a/engine/engine.go +++ b/engine/engine.go @@ -16,6 +16,10 @@ import ( // Config globally defines behavior of all components driven by the engine. type Config struct { OutputSize uint32 // Maximum size of output from a single rendered page + SessionId string + Root string + FlagCount uint32 + CacheSize uint32 } // Engine is an execution engine that handles top-level errors when running client inputs against code in the bytecode buffer. @@ -24,10 +28,11 @@ type Engine struct { rs resource.Resource ca cache.Memory vm *vm.Vm + initd bool } // NewEngine creates a new Engine -func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memory) Engine { +func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memory, ctx context.Context) Engine { var szr *render.Sizer if cfg.OutputSize > 0 { szr = render.NewSizer(cfg.OutputSize) @@ -38,6 +43,9 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor ca: ca, vm: vm.NewVm(st, rs, ca, szr), } + if cfg.Root != "" { + engine.Init(cfg.Root, ctx) + } return engine } @@ -45,6 +53,13 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor // // It loads and executes code for the start node. func(en *Engine) Init(sym string, ctx context.Context) error { + if en.initd { + log.Printf("already initialized") + return nil + } + if sym == "" { + return fmt.Errorf("start sym empty") + } err := en.st.SetInput([]byte{}) if err != nil { return err @@ -55,6 +70,7 @@ func(en *Engine) Init(sym string, ctx context.Context) error { return err } en.st.SetCode(b) + en.initd = true return nil } diff --git a/engine/engine_test.go b/engine/engine_test.go index a1c72b3..f4463c5 100644 --- a/engine/engine_test.go +++ b/engine/engine_test.go @@ -40,12 +40,18 @@ func(fs FsWrapper) inky(sym string, ctx context.Context) (string, error) { return "tinkywinky", nil } +func(fs FsWrapper) pinky(sym string, ctx context.Context) (string, error) { + return "xyzzy", nil +} + func(fs FsWrapper) FuncFor(sym string) (resource.EntryFunc, error) { switch sym { case "one": return fs.one, nil case "inky": return fs.inky, nil + case "pinky": + return fs.pinky, nil } return nil, fmt.Errorf("function for %v not found", sym) } @@ -75,7 +81,7 @@ func TestEngineInit(t *testing.T) { rs := NewFsWrapper(dataDir, &st) ca := cache.NewCache().WithCacheSize(1024) - en := NewEngine(Config{}, &st, &rs, ca) + en := NewEngine(Config{}, &st, &rs, ca, ctx) err := en.Init("root", ctx) if err != nil { t.Fatal(err) @@ -129,7 +135,7 @@ func TestEngineExecInvalidInput(t *testing.T) { rs := NewFsWrapper(dataDir, &st) ca := cache.NewCache().WithCacheSize(1024) - en := NewEngine(Config{}, &st, &rs, ca) + en := NewEngine(Config{}, &st, &rs, ca, ctx) err := en.Init("root", ctx) if err != nil { t.Fatal(err) diff --git a/engine/loop.go b/engine/loop.go index 5b661ec..2d7f2ed 100644 --- a/engine/loop.go +++ b/engine/loop.go @@ -18,13 +18,8 @@ import ( // Any error not handled by the engine will terminate the oop and return an error. // // Rendered output is written to the provided writer. -func Loop(en *Engine, startSym string, ctx context.Context, reader io.Reader, writer io.Writer) error { - err := en.Init(startSym, ctx) - if err != nil { - return fmt.Errorf("cannot init: %v\n", err) - } - - err = en.WriteResult(writer, ctx) +func Loop(en *Engine, reader io.Reader, writer io.Writer, ctx context.Context) error { + err := en.WriteResult(writer, ctx) if err != nil { return err } diff --git a/engine/loop_test.go b/engine/loop_test.go index fd8c21a..9c86824 100644 --- a/engine/loop_test.go +++ b/engine/loop_test.go @@ -19,8 +19,11 @@ func TestLoopTop(t *testing.T) { st := state.NewState(0) rs := resource.NewFsResource(dataDir) ca := cache.NewCache().WithCacheSize(1024) - - en := NewEngine(Config{}, &st, &rs, ca) + + cfg := Config{ + Root: "root", + } + en := NewEngine(cfg, &st, &rs, ca, ctx) err := en.Init("root", ctx) if err != nil { t.Fatal(err) @@ -36,7 +39,7 @@ func TestLoopTop(t *testing.T) { outputBuf := bytes.NewBuffer(nil) log.Printf("running with input: %s", inputBuf.Bytes()) - err = Loop(&en, "root", ctx, inputBuf, outputBuf) + err = Loop(&en, inputBuf, outputBuf, ctx) if err != nil { t.Fatal(err) } @@ -53,7 +56,10 @@ func TestLoopBackForth(t *testing.T) { rs := resource.NewFsResource(dataDir) ca := cache.NewCache().WithCacheSize(1024) - en := NewEngine(Config{}, &st, &rs, ca) + cfg := Config{ + Root: "root", + } + en := NewEngine(cfg, &st, &rs, ca, ctx) err := en.Init("root", ctx) if err != nil { t.Fatal(err) @@ -70,7 +76,7 @@ func TestLoopBackForth(t *testing.T) { outputBuf := bytes.NewBuffer(nil) log.Printf("running with input: %s", inputBuf.Bytes()) - err = Loop(&en, "root", ctx, inputBuf, outputBuf) + err = Loop(&en, inputBuf, outputBuf, ctx) if err != nil { t.Fatal(err) } @@ -85,8 +91,9 @@ func TestLoopBrowse(t *testing.T) { cfg := Config{ OutputSize: 68, + Root: "root", } - en := NewEngine(cfg, &st, &rs, ca) + en := NewEngine(cfg, &st, &rs, ca, ctx) err := en.Init("root", ctx) if err != nil { t.Fatal(err) @@ -104,7 +111,7 @@ func TestLoopBrowse(t *testing.T) { outputBuf := bytes.NewBuffer(nil) log.Printf("running with input: %s", inputBuf.Bytes()) - err = Loop(&en, "root", ctx, inputBuf, outputBuf) + err = Loop(&en, inputBuf, outputBuf, ctx) if err != nil { t.Fatal(err) } diff --git a/engine/persist.go b/engine/persist.go new file mode 100644 index 0000000..f815093 --- /dev/null +++ b/engine/persist.go @@ -0,0 +1,28 @@ +package engine + +import ( + "context" + "io" + "log" + + "git.defalsify.org/festive/persist" + "git.defalsify.org/festive/resource" +) + +func RunPersisted(cfg Config, rs resource.Resource, pr persist.Persister, input []byte, w io.Writer, ctx context.Context) error { + err := pr.Load(cfg.SessionId) + if err != nil { + return err + } + st := pr.GetState() + log.Printf("st %v", st) + en := NewEngine(cfg, pr.GetState(), rs, pr.GetMemory(), ctx) + + if len(input) > 0 { + _, err = en.Exec(input, ctx) + if err != nil { + return err + } + } + return nil +} diff --git a/engine/persist_test.go b/engine/persist_test.go new file mode 100644 index 0000000..58992b0 --- /dev/null +++ b/engine/persist_test.go @@ -0,0 +1,68 @@ +package engine + +import ( + "bytes" + "context" + "errors" + "io/ioutil" + "os" + "testing" + + "git.defalsify.org/festive/cache" + "git.defalsify.org/festive/persist" + "git.defalsify.org/festive/state" +) + +func TestPersist(t *testing.T) { + generateTestData(t) + cfg := Config{ + OutputSize: 128, + SessionId: "xyzzy", + Root: "root", + } + rs := NewFsWrapper(dataDir, nil) + + persistDir, err := ioutil.TempDir("", "festive_engine_persist") + if err != nil { + t.Fatal(err) + } + + st := state.NewState(3) + ca := cache.NewCache().WithCacheSize(1024) + pr := persist.NewFsPersister(persistDir).WithContent(&st, ca) + + w := bytes.NewBuffer(nil) + ctx := context.TODO() + + + err = RunPersisted(cfg, rs, pr, []byte{}, w, ctx) + if err != nil { + if !errors.Is(err, os.ErrNotExist) { + t.Fatal(err) + } + st := state.NewState(cfg.FlagCount) + ca := cache.NewCache() + if cfg.CacheSize > 0 { + ca = ca.WithCacheSize(cfg.CacheSize) + } + pr = persist.NewFsPersister(persistDir).WithContent(&st, ca) + err = pr.Save(cfg.SessionId) + if err != nil { + t.Fatal(err) + } + } + + pr = persist.NewFsPersister(persistDir) + inputs := []string{ + "", + "1", + "2", + "00", + } + for _, v := range inputs { + err = RunPersisted(cfg, rs, pr, []byte(v), w, ctx) + if err != nil { + t.Fatal(err) + } + } +} diff --git a/persist/fs.go b/persist/fs.go index afb7d5b..57dbbec 100644 --- a/persist/fs.go +++ b/persist/fs.go @@ -2,6 +2,7 @@ package persist import ( "io/ioutil" + "log" "path" "path/filepath" "github.com/fxamacker/cbor/v2" @@ -32,6 +33,14 @@ func(p *FsPersister) WithContent(st *state.State, ca *cache.Cache) *FsPersister return p } +func(p *FsPersister) GetState() *state.State { + return p.State +} + +func(p *FsPersister) GetMemory() cache.Memory { + return p.Memory +} + func(p *FsPersister) Serialize() ([]byte, error) { return cbor.Marshal(p) } @@ -47,6 +56,7 @@ func(p *FsPersister) Save(key string) error { return err } fp := path.Join(p.dir, key) + log.Printf("saved key %v", key) return ioutil.WriteFile(fp, b, 0600) } @@ -57,5 +67,6 @@ func(p *FsPersister) Load(key string) error { return err } err = p.Deserialize(b) + log.Printf("loaded key %v", key) return err } diff --git a/persist/persist.go b/persist/persist.go index d57cdb3..9eb64d3 100644 --- a/persist/persist.go +++ b/persist/persist.go @@ -1,9 +1,16 @@ package persist +import ( + "git.defalsify.org/festive/cache" + "git.defalsify.org/festive/state" +) + type Persister interface { Serialize() ([]byte, error) Deserialize(b []byte) error Save(key string) error Load(key string) error + GetState() *state.State + GetMemory() cache.Memory } diff --git a/state/state.go b/state/state.go index 91405e1..08abe72 100644 --- a/state/state.go +++ b/state/state.go @@ -33,7 +33,7 @@ type State struct { ExecPath []string // Command symbols stack BitSize uint32 // size of (32-bit capacity) bit flag byte array SizeIdx uint16 - flags []byte // Error state + Flags []byte // Error state input []byte // Last input } @@ -64,9 +64,9 @@ func NewState(BitSize uint32) State { } byteSize := toByteSize(BitSize + 8) if byteSize > 0 { - st.flags = make([]byte, byteSize) + st.Flags = make([]byte, byteSize) } else { - st.flags = []byte{} + st.Flags = []byte{} } return st } @@ -80,14 +80,14 @@ func(st *State) SetFlag(bitIndex uint32) (bool, error) { if bitIndex + 1 > st.BitSize { return false, fmt.Errorf("bit index %v is out of range of bitfield size %v", bitIndex, st.BitSize) } - r := getFlag(bitIndex, st.flags) + r := getFlag(bitIndex, st.Flags) if r { return false, nil } byteIndex := bitIndex / 8 localBitIndex := bitIndex % 8 - b := st.flags[byteIndex] - st.flags[byteIndex] = b | (1 << localBitIndex) + b := st.Flags[byteIndex] + st.Flags[byteIndex] = b | (1 << localBitIndex) return true, nil } @@ -101,14 +101,14 @@ func(st *State) ResetFlag(bitIndex uint32) (bool, error) { if bitIndex + 1 > st.BitSize { return false, fmt.Errorf("bit index %v is out of range of bitfield size %v", bitIndex, st.BitSize) } - r := getFlag(bitIndex, st.flags) + r := getFlag(bitIndex, st.Flags) if !r { return false, nil } byteIndex := bitIndex / 8 localBitIndex := bitIndex % 8 - b := st.flags[byteIndex] - st.flags[byteIndex] = b & (^(1 << localBitIndex)) + b := st.Flags[byteIndex] + st.Flags[byteIndex] = b & (^(1 << localBitIndex)) return true, nil } @@ -119,7 +119,7 @@ func(st *State) GetFlag(bitIndex uint32) (bool, error) { if bitIndex + 1 > st.BitSize { return false, fmt.Errorf("bit index %v is out of range of bitfield size %v", bitIndex, st.BitSize) } - return getFlag(bitIndex, st.flags), nil + return getFlag(bitIndex, st.Flags), nil } // FlagBitSize reports the amount of bits available in the bit field index. @@ -129,7 +129,7 @@ func(st *State) FlagBitSize() uint32 { // FlagBitSize reports the amount of bits available in the bit field index. func(st *State) FlagByteSize() uint8 { - return uint8(len(st.flags)) + return uint8(len(st.Flags)) } // MatchFlag matches the current state of the given flag. @@ -169,7 +169,7 @@ func(st *State) GetIndex(flags []byte) bool { var i uint32 for i = 0; i < st.BitSize; i++ { testVal := flags[byteIndex] & (1 << localIndex) - if (testVal & st.flags[byteIndex]) > 0 { + if (testVal & st.Flags[byteIndex]) > 0 { return true } globalIndex += 1 diff --git a/state/state_test.go b/state/state_test.go index a22051f..6d75b3c 100644 --- a/state/state_test.go +++ b/state/state_test.go @@ -8,16 +8,16 @@ import ( // Check creation func TestNewState(t *testing.T) { st := NewState(5) - if len(st.flags) != 2 { - t.Fatalf("invalid state flag length: %v", len(st.flags)) + if len(st.Flags) != 2 { + t.Fatalf("invalid state flag length: %v", len(st.Flags)) } st = NewState(8) - if len(st.flags) != 2 { - t.Fatalf("invalid state flag length: %v", len(st.flags)) + if len(st.Flags) != 2 { + t.Fatalf("invalid state flag length: %v", len(st.Flags)) } st = NewState(17) - if len(st.flags) != 4 { - t.Fatalf("invalid state flag length: %v", len(st.flags)) + if len(st.Flags) != 4 { + t.Fatalf("invalid state flag length: %v", len(st.Flags)) } } @@ -98,8 +98,8 @@ func TestStateflags(t *testing.T) { if err == nil { t.Fatalf("Expected out of range for bit index 17") } - if !bytes.Equal(st.flags[:3], []byte{0x04, 0x04, 0x01}) { - t.Fatalf("Expected 0x040401, got %v", st.flags[:3]) + if !bytes.Equal(st.Flags[:3], []byte{0x04, 0x04, 0x01}) { + t.Fatalf("Expected 0x040401, got %v", st.Flags[:3]) } }