293 lines
6.5 KiB
Go
293 lines
6.5 KiB
Go
package runner
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"sync"
|
|
|
|
"Moonshark/logger"
|
|
|
|
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
|
)
|
|
|
|
type ModuleConfig struct {
|
|
ScriptDir string
|
|
LibDirs []string
|
|
}
|
|
|
|
type ModuleLoader struct {
|
|
config *ModuleConfig
|
|
pathCache map[string]string // For reverse lookups (path -> module name)
|
|
debug bool
|
|
mu sync.RWMutex
|
|
}
|
|
|
|
func NewModuleLoader(config *ModuleConfig) *ModuleLoader {
|
|
if config == nil {
|
|
config = &ModuleConfig{}
|
|
}
|
|
|
|
return &ModuleLoader{
|
|
config: config,
|
|
pathCache: make(map[string]string),
|
|
}
|
|
}
|
|
|
|
func (l *ModuleLoader) EnableDebug() {
|
|
l.debug = true
|
|
}
|
|
|
|
func (l *ModuleLoader) SetScriptDir(dir string) {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
l.config.ScriptDir = dir
|
|
}
|
|
|
|
func (l *ModuleLoader) debugLog(format string, args ...any) {
|
|
if l.debug {
|
|
logger.Debugf("ModuleLoader "+format, args...)
|
|
}
|
|
}
|
|
|
|
func (l *ModuleLoader) SetupRequire(state *luajit.State) error {
|
|
// Set package.path
|
|
paths := l.getSearchPaths()
|
|
pathStr := strings.Join(paths, ";")
|
|
|
|
return state.DoString(`package.path = "` + escapeLuaString(pathStr) + `"`)
|
|
}
|
|
|
|
func (l *ModuleLoader) getSearchPaths() []string {
|
|
var paths []string
|
|
seen := make(map[string]bool)
|
|
|
|
// Script directory first
|
|
if l.config.ScriptDir != "" {
|
|
if absPath, err := filepath.Abs(l.config.ScriptDir); err == nil && !seen[absPath] {
|
|
paths = append(paths, filepath.Join(absPath, "?.lua"))
|
|
seen[absPath] = true
|
|
}
|
|
}
|
|
|
|
// Library directories
|
|
for _, dir := range l.config.LibDirs {
|
|
if dir == "" {
|
|
continue
|
|
}
|
|
if absPath, err := filepath.Abs(dir); err == nil && !seen[absPath] {
|
|
paths = append(paths, filepath.Join(absPath, "?.lua"))
|
|
seen[absPath] = true
|
|
}
|
|
}
|
|
|
|
return paths
|
|
}
|
|
|
|
func (l *ModuleLoader) PreloadModules(state *luajit.State) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
// Reset caches
|
|
l.pathCache = make(map[string]string)
|
|
|
|
// 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
|
|
if not core[name] then package.loaded[name] = nil end
|
|
end
|
|
package.preload = {}
|
|
`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Scan and preload modules
|
|
for _, dir := range l.config.LibDirs {
|
|
if err := l.scanDirectory(state, dir); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// Install simplified require
|
|
return state.DoString(`
|
|
function __setup_require(env)
|
|
env.require = function(modname)
|
|
if package.loaded[modname] then
|
|
return package.loaded[modname]
|
|
end
|
|
|
|
local loader = package.preload[modname]
|
|
if loader then
|
|
setfenv(loader, env)
|
|
local result = loader() or true
|
|
package.loaded[modname] = result
|
|
return result
|
|
end
|
|
|
|
-- Standard path search
|
|
for path in package.path:gmatch("[^;]+") do
|
|
local file = path:gsub("?", modname:gsub("%.", "/"))
|
|
local chunk = loadfile(file)
|
|
if chunk then
|
|
setfenv(chunk, env)
|
|
local result = chunk() or true
|
|
package.loaded[modname] = result
|
|
return result
|
|
end
|
|
end
|
|
|
|
error("module '" .. modname .. "' not found", 2)
|
|
end
|
|
return env
|
|
end
|
|
`)
|
|
}
|
|
|
|
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) {
|
|
l.mu.RLock()
|
|
defer l.mu.RUnlock()
|
|
|
|
absPath, err := filepath.Abs(path)
|
|
if err != nil {
|
|
absPath = filepath.Clean(path)
|
|
}
|
|
|
|
// Direct lookup
|
|
for modName, modPath := range l.pathCache {
|
|
if modPath == absPath {
|
|
return modName, true
|
|
}
|
|
}
|
|
|
|
// Construct from lib dirs
|
|
for _, dir := range l.config.LibDirs {
|
|
absDir, err := filepath.Abs(dir)
|
|
if err != nil {
|
|
continue
|
|
}
|
|
|
|
relPath, err := filepath.Rel(absDir, absPath)
|
|
if err != nil || strings.HasPrefix(relPath, "..") || !strings.HasSuffix(relPath, ".lua") {
|
|
continue
|
|
}
|
|
|
|
modName := strings.TrimSuffix(relPath, ".lua")
|
|
modName = strings.ReplaceAll(modName, string(filepath.Separator), ".")
|
|
return modName, true
|
|
}
|
|
|
|
return "", false
|
|
}
|
|
|
|
func (l *ModuleLoader) RefreshModule(state *luajit.State, moduleName string) error {
|
|
l.mu.Lock()
|
|
defer l.mu.Unlock()
|
|
|
|
path, exists := l.pathCache[moduleName]
|
|
if !exists {
|
|
return fmt.Errorf("module %s not found", moduleName)
|
|
}
|
|
|
|
l.debugLog("Refreshing module: %s", moduleName)
|
|
|
|
content, err := os.ReadFile(path)
|
|
if err != nil {
|
|
return fmt.Errorf("failed to read module: %w", err)
|
|
}
|
|
|
|
// Compile new version
|
|
if err := state.LoadString(string(content)); err != nil {
|
|
return fmt.Errorf("failed to compile module: %w", err)
|
|
}
|
|
|
|
// Update package.preload
|
|
state.GetGlobal("package")
|
|
state.GetField(-1, "preload")
|
|
state.PushString(moduleName)
|
|
state.PushCopy(-4) // Copy function
|
|
state.SetTable(-3)
|
|
state.Pop(2) // Pop package and preload
|
|
state.Pop(1) // Pop function
|
|
|
|
// Clear from loaded
|
|
state.DoString(`package.loaded["` + escapeLuaString(moduleName) + `"] = nil`)
|
|
|
|
l.debugLog("Successfully refreshed: %s", moduleName)
|
|
return nil
|
|
}
|
|
|
|
func (l *ModuleLoader) RefreshModuleByPath(state *luajit.State, filePath string) error {
|
|
moduleName, exists := l.GetModuleByPath(filePath)
|
|
if !exists {
|
|
return fmt.Errorf("no module found for path: %s", filePath)
|
|
}
|
|
return l.RefreshModule(state, moduleName)
|
|
}
|
|
|
|
func escapeLuaString(s string) string {
|
|
return strings.NewReplacer(
|
|
`\`, `\\`,
|
|
`"`, `\"`,
|
|
"\n", `\n`,
|
|
"\r", `\r`,
|
|
"\t", `\t`,
|
|
).Replace(s)
|
|
}
|