package controllogic import ( "sync" lua "github.com/yuin/gopher-lua" ) // luaRuntime holds a sandboxed Lua VM for one action.lua node. The VM is created // lazily and reused; access is serialised so concurrent flow activations of the // same node don't share VM state unsafely. Host functions exposed to scripts: // // get("ds:name") → number read a signal / sys / local value (NaN if absent) // set("ds:name", v) write a value (bare name → graph-local var) // log(msg) append to the server log // // The os, io, package, debug and code-loading globals are removed, mirroring the // dsp LuaNode sandbox. type luaRuntime struct { script string mu sync.Mutex L *lua.LState // Bound to the current activation while run() holds mu. curResolve Resolver curSet func(target string, val float64) curLog func(msg string) } func newLuaRuntime(script string) *luaRuntime { return &luaRuntime{script: script} } func (lr *luaRuntime) ensure() error { if lr.L != nil { return nil } L := lua.NewState(lua.Options{SkipOpenLibs: true}) for _, pair := range []struct { name string fn lua.LGFunction }{ {lua.LoadLibName, lua.OpenPackage}, {lua.BaseLibName, lua.OpenBase}, {lua.MathLibName, lua.OpenMath}, {lua.StringLibName, lua.OpenString}, {lua.TabLibName, lua.OpenTable}, } { if err := L.CallByParam(lua.P{Fn: L.NewFunction(pair.fn), NRet: 0, Protect: true}, lua.LString(pair.name)); err != nil { L.Close() return err } } L.SetGlobal("load", lua.LNil) L.SetGlobal("loadfile", lua.LNil) L.SetGlobal("dofile", lua.LNil) L.SetGlobal("require", lua.LNil) L.SetGlobal("get", L.NewFunction(func(s *lua.LState) int { target := s.CheckString(1) ds, name, ok := parseRef(target) var v float64 if ok && lr.curResolve != nil { v = lr.curResolve(ds, name) } s.Push(lua.LNumber(v)) return 1 })) L.SetGlobal("set", L.NewFunction(func(s *lua.LState) int { target := s.CheckString(1) val := float64(s.CheckNumber(2)) if lr.curSet != nil { lr.curSet(target, val) } return 0 })) L.SetGlobal("log", L.NewFunction(func(s *lua.LState) int { if lr.curLog != nil { lr.curLog(s.CheckString(1)) } return 0 })) lr.L = L return nil } // run executes the script once with the given host hooks bound. func (lr *luaRuntime) run(resolve Resolver, set func(string, float64), logf func(string)) { lr.mu.Lock() defer lr.mu.Unlock() if err := lr.ensure(); err != nil { logf("lua init error: " + err.Error()) return } lr.curResolve = resolve lr.curSet = set lr.curLog = logf defer func() { lr.curResolve = nil lr.curSet = nil lr.curLog = nil if r := recover(); r != nil { logf("lua panic") } }() lr.L.SetTop(0) if err := lr.L.DoString(lr.script); err != nil { logf("lua error: " + err.Error()) } }