337 lines
7.9 KiB
Go
337 lines
7.9 KiB
Go
package runner
|
|
|
|
import (
|
|
"context"
|
|
"errors"
|
|
"sync"
|
|
"sync/atomic"
|
|
|
|
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
|
)
|
|
|
|
// Common errors
|
|
var (
|
|
ErrRunnerClosed = errors.New("lua runner is closed")
|
|
ErrInitFailed = errors.New("initialization failed")
|
|
)
|
|
|
|
// StateInitFunc is a function that initializes a Lua state
|
|
type StateInitFunc func(*luajit.State) error
|
|
|
|
// LuaRunner runs Lua scripts using a single Lua state
|
|
type LuaRunner struct {
|
|
state *luajit.State // The Lua state
|
|
jobQueue chan job // Channel for incoming jobs
|
|
isRunning atomic.Bool // Flag indicating if the runner is active
|
|
mu sync.RWMutex // Mutex for thread safety
|
|
wg sync.WaitGroup // WaitGroup for clean shutdown
|
|
initFunc StateInitFunc // Optional function to initialize Lua state
|
|
bufferSize int // Size of the job queue buffer
|
|
requireCache *RequireCache // Cache for required modules
|
|
requireCfg *RequireConfig // Configuration for require paths
|
|
moduleLoader luajit.GoFunction // Keep reference to prevent GC
|
|
sandbox *Sandbox // The sandbox environment
|
|
}
|
|
|
|
// NewRunner creates a new LuaRunner
|
|
func NewRunner(options ...RunnerOption) (*LuaRunner, error) {
|
|
// Default configuration
|
|
runner := &LuaRunner{
|
|
bufferSize: 10, // Default buffer size
|
|
requireCache: NewRequireCache(),
|
|
requireCfg: &RequireConfig{
|
|
LibDirs: []string{},
|
|
},
|
|
sandbox: NewSandbox(),
|
|
}
|
|
|
|
// Apply options
|
|
for _, opt := range options {
|
|
opt(runner)
|
|
}
|
|
|
|
// Initialize Lua state
|
|
state := luajit.New()
|
|
if state == nil {
|
|
return nil, errors.New("failed to create Lua state")
|
|
}
|
|
runner.state = state
|
|
|
|
// Create job queue
|
|
runner.jobQueue = make(chan job, runner.bufferSize)
|
|
runner.isRunning.Store(true)
|
|
|
|
// Create a shared config pointer that will be updated per request
|
|
runner.requireCfg = &RequireConfig{
|
|
ScriptDir: runner.scriptDir(),
|
|
LibDirs: runner.libDirs(),
|
|
}
|
|
|
|
// Set up require functionality
|
|
moduleLoader := func(s *luajit.State) int {
|
|
// Get module name
|
|
modName := s.ToString(1)
|
|
if modName == "" {
|
|
s.PushString("module name required")
|
|
return -1
|
|
}
|
|
|
|
// Find and compile module
|
|
bytecode, err := findAndCompileModule(s, runner.requireCache, *runner.requireCfg, modName)
|
|
if err != nil {
|
|
if err == ErrModuleNotFound {
|
|
s.PushString("module '" + modName + "' not found")
|
|
} else {
|
|
s.PushString("error loading module: " + err.Error())
|
|
}
|
|
return -1 // Return error
|
|
}
|
|
|
|
// Load the bytecode
|
|
if err := s.LoadBytecode(bytecode, modName); err != nil {
|
|
s.PushString("error loading bytecode: " + err.Error())
|
|
return -1 // Return error
|
|
}
|
|
|
|
// Return the loaded function
|
|
return 1
|
|
}
|
|
|
|
// Store reference to prevent garbage collection
|
|
runner.moduleLoader = moduleLoader
|
|
|
|
// Register with Lua state
|
|
if err := state.RegisterGoFunction("__go_load_module", moduleLoader); err != nil {
|
|
state.Close()
|
|
return nil, ErrInitFailed
|
|
}
|
|
|
|
// Set up the require mechanism
|
|
if err := setupRequireFunction(state); err != nil {
|
|
state.Close()
|
|
return nil, ErrInitFailed
|
|
}
|
|
|
|
// Set up sandbox
|
|
if err := runner.sandbox.Setup(state); err != nil {
|
|
state.Close()
|
|
return nil, ErrInitFailed
|
|
}
|
|
|
|
// Run init function if provided
|
|
if runner.initFunc != nil {
|
|
if err := runner.initFunc(state); err != nil {
|
|
state.Close()
|
|
return nil, ErrInitFailed
|
|
}
|
|
}
|
|
|
|
// Start the event loop
|
|
runner.wg.Add(1)
|
|
go runner.processJobs()
|
|
|
|
return runner, nil
|
|
}
|
|
|
|
// setupRequireFunction adds the secure require implementation
|
|
func setupRequireFunction(state *luajit.State) error {
|
|
return state.DoString(`
|
|
function __setup_secure_require(env)
|
|
-- Replace env.require with our secure version
|
|
env.require = function(modname)
|
|
-- Check if already loaded in package.loaded
|
|
if package.loaded[modname] then
|
|
return package.loaded[modname]
|
|
end
|
|
|
|
-- Try to load the module using our Go loader
|
|
local loader = __go_load_module
|
|
|
|
-- Load the module
|
|
local f, err = loader(modname)
|
|
if not f then
|
|
error(err or "failed to load module: " .. modname)
|
|
end
|
|
|
|
-- Set the environment for the module
|
|
setfenv(f, env)
|
|
|
|
-- Execute the module
|
|
local result = f()
|
|
|
|
-- If module didn't return a value, use true
|
|
if result == nil then
|
|
result = true
|
|
end
|
|
|
|
-- Cache the result
|
|
package.loaded[modname] = result
|
|
|
|
return result
|
|
end
|
|
|
|
return env
|
|
end
|
|
`)
|
|
}
|
|
|
|
// RunnerOption defines a functional option for configuring the LuaRunner
|
|
type RunnerOption func(*LuaRunner)
|
|
|
|
// WithBufferSize sets the job queue buffer size
|
|
func WithBufferSize(size int) RunnerOption {
|
|
return func(r *LuaRunner) {
|
|
if size > 0 {
|
|
r.bufferSize = size
|
|
}
|
|
}
|
|
}
|
|
|
|
// WithInitFunc sets the init function for the Lua state
|
|
func WithInitFunc(initFunc StateInitFunc) RunnerOption {
|
|
return func(r *LuaRunner) {
|
|
r.initFunc = initFunc
|
|
}
|
|
}
|
|
|
|
// WithScriptDir sets the base directory for scripts
|
|
func WithScriptDir(dir string) RunnerOption {
|
|
return func(r *LuaRunner) {
|
|
r.requireCfg.ScriptDir = dir
|
|
}
|
|
}
|
|
|
|
// WithLibDirs sets additional library directories
|
|
func WithLibDirs(dirs ...string) RunnerOption {
|
|
return func(r *LuaRunner) {
|
|
r.requireCfg.LibDirs = dirs
|
|
}
|
|
}
|
|
|
|
// scriptDir returns the current script directory
|
|
func (r *LuaRunner) scriptDir() string {
|
|
if r.requireCfg != nil {
|
|
return r.requireCfg.ScriptDir
|
|
}
|
|
return ""
|
|
}
|
|
|
|
// libDirs returns the current library directories
|
|
func (r *LuaRunner) libDirs() []string {
|
|
if r.requireCfg != nil {
|
|
return r.requireCfg.LibDirs
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// processJobs handles the job queue
|
|
func (r *LuaRunner) processJobs() {
|
|
defer r.wg.Done()
|
|
defer r.state.Close()
|
|
|
|
for job := range r.jobQueue {
|
|
// Execute the job and send result
|
|
result := r.executeJob(job)
|
|
select {
|
|
case job.Result <- result:
|
|
// Result sent successfully
|
|
default:
|
|
// Result channel closed or full, discard the result
|
|
}
|
|
}
|
|
}
|
|
|
|
// executeJob runs a script in the sandbox environment
|
|
func (r *LuaRunner) executeJob(j job) JobResult {
|
|
// If the job has a script path, update paths without re-registering
|
|
if j.ScriptPath != "" {
|
|
r.mu.Lock()
|
|
UpdateRequirePaths(r.requireCfg, j.ScriptPath)
|
|
r.mu.Unlock()
|
|
}
|
|
|
|
// Re-run init function if needed
|
|
if r.initFunc != nil {
|
|
if err := r.initFunc(r.state); err != nil {
|
|
return JobResult{nil, err}
|
|
}
|
|
}
|
|
|
|
// Convert context for sandbox
|
|
var ctx map[string]any
|
|
if j.Context != nil {
|
|
ctx = j.Context.Values
|
|
}
|
|
|
|
// Execute in sandbox
|
|
value, err := r.sandbox.Execute(r.state, j.Bytecode, ctx)
|
|
return JobResult{value, err}
|
|
}
|
|
|
|
// RunWithContext executes a Lua script with context and timeout
|
|
func (r *LuaRunner) RunWithContext(ctx context.Context, bytecode []byte, execCtx *Context, scriptPath string) (any, error) {
|
|
r.mu.RLock()
|
|
if !r.isRunning.Load() {
|
|
r.mu.RUnlock()
|
|
return nil, ErrRunnerClosed
|
|
}
|
|
r.mu.RUnlock()
|
|
|
|
resultChan := make(chan JobResult, 1)
|
|
j := job{
|
|
Bytecode: bytecode,
|
|
Context: execCtx,
|
|
ScriptPath: scriptPath,
|
|
Result: resultChan,
|
|
}
|
|
|
|
// Submit job with context
|
|
select {
|
|
case r.jobQueue <- j:
|
|
// Job submitted
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
|
|
// Wait for result with context
|
|
select {
|
|
case result := <-resultChan:
|
|
return result.Value, result.Error
|
|
case <-ctx.Done():
|
|
return nil, ctx.Err()
|
|
}
|
|
}
|
|
|
|
// Run executes a Lua script
|
|
func (r *LuaRunner) Run(bytecode []byte, execCtx *Context, scriptPath string) (any, error) {
|
|
return r.RunWithContext(context.Background(), bytecode, execCtx, scriptPath)
|
|
}
|
|
|
|
// Close gracefully shuts down the LuaRunner
|
|
func (r *LuaRunner) Close() error {
|
|
r.mu.Lock()
|
|
defer r.mu.Unlock()
|
|
|
|
if !r.isRunning.Load() {
|
|
return ErrRunnerClosed
|
|
}
|
|
|
|
r.isRunning.Store(false)
|
|
close(r.jobQueue)
|
|
|
|
// Wait for event loop to finish
|
|
r.wg.Wait()
|
|
|
|
return nil
|
|
}
|
|
|
|
// ClearRequireCache clears the cache of loaded modules
|
|
func (r *LuaRunner) ClearRequireCache() {
|
|
r.requireCache = NewRequireCache()
|
|
}
|
|
|
|
// AddModule adds a module to the sandbox environment
|
|
func (r *LuaRunner) AddModule(name string, module any) {
|
|
r.sandbox.AddModule(name, module)
|
|
}
|