optimize moduleLoader, re-add to runner

This commit is contained in:
Sky Johnson 2025-06-04 21:50:56 -05:00
parent e3ee503c31
commit bf8ce59b73
3 changed files with 131 additions and 265 deletions

View File

@ -148,7 +148,7 @@ func (s *Moonshark) initRunner(poolSize int) error {
sessions.GlobalSessionManager.SetCookieOptions("MoonsharkSID", "/", "", false, true, 86400) sessions.GlobalSessionManager.SetCookieOptions("MoonsharkSID", "/", "", false, true, 86400)
var err error var err error
s.LuaRunner, err = runner.NewRunner(poolSize, s.Config.Dirs.Data, s.Config.Dirs.FS) s.LuaRunner, err = runner.NewRunner(poolSize, s.Config.Dirs.Data, s.Config.Dirs.FS, s.Config.Dirs.Libs)
if err != nil { if err != nil {
return fmt.Errorf("lua runner init failed: %v", err) return fmt.Errorf("lua runner init failed: %v", err)
} }

View File

@ -12,403 +12,267 @@ import (
luajit "git.sharkk.net/Sky/LuaJIT-to-Go" luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
) )
// ModuleConfig holds configuration for Lua's module loading system
type ModuleConfig struct { type ModuleConfig struct {
ScriptDir string // Base directory for script being executed ScriptDir string
LibDirs []string // Additional library directories LibDirs []string
} }
// ModuleLoader manages module loading and caching
type ModuleLoader struct { type ModuleLoader struct {
config *ModuleConfig config *ModuleConfig
pathCache map[string]string // Cache module paths for fast lookups pathCache map[string]string // For reverse lookups (path -> module name)
bytecodeCache map[string][]byte // Cache of compiled bytecode debug bool
debug bool mu sync.RWMutex
mu sync.RWMutex
} }
// NewModuleLoader creates a new module loader
func NewModuleLoader(config *ModuleConfig) *ModuleLoader { func NewModuleLoader(config *ModuleConfig) *ModuleLoader {
if config == nil { if config == nil {
config = &ModuleConfig{ config = &ModuleConfig{}
ScriptDir: "",
LibDirs: []string{},
}
} }
return &ModuleLoader{ return &ModuleLoader{
config: config, config: config,
pathCache: make(map[string]string), pathCache: make(map[string]string),
bytecodeCache: make(map[string][]byte),
debug: false,
} }
} }
// EnableDebug turns on debug logging
func (l *ModuleLoader) EnableDebug() { func (l *ModuleLoader) EnableDebug() {
l.debug = true l.debug = true
} }
// SetScriptDir sets the script directory
func (l *ModuleLoader) SetScriptDir(dir string) { func (l *ModuleLoader) SetScriptDir(dir string) {
l.mu.Lock() l.mu.Lock()
defer l.mu.Unlock() defer l.mu.Unlock()
l.config.ScriptDir = dir l.config.ScriptDir = dir
} }
// debugLog logs a message if debug mode is enabled func (l *ModuleLoader) debugLog(format string, args ...any) {
func (l *ModuleLoader) debugLog(format string, args ...interface{}) {
if l.debug { if l.debug {
logger.Debugf("ModuleLoader "+format, args...) logger.Debugf("ModuleLoader "+format, args...)
} }
} }
// SetupRequire configures the require system in a Lua state
func (l *ModuleLoader) SetupRequire(state *luajit.State) error { func (l *ModuleLoader) SetupRequire(state *luajit.State) error {
l.mu.RLock() // Set package.path
defer l.mu.RUnlock()
// Initialize our module registry in Lua
err := state.DoString(`
-- Initialize global module registry
__module_paths = {}
__module_bytecode = {}
__ready_modules = {}
-- Create module preload table
package.preload = package.preload or {}
`)
if err != nil {
return err
}
// Set up package.path based on search paths
paths := l.getSearchPaths() paths := l.getSearchPaths()
pathStr := strings.Join(paths, ";") pathStr := strings.Join(paths, ";")
escapedPathStr := escapeLuaString(pathStr)
return state.DoString(`package.path = "` + escapedPathStr + `"`) return state.DoString(`package.path = "` + escapeLuaString(pathStr) + `"`)
} }
// getSearchPaths returns a list of Lua search paths
func (l *ModuleLoader) getSearchPaths() []string { func (l *ModuleLoader) getSearchPaths() []string {
absPaths := []string{} var paths []string
seen := map[string]bool{} seen := make(map[string]bool)
// Add script directory (highest priority) // Script directory first
if l.config.ScriptDir != "" { if l.config.ScriptDir != "" {
absPath, err := filepath.Abs(l.config.ScriptDir) if absPath, err := filepath.Abs(l.config.ScriptDir); err == nil && !seen[absPath] {
if err == nil && !seen[absPath] { paths = append(paths, filepath.Join(absPath, "?.lua"))
absPaths = append(absPaths, filepath.Join(absPath, "?.lua"))
seen[absPath] = true seen[absPath] = true
} }
} }
// Add lib directories // Library directories
for _, dir := range l.config.LibDirs { for _, dir := range l.config.LibDirs {
if dir == "" { if dir == "" {
continue continue
} }
if absPath, err := filepath.Abs(dir); err == nil && !seen[absPath] {
absPath, err := filepath.Abs(dir) paths = append(paths, filepath.Join(absPath, "?.lua"))
if err == nil && !seen[absPath] {
absPaths = append(absPaths, filepath.Join(absPath, "?.lua"))
seen[absPath] = true seen[absPath] = true
} }
} }
return absPaths return paths
} }
// PreloadModules preloads modules from library directories
func (l *ModuleLoader) PreloadModules(state *luajit.State) error { func (l *ModuleLoader) PreloadModules(state *luajit.State) error {
l.mu.Lock() l.mu.Lock()
defer l.mu.Unlock() defer l.mu.Unlock()
// Reset caches // Reset caches
l.pathCache = make(map[string]string) l.pathCache = make(map[string]string)
l.bytecodeCache = make(map[string][]byte)
// Reset module registry in Lua
if err := state.DoString(`
-- Reset module registry
__module_paths = {}
__module_bytecode = {}
__ready_modules = {}
-- Clear non-core modules from package.loaded
local core_modules = {
string = true, table = true, math = true, os = true,
package = true, io = true, coroutine = true, debug = true, _G = true
}
// Clear non-core modules
err := state.DoString(`
local core = {string=1, table=1, math=1, os=1, package=1, io=1, coroutine=1, debug=1, _G=1}
for name in pairs(package.loaded) do for name in pairs(package.loaded) do
if not core_modules[name] then if not core[name] then package.loaded[name] = nil end
package.loaded[name] = nil
end
end end
-- Reset preload table
package.preload = {} package.preload = {}
`); err != nil { `)
if err != nil {
return err return err
} }
// Scan and preload modules from all library directories // Scan and preload modules
for _, dir := range l.config.LibDirs { for _, dir := range l.config.LibDirs {
if dir == "" { if err := l.scanDirectory(state, dir); err != nil {
continue
}
absDir, err := filepath.Abs(dir)
if err != nil {
continue
}
l.debugLog("Scanning directory: %s", absDir)
// Find all Lua files
err = filepath.Walk(absDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".lua") {
return nil
}
// Get module name from path
relPath, err := filepath.Rel(absDir, path)
if err != nil || strings.HasPrefix(relPath, "..") {
return nil
}
// Convert path to module name
modName := strings.TrimSuffix(relPath, ".lua")
modName = strings.ReplaceAll(modName, string(filepath.Separator), ".")
l.debugLog("Found module: %s at %s", modName, path)
// Register in our caches
l.pathCache[modName] = path
// Load file content
content, err := os.ReadFile(path)
if err != nil {
l.debugLog("Failed to read module file: %v", err)
return nil
}
// Compile to bytecode
bytecode, err := state.CompileBytecode(string(content), path)
if err != nil {
l.debugLog("Failed to compile module: %v", err)
return nil
}
// Cache bytecode
l.bytecodeCache[modName] = bytecode
// Register in Lua - store path info
escapedPath := escapeLuaString(path)
escapedName := escapeLuaString(modName)
if err := state.DoString(`__module_paths["` + escapedName + `"] = "` + escapedPath + `"`); err != nil {
return nil
}
// Load bytecode and register in package.preload properly
if err := state.LoadBytecode(bytecode, path); err != nil {
return nil
}
// Store the function in package.preload - the function is on the stack
state.GetGlobal("package")
state.GetField(-1, "preload")
state.PushString(modName)
state.PushCopy(-4) // Copy the compiled function
state.SetTable(-3) // preload[modName] = function
state.Pop(2) // Pop package and preload tables
// Mark as ready
if err := state.DoString(`__ready_modules["` + escapedName + `"] = true`); err != nil {
state.Pop(1) // Remove the function from stack
return nil
}
state.Pop(1) // Remove the function from stack
return nil
})
if err != nil {
return err return err
} }
} }
// Install optimized require implementation // Install simplified require
return state.DoString(` return state.DoString(`
-- Setup environment-aware require function
function __setup_require(env) function __setup_require(env)
-- Create require function specific to this environment
env.require = function(modname) env.require = function(modname)
-- Check if already loaded
if package.loaded[modname] then if package.loaded[modname] then
return package.loaded[modname] return package.loaded[modname]
end end
-- Check preloaded modules local loader = package.preload[modname]
if __ready_modules[modname] then if loader then
local loader = package.preload[modname] setfenv(loader, env)
if loader then local result = loader() or true
-- Set environment for loader package.loaded[modname] = result
setfenv(loader, env) return result
-- Execute and store result
local result = loader()
if result == nil then
result = true
end
package.loaded[modname] = result
return result
end
end end
-- Direct file load as fallback -- Standard path search
if __module_paths[modname] then
local path = __module_paths[modname]
local chunk, err = loadfile(path)
if chunk then
setfenv(chunk, env)
local result = chunk()
if result == nil then
result = true
end
package.loaded[modname] = result
return result
end
end
-- Full path search as last resort
local errors = {}
for path in package.path:gmatch("[^;]+") do for path in package.path:gmatch("[^;]+") do
local file_path = path:gsub("?", modname:gsub("%.", "/")) local file = path:gsub("?", modname:gsub("%.", "/"))
local chunk, err = loadfile(file_path) local chunk = loadfile(file)
if chunk then if chunk then
setfenv(chunk, env) setfenv(chunk, env)
local result = chunk() local result = chunk() or true
if result == nil then
result = true
end
package.loaded[modname] = result package.loaded[modname] = result
return result return result
end end
table.insert(errors, "\tno file '" .. file_path .. "'")
end end
error("module '" .. modname .. "' not found:\n" .. table.concat(errors, "\n"), 2) error("module '" .. modname .. "' not found", 2)
end end
return env return env
end end
`) `)
} }
// GetModuleByPath finds the module name for a file path func (l *ModuleLoader) scanDirectory(state *luajit.State, dir string) error {
if dir == "" {
return nil
}
absDir, err := filepath.Abs(dir)
if err != nil {
return nil
}
l.debugLog("Scanning directory: %s", absDir)
return filepath.Walk(absDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(path, ".lua") {
return nil
}
relPath, err := filepath.Rel(absDir, path)
if err != nil || strings.HasPrefix(relPath, "..") {
return nil
}
// Convert to module name
modName := strings.TrimSuffix(relPath, ".lua")
modName = strings.ReplaceAll(modName, string(filepath.Separator), ".")
l.debugLog("Found module: %s at %s", modName, path)
l.pathCache[modName] = path
// Load and compile module
content, err := os.ReadFile(path)
if err != nil {
l.debugLog("Failed to read %s: %v", path, err)
return nil
}
if err := state.LoadString(string(content)); err != nil {
l.debugLog("Failed to compile %s: %v", path, err)
return nil
}
// Store in package.preload
state.GetGlobal("package")
state.GetField(-1, "preload")
state.PushString(modName)
state.PushCopy(-4) // Copy compiled function
state.SetTable(-3)
state.Pop(2) // Pop package and preload
state.Pop(1) // Pop function
return nil
})
}
func (l *ModuleLoader) GetModuleByPath(path string) (string, bool) { func (l *ModuleLoader) GetModuleByPath(path string) (string, bool) {
l.mu.RLock() l.mu.RLock()
defer l.mu.RUnlock() defer l.mu.RUnlock()
// Convert to absolute path for consistent comparison
absPath, err := filepath.Abs(path) absPath, err := filepath.Abs(path)
if err != nil { if err != nil {
absPath = filepath.Clean(path) absPath = filepath.Clean(path)
} }
// Try direct lookup from cache with absolute path // Direct lookup
for modName, modPath := range l.pathCache { for modName, modPath := range l.pathCache {
if modPath == absPath { if modPath == absPath {
return modName, true return modName, true
} }
} }
// Try to construct module name from lib dirs // Construct from lib dirs
for _, dir := range l.config.LibDirs { for _, dir := range l.config.LibDirs {
absDir, err := filepath.Abs(dir) absDir, err := filepath.Abs(dir)
if err != nil { if err != nil {
continue continue
} }
// Check if the file is under this lib directory
relPath, err := filepath.Rel(absDir, absPath) relPath, err := filepath.Rel(absDir, absPath)
if err != nil || strings.HasPrefix(relPath, "..") { if err != nil || strings.HasPrefix(relPath, "..") || !strings.HasSuffix(relPath, ".lua") {
continue continue
} }
if strings.HasSuffix(relPath, ".lua") { modName := strings.TrimSuffix(relPath, ".lua")
modName := strings.TrimSuffix(relPath, ".lua") modName = strings.ReplaceAll(modName, string(filepath.Separator), ".")
modName = strings.ReplaceAll(modName, string(filepath.Separator), ".") return modName, true
l.debugLog("Found module %s for path %s", modName, path)
return modName, true
}
} }
l.debugLog("No module found for path %s", path)
return "", false return "", false
} }
// RefreshModule recompiles and updates a specific module
func (l *ModuleLoader) RefreshModule(state *luajit.State, moduleName string) error { func (l *ModuleLoader) RefreshModule(state *luajit.State, moduleName string) error {
l.mu.Lock() l.mu.Lock()
defer l.mu.Unlock() defer l.mu.Unlock()
// Get module path
path, exists := l.pathCache[moduleName] path, exists := l.pathCache[moduleName]
if !exists { if !exists {
l.debugLog("Module not found in cache: %s", moduleName)
return fmt.Errorf("module %s not found", moduleName) return fmt.Errorf("module %s not found", moduleName)
} }
l.debugLog("Refreshing module: %s at %s", moduleName, path) l.debugLog("Refreshing module: %s", moduleName)
// Read updated file content
content, err := os.ReadFile(path) content, err := os.ReadFile(path)
if err != nil { if err != nil {
return fmt.Errorf("failed to read module file: %w", err) return fmt.Errorf("failed to read module: %w", err)
} }
// Recompile to bytecode // Compile new version
bytecode, err := state.CompileBytecode(string(content), path) if err := state.LoadString(string(content)); err != nil {
if err != nil {
return fmt.Errorf("failed to compile module: %w", err) return fmt.Errorf("failed to compile module: %w", err)
} }
// Update bytecode cache // Update package.preload
l.bytecodeCache[moduleName] = bytecode
// Load new bytecode
if err := state.LoadBytecode(bytecode, path); err != nil {
return fmt.Errorf("failed to load bytecode: %w", err)
}
// Update package.preload with new function (function is on stack)
state.GetGlobal("package") state.GetGlobal("package")
state.GetField(-1, "preload") state.GetField(-1, "preload")
state.PushString(moduleName) state.PushString(moduleName)
state.PushCopy(-4) // Copy the new compiled function state.PushCopy(-4) // Copy function
state.SetTable(-3) // preload[moduleName] = new_function state.SetTable(-3)
state.Pop(2) // Pop package and preload tables state.Pop(2) // Pop package and preload
state.Pop(1) // Pop the function state.Pop(1) // Pop function
// Clear from package.loaded so it gets reloaded // Clear from loaded
escapedName := escapeLuaString(moduleName) state.DoString(`package.loaded["` + escapeLuaString(moduleName) + `"] = nil`)
if err := state.DoString(`package.loaded["` + escapedName + `"] = nil`); err != nil {
return fmt.Errorf("failed to clear loaded module: %w", err)
}
l.debugLog("Successfully refreshed module: %s", moduleName) l.debugLog("Successfully refreshed: %s", moduleName)
return nil return nil
} }
// RefreshModuleByPath refreshes a module by its file path
func (l *ModuleLoader) RefreshModuleByPath(state *luajit.State, filePath string) error { func (l *ModuleLoader) RefreshModuleByPath(state *luajit.State, filePath string) error {
moduleName, exists := l.GetModuleByPath(filePath) moduleName, exists := l.GetModuleByPath(filePath)
if !exists { if !exists {
@ -417,14 +281,12 @@ func (l *ModuleLoader) RefreshModuleByPath(state *luajit.State, filePath string)
return l.RefreshModule(state, moduleName) return l.RefreshModule(state, moduleName)
} }
// escapeLuaString escapes special characters in a string for Lua
func escapeLuaString(s string) string { func escapeLuaString(s string) string {
replacer := strings.NewReplacer( return strings.NewReplacer(
"\\", "\\\\", `\`, `\\`,
"\"", "\\\"", `"`, `\"`,
"\n", "\\n", "\n", `\n`,
"\r", "\\r", "\r", `\r`,
"\t", "\\t", "\t", `\t`,
) ).Replace(s)
return replacer.Replace(s)
} }

View File

@ -1,4 +1,3 @@
// runner.go - Simplified interface
package runner package runner
import ( import (
@ -52,14 +51,19 @@ type Runner struct {
paramsPool sync.Pool paramsPool sync.Pool
} }
func NewRunner(poolSize int, dataDir, fsDir string) (*Runner, error) { func NewRunner(poolSize int, dataDir, fsDir string, libDirs []string) (*Runner, error) {
if poolSize <= 0 { if poolSize <= 0 {
poolSize = runtime.GOMAXPROCS(0) poolSize = runtime.GOMAXPROCS(0)
} }
// Configure module loader with lib directories
moduleConfig := &ModuleConfig{
LibDirs: libDirs,
}
r := &Runner{ r := &Runner{
poolSize: poolSize, poolSize: poolSize,
moduleLoader: NewModuleLoader(&ModuleConfig{}), moduleLoader: NewModuleLoader(moduleConfig),
ctxPool: sync.Pool{ ctxPool: sync.Pool{
New: func() any { return make(map[string]any, 8) }, New: func() any { return make(map[string]any, 8) },
}, },