- 基于 gopher-lua 实现类似 OpenResty 的脚本嵌入能力 - LuaEngine: server 级单 LState + 请求级临时协程 - LuaContext: 请求上下文,变量存储和阶段管理 - LuaCoroutine: 沙箱隔离,Yield/Resume 循环,执行超时 - CodeCache: 字节码缓存,LRU 淘汰 + TTL 过期 - 新增 testify 用于测试断言 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
189 lines
4.3 KiB
Go
189 lines
4.3 KiB
Go
// Package lua 提供 Lua 脚本嵌入能力
|
||
package lua
|
||
|
||
import (
|
||
"context"
|
||
"fmt"
|
||
"sync/atomic"
|
||
"time"
|
||
|
||
glua "github.com/yuin/gopher-lua"
|
||
"github.com/valyala/fasthttp"
|
||
)
|
||
|
||
// Phase 处理阶段
|
||
type Phase int
|
||
|
||
const (
|
||
PhaseInit Phase = iota
|
||
PhaseRewrite
|
||
PhaseAccess
|
||
PhaseContent
|
||
PhaseLog
|
||
PhaseHeaderFilter
|
||
PhaseBodyFilter
|
||
)
|
||
|
||
func (p Phase) String() string {
|
||
switch p {
|
||
case PhaseInit:
|
||
return "init"
|
||
case PhaseRewrite:
|
||
return "rewrite"
|
||
case PhaseAccess:
|
||
return "access"
|
||
case PhaseContent:
|
||
return "content"
|
||
case PhaseLog:
|
||
return "log"
|
||
case PhaseHeaderFilter:
|
||
return "header_filter"
|
||
case PhaseBodyFilter:
|
||
return "body_filter"
|
||
default:
|
||
return "unknown"
|
||
}
|
||
}
|
||
|
||
// LuaCoroutine 请求级临时协程
|
||
// 注意:协程在 ResumeOK 后变成 dead 状态,不能复用
|
||
type LuaCoroutine struct {
|
||
// 所属引擎
|
||
Engine *LuaEngine
|
||
|
||
// 协程 LState(通过 NewThread 创建)
|
||
Co *glua.LState
|
||
|
||
// 取消函数
|
||
Cancel context.CancelFunc
|
||
|
||
// 请求上下文
|
||
RequestCtx *fasthttp.RequestCtx
|
||
|
||
// 执行上下文
|
||
ExecutionContext context.Context
|
||
executionCancel context.CancelFunc
|
||
|
||
// 创建时间
|
||
CreatedAt time.Time
|
||
|
||
// 状态
|
||
Exited bool // 是否已调用 exit
|
||
|
||
// 输出缓冲
|
||
OutputBuffer []byte
|
||
}
|
||
|
||
// SetupSandbox 创建 per-request _ENV 沙箱
|
||
// 每个请求创建独立的 _ENV 表,通过元表继承全局环境
|
||
func (c *LuaCoroutine) SetupSandbox() error {
|
||
// 创建独立的 _ENV 表
|
||
env := c.Co.NewTable()
|
||
|
||
// 获取全局环境 - 使用 Engine 的主 LState 全局表
|
||
// 协程通过 NewThread 继承了父 LState 的全局环境
|
||
globals := c.Engine.L.GetGlobal("_G")
|
||
|
||
// 设置元表,使未找到的变量从全局环境读取
|
||
mt := c.Co.NewTable()
|
||
mt.RawSetString("__index", globals)
|
||
|
||
// 阻止写入全局环境(可选)
|
||
readOnlyFn := c.Co.NewFunction(func(L *glua.LState) int {
|
||
L.RaiseError("attempt to modify global table (read-only)")
|
||
return 0
|
||
})
|
||
mt.RawSetString("__newindex", readOnlyFn)
|
||
|
||
// 设置元表
|
||
c.Co.SetMetatable(env, mt)
|
||
|
||
// 将 _ENV 设置到协程
|
||
c.Co.SetGlobal("_ENV", env)
|
||
|
||
return nil
|
||
}
|
||
|
||
// Execute 在协程中执行 Lua 脚本(支持 Yield/Resume)
|
||
func (c *LuaCoroutine) Execute(script string) error {
|
||
proto, err := c.Engine.codeCache.GetOrCompileInline(script)
|
||
if err != nil {
|
||
return fmt.Errorf("compile script: %w", err)
|
||
}
|
||
return c.executeProto(proto)
|
||
}
|
||
|
||
// ExecuteFile 执行文件脚本
|
||
func (c *LuaCoroutine) ExecuteFile(path string) error {
|
||
proto, err := c.Engine.codeCache.GetOrCompileFile(path)
|
||
if err != nil {
|
||
return fmt.Errorf("compile file: %w", err)
|
||
}
|
||
return c.executeProto(proto)
|
||
}
|
||
|
||
// executeProto 执行编译后的字节码,处理 yield/resume 循环
|
||
func (c *LuaCoroutine) executeProto(proto *glua.FunctionProto) error {
|
||
fn := c.Engine.L.NewFunctionFromProto(proto)
|
||
st, execErr, values := c.Engine.L.Resume(c.Co, fn)
|
||
|
||
for st == glua.ResumeYield {
|
||
results, handleErr := c.handleYield(values)
|
||
if handleErr != nil {
|
||
return fmt.Errorf("handle yield: %w", handleErr)
|
||
}
|
||
st, execErr, values = c.Engine.L.Resume(c.Co, nil, results...)
|
||
}
|
||
|
||
if st == glua.ResumeError {
|
||
atomic.AddUint64(&c.Engine.stats.ScriptsErrors, 1)
|
||
return fmt.Errorf("lua execution error: %w", execErr)
|
||
}
|
||
|
||
atomic.AddUint64(&c.Engine.stats.ScriptsExecuted, 1)
|
||
return nil
|
||
}
|
||
|
||
// handleYield 处理协程 yield
|
||
func (c *LuaCoroutine) handleYield(values []glua.LValue) ([]glua.LValue, error) {
|
||
if len(values) == 0 {
|
||
return nil, fmt.Errorf("yield without reason")
|
||
}
|
||
|
||
reason := glua.LVAsString(values[0])
|
||
|
||
switch reason {
|
||
case "sleep":
|
||
return c.handleSleep(values[1:])
|
||
default:
|
||
return nil, fmt.Errorf("unknown yield reason: %s", reason)
|
||
}
|
||
}
|
||
|
||
// handleSleep 处理 sleep yield
|
||
// 注意:此实现会阻塞当前 goroutine
|
||
func (c *LuaCoroutine) handleSleep(values []glua.LValue) ([]glua.LValue, error) {
|
||
if len(values) == 0 {
|
||
return nil, fmt.Errorf("sleep requires duration")
|
||
}
|
||
|
||
duration := float64(glua.LVAsNumber(values[0]))
|
||
d := time.Duration(duration * float64(time.Second))
|
||
|
||
timer := time.NewTimer(d)
|
||
defer timer.Stop()
|
||
|
||
select {
|
||
case <-timer.C:
|
||
// sleep 完成,返回空结果
|
||
return []glua.LValue{}, nil
|
||
case <-c.ExecutionContext.Done():
|
||
// 执行超时或取消
|
||
return nil, fmt.Errorf("sleep interrupted: %w", c.ExecutionContext.Err())
|
||
}
|
||
}
|
||
|
||
// Close 关闭协程
|
||
func (c *LuaCoroutine) Close() {
|
||
c.Engine.releaseCoroutine(c)
|
||
} |