go-vise

Constrained Size Output Virtual Machine
Info | Log | Files | Refs | README | LICENSE

commit e9267c22cf90e505139335e2f6a7b08aa32d6ee2
parent b922c3fb94942c4d980548bd44ad813ecd8dcad4
Author: lash <dev@holbrook.no>
Date:   Thu, 20 Apr 2023 16:55:11 +0100

Recover non-root state on engine persist

Diffstat:
Mcache/cache.go | 5+++--
Mdev/interactive/main.go | 14++++++++++----
Mengine/default.go | 68++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++----
Mengine/engine.go | 26++++++++++++++++++++++++++
Mengine/loop.go | 3++-
Mengine/persist.go | 24+++++++++++++++++++-----
Mpersist/fs.go | 21++++++++++++++++++---
Mpersist/persist.go | 4+++-
Mrender/page.go | 9+++++++++
Arender/render.go | 6++++++
Mvm/runner.go | 14++++++--------
11 files changed, 166 insertions(+), 28 deletions(-)

diff --git a/cache/cache.go b/cache/cache.go @@ -44,11 +44,12 @@ func(ca *Cache) Add(key string, value string, sizeLimit uint16) error { } checkFrame := ca.frameOf(key) if checkFrame > -1 { - if checkFrame == len(ca.Cache) - 1 { + thisFrame := len(ca.Cache) - 1 + if checkFrame == thisFrame { Logg.Debugf("Ignoring load request on frame that has symbol already loaded") return nil } - return fmt.Errorf("key %v already defined in frame %v", key, checkFrame) + return fmt.Errorf("key %v already defined in frame %v, this is frame %v", key, checkFrame, thisFrame) } var sz uint32 if len(value) > 0 { diff --git a/dev/interactive/main.go b/dev/interactive/main.go @@ -13,16 +13,22 @@ func main() { var dir string var root string var size uint - //var sessionId string + var sessionId string + var persist bool flag.StringVar(&dir, "d", ".", "resource dir to read from") flag.UintVar(&size, "s", 0, "max size of output") flag.StringVar(&root, "root", "root", "entry point symbol") - //flag.StringVar(&sessionId, "session-id", "default", "session id") + flag.StringVar(&sessionId, "session-id", "default", "session id") + flag.BoolVar(&persist, "persist", true, "use state persistence") flag.Parse() fmt.Fprintf(os.Stderr, "starting session at symbol '%s' using resource dir: %s\n", root, dir) ctx := context.Background() - en := engine.NewSizedEngine(dir, uint32(size)) + en, err := engine.NewSizedEngine(dir, uint32(size), persist, &sessionId) + if err != nil { + fmt.Fprintf(os.Stderr, "engine create error: %v", err) + os.Exit(1) + } cont, err := en.Init(ctx) if err != nil { fmt.Fprintf(os.Stderr, "engine init exited with error: %v\n", err) @@ -37,7 +43,7 @@ func main() { os.Stdout.Write([]byte{0x0a}) os.Exit(0) } - err = engine.Loop(&en, os.Stdin, os.Stdout, ctx) + err = engine.Loop(en, os.Stdin, os.Stdout, ctx) if err != nil { fmt.Fprintf(os.Stderr, "loop exited with error: %v\n", err) os.Exit(1) diff --git a/engine/default.go b/engine/default.go @@ -2,26 +2,59 @@ package engine import ( "context" + "fmt" + "os" + "path" "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/persist" "git.defalsify.org/vise.git/resource" "git.defalsify.org/vise.git/state" ) // NewDefaultEngine is a convenience function to instantiate a filesystem-backed engine with no output constraints. -func NewDefaultEngine(dir string) Engine { +func NewDefaultEngine(dir string, persisted bool, session *string) (EngineIsh, error) { + var err error st := state.NewState(0) rs := resource.NewFsResource(dir) ca := cache.NewCache() cfg := Config{ Root: "root", } + if session != nil { + cfg.SessionId = *session + } else if !persisted { + return nil, fmt.Errorf("session must be set if persist is used") + } ctx := context.TODO() - return NewEngine(cfg, &st, &rs, ca, ctx) + var en EngineIsh + if persisted { + dp := path.Join(dir, ".state") + err = os.MkdirAll(dp, 0700) + if err != nil { + return nil, err + } + pr := persist.NewFsPersister(dp) + en, err = NewPersistedEngine(cfg, pr, &rs, ctx) + if err != nil { + Logg.Infof("persisted engine create error. trying again with persisting empty state first...") + pr = pr.WithContent(&st, ca) + err = pr.Save(cfg.SessionId, nil) + if err != nil { + return nil, err + } + en, err = NewPersistedEngine(cfg, pr, &rs, ctx) + } + } else { + enb := NewEngine(cfg, &st, &rs, ca, ctx) + en = &enb + } + return en, err } // NewSizedEngine is a convenience function to instantiate a filesystem-backed engine with a specified output constraint. -func NewSizedEngine(dir string, size uint32) Engine { +func NewSizedEngine(dir string, size uint32, persisted bool, session *string) (EngineIsh, error) { + var err error st := state.NewState(0) rs := resource.NewFsResource(dir) ca := cache.NewCache() @@ -29,6 +62,33 @@ func NewSizedEngine(dir string, size uint32) Engine { OutputSize: size, Root: "root", } + if session != nil { + cfg.SessionId = *session + } else if !persisted { + return nil, fmt.Errorf("session must be set if persist is used") + } ctx := context.TODO() - return NewEngine(cfg, &st, &rs, ca, ctx) + var en EngineIsh + if persisted { + dp := path.Join(dir, ".state") + err = os.MkdirAll(dp, 0700) + if err != nil { + return nil, err + } + pr := persist.NewFsPersister(dp) + en, err = NewPersistedEngine(cfg, pr, &rs, ctx) + if err != nil { + Logg.Infof("persisted engine create error. trying again with persisting empty state first...") + pr = pr.WithContent(&st, ca) + err = pr.Save(cfg.SessionId, nil) + if err != nil { + return nil, err + } + en, err = NewPersistedEngine(cfg, pr, &rs, ctx) + } + } else { + enb := NewEngine(cfg, &st, &rs, ca, ctx) + en = &enb + } + return en, err } diff --git a/engine/engine.go b/engine/engine.go @@ -12,6 +12,13 @@ import ( "git.defalsify.org/vise.git/vm" ) +type EngineIsh interface { + Init(ctx context.Context) (bool, error) + Exec(input []byte, ctx context.Context) (bool, error) + WriteResult(w io.Writer, ctx context.Context) (int, error) + Finish() error +} + // 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 @@ -51,10 +58,26 @@ func NewEngine(cfg Config, st *state.State, rs resource.Resource, ca cache.Memor return engine } +// Finish implements EngineIsh interface +func(en *Engine) Finish() error { + return nil +} + +func(en *Engine) restore() { + location, _ := en.st.Where() + if len(location) == 0 { + return + } + if en.root != location { + en.root = "." //location + } +} + // Init must be explicitly called before using the Engine instance. // // It loads and executes code for the start node. func(en *Engine) Init(ctx context.Context) (bool, error) { + en.restore() if en.initd { Logg.DebugCtxf(ctx, "already initialized") return true, nil @@ -111,7 +134,10 @@ func (en *Engine) Exec(input []byte, ctx context.Context) (bool, error) { if err != nil { return false, err } + return en.exec(input, ctx) +} +func(en *Engine) exec(input []byte, ctx context.Context) (bool, error) { Logg.InfoCtxf(ctx, "new VM execution with input", "input", string(input)) code, err := en.st.GetCode() if err != nil { diff --git a/engine/loop.go b/engine/loop.go @@ -17,7 +17,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, reader io.Reader, writer io.Writer, ctx context.Context) error { +func Loop(en EngineIsh, reader io.Reader, writer io.Writer, ctx context.Context) error { + defer en.Finish() var err error _, err = en.WriteResult(writer, ctx) if err != nil { diff --git a/engine/persist.go b/engine/persist.go @@ -8,12 +8,14 @@ import ( "git.defalsify.org/vise.git/resource" ) +// PersistedEngine adds persisted state to the Engine object. It provides a persisted state option for synchronous/interactive clients. type PersistedEngine struct { *Engine pr persist.Persister } +// NewPersistedEngine creates a new PersistedEngine func NewPersistedEngine(cfg Config, pr persist.Persister, rs resource.Resource, ctx context.Context) (PersistedEngine, error) { err := pr.Load(cfg.SessionId) if err != nil { @@ -21,23 +23,32 @@ func NewPersistedEngine(cfg Config, pr persist.Persister, rs resource.Resource, } st := pr.GetState() ca := pr.GetMemory() + enb := NewEngine(cfg, st, rs, ca, ctx) en := PersistedEngine{ &enb, pr, } - return en, nil + return en, err } -func(pe *PersistedEngine) Exec(input []byte, ctx context.Context) (bool, error) { +// Exec executes the parent method Engine.Exec, and afterwards persists the new state. +func(pe PersistedEngine) Exec(input []byte, ctx context.Context) (bool, error) { v, err := pe.Engine.Exec(input, ctx) if err != nil { return v, err } - err = pe.pr.Save(pe.Engine.session) + renderer := pe.Engine.vm.Renderer() + err = pe.pr.Save(pe.Engine.session, renderer) return v, err } +// Finish implements EngineIsh interface +func(pe PersistedEngine) Finish() error { + renderer := pe.Engine.vm.Renderer() + return pe.pr.Save(pe.Engine.session, renderer) +} + // RunPersisted performs a single vm execution from client input using a persisted state. // // State is first loaded from storage. The vm is initialized with the state and executed. The new state is then saved to storage. @@ -60,7 +71,8 @@ func RunPersisted(cfg Config, rs resource.Resource, pr persist.Persister, input if err != nil { return err } - err = pr.Save(cfg.SessionId) + renderer := en.vm.Renderer() + err = pr.Save(cfg.SessionId, renderer) if err != nil { return err } @@ -76,5 +88,7 @@ func RunPersisted(cfg Config, rs resource.Resource, pr persist.Persister, input if err != nil { return err } - return pr.Save(cfg.SessionId) + en.Finish() + renderer = en.vm.Renderer() + return pr.Save(cfg.SessionId, renderer) } diff --git a/persist/fs.go b/persist/fs.go @@ -7,6 +7,7 @@ import ( "github.com/fxamacker/cbor/v2" "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/render" "git.defalsify.org/vise.git/state" ) @@ -14,6 +15,7 @@ import ( type FsPersister struct { State *state.State Memory *cache.Cache + MapKeys []string dir string } @@ -39,6 +41,12 @@ func(p *FsPersister) WithContent(st *state.State, ca *cache.Cache) *FsPersister return p } +// WithRenderer extracts the mapped keys to add to serialization. +func(p *FsPersister) WithRenderer(pg render.Renderer) *FsPersister { + p.MapKeys = pg.Keys() + return p +} + // GetState implements the Persister interface. func(p *FsPersister) GetState() *state.State { return p.State @@ -61,13 +69,16 @@ func(p *FsPersister) Deserialize(b []byte) error { } // GetState implements the Persister interface. -func(p *FsPersister) Save(key string) error { +func(p *FsPersister) Save(key string, renderer render.Renderer) error { + if renderer != nil { + p = p.WithRenderer(renderer) + } b, err := p.Serialize() if err != nil { return err } fp := path.Join(p.dir, key) - Logg.Debugf("saved state and cache", "key", key, "bytecode", p.State.Code) + Logg.Debugf("saved state and cache", "key", key, "bytecode", p.State.Code, "map", p.MapKeys) return ioutil.WriteFile(fp, b, 0600) } @@ -79,6 +90,10 @@ func(p *FsPersister) Load(key string) error { return err } err = p.Deserialize(b) - Logg.Debugf("loaded state and cache", "key", key, "bytecode", p.State.Code) + Logg.Debugf("loaded state and cache", "key", key, "bytecode", p.State.Code, "map", p.MapKeys) return err } + +func(p *FsPersister) GetKeys() []string { + return p.MapKeys +} diff --git a/persist/persist.go b/persist/persist.go @@ -2,6 +2,7 @@ package persist import ( "git.defalsify.org/vise.git/cache" + "git.defalsify.org/vise.git/render" "git.defalsify.org/vise.git/state" ) @@ -9,9 +10,10 @@ import ( type Persister interface { Serialize() ([]byte, error) // Output serializes representation of the state. Deserialize(b []byte) error // Restore state from a serialized state. - Save(key string) error // Serialize and commit the state representation to persisted storage. + Save(key string, renderer render.Renderer) error // Serialize and commit the state representation to persisted storage. Load(key string) error // Load the state representation from persisted storage and Deserialize. GetState() *state.State // Get the currently loaded State object. GetMemory() cache.Memory // Get the currently loaded Cache object. + GetKeys() []string // Get all mapped keys for renderer. } diff --git a/render/page.go b/render/page.go @@ -128,6 +128,15 @@ func(pg *Page) Sizes() (map[string]uint16, error) { return sizes, nil } +// Keys returns all mapped symbols. +func(pg *Page) Keys() []string { + var r []string + for k, _ := range pg.cacheMap { + r = append(r, k) + } + return r +} + // RenderTemplate is an adapter to implement the builtin golang text template renderer as resource.RenderTemplate. func(pg *Page) RenderTemplate(sym string, values map[string]string, idx uint16) (string, error) { tpl, err := pg.resource.GetTemplate(sym) diff --git a/render/render.go b/render/render.go @@ -0,0 +1,6 @@ +package render + +type Renderer interface { + Keys() []string + Map(key string) error +} diff --git a/vm/runner.go b/vm/runner.go @@ -34,6 +34,11 @@ func NewVm(st *state.State, rs resource.Resource, ca cache.Memory, sizer *render return vmi } +// Renderer returns the current state of the renderer operated on by the vm. +func(vmi *Vm) Renderer() render.Renderer { + return vmi.pg +} + // Reset re-initializes sub-components for output rendering. func(vmi *Vm) Reset() { vmi.mn = render.NewMenu() @@ -281,14 +286,7 @@ func(vm *Vm) RunMove(b []byte, ctx context.Context) ([]byte, error) { if err != nil { return b, err } - if sym == "_" { - vm.st.Up() - vm.ca.Pop() - sym, _ = vm.st.Where() - } else { - vm.st.Down(sym) - vm.ca.Push() - } + sym, _, err = applyTarget([]byte(sym), vm.st, vm.ca, ctx) code, err := vm.rs.GetCode(sym) if err != nil { return b, err