Compare commits

..

No commits in common. "5bba4ffcf8f4c945cad7905952588d1d912a7aec" and "945886abe639e321aa9325f462c1441f131a496e" have entirely different histories.

21 changed files with 793 additions and 1600 deletions

View File

@ -21,7 +21,7 @@ type Moonshark struct {
Config *config.Config
LuaRouter *routers.LuaRouter
StaticRouter *routers.StaticRouter
LuaRunner *runner.Runner
LuaRunner *runner.LuaRunner
HTTPServer *http.Server
// Clean-up functions for watchers
@ -77,8 +77,8 @@ func (s *Moonshark) loadConfig(configPath string) error {
// Load configuration from file
s.Config, err = config.Load(configPath)
if err != nil {
logger.Warning("Wipeout! Couldn't load config file: %v", err)
logger.WarningCont("Rolling with the default setup")
logger.Warning("Failed to load config file: %v", err)
logger.Info("Using default configuration")
s.Config = config.New()
}
@ -104,7 +104,7 @@ func (s *Moonshark) setupLogging() {
// Set debug mode if configured
if s.Config.Debug {
logger.EnableDebug() // Force debug logs regardless of level
logger.Debug("Debug mode is ready to party, bro")
logger.Debug("Debug mode enabled")
}
}
@ -139,14 +139,14 @@ func (s *Moonshark) initRouters() error {
return fmt.Errorf("failed to initialize Lua router: %v", err)
}
}
logger.Info("Lua router is stoked and riding routes from %s", s.Config.RoutesDir)
logger.Info("Lua router initialized with routes from %s", s.Config.RoutesDir)
// Initialize static file router
s.StaticRouter, err = routers.NewStaticRouter(s.Config.StaticDir)
if err != nil {
return fmt.Errorf("failed to initialize static router: %v", err)
}
logger.Info("Static router catching waves with files from %s", s.Config.StaticDir)
logger.Info("Static router initialized with files from %s", s.Config.StaticDir)
s.StaticRouter.EnableDebugLog()
return nil
@ -201,7 +201,7 @@ func (s *Moonshark) initRunner() error {
return fmt.Errorf("failed to initialize Lua runner: %v", err)
}
logger.Server("Lua runner waxing up with a pool size of %d", s.Config.PoolSize)
logger.Server("Lua runner initialized with pool size %d", s.Config.PoolSize)
return nil
}
@ -246,13 +246,13 @@ func (s *Moonshark) setupWatchers() error {
// Start starts the HTTP server
func (s *Moonshark) Start() error {
logger.Server("Surf's up on port %d!", s.Config.Port)
logger.Server("Starting server on port %d", s.Config.Port)
// Log HTTP logging status
if s.Config.HTTPLoggingEnabled {
logger.ServerCont("HTTP logging is turned on - watch those waves")
logger.Info("HTTP logging is enabled")
} else {
logger.ServerCont("HTTP logging is turned off - waves are flat bro")
logger.Info("HTTP logging is disabled")
}
// Start the server in a non-blocking way
@ -271,6 +271,7 @@ func (s *Moonshark) Start() error {
func (s *Moonshark) Shutdown() error {
logger.Server("Shutting down server...")
// Shutdown HTTP server with timeout
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
@ -279,14 +280,16 @@ func (s *Moonshark) Shutdown() error {
return err
}
// Run cleanup functions
for _, cleanup := range s.cleanupFuncs {
if err := cleanup(); err != nil {
logger.Warning("Cleanup error: %v", err)
}
}
// Close Lua runner
s.LuaRunner.Close()
logger.ServerCont("Server stopped")
logger.Server("Server stopped")
return nil
}

View File

@ -76,7 +76,7 @@ func Load(filePath string) (*Config, error) {
// Execute the config file
if err := state.DoFile(filePath); err != nil {
return nil, fmt.Errorf("%w", err)
return nil, fmt.Errorf("failed to load config file: %w", err)
}
// Extract configuration values

View File

@ -19,7 +19,7 @@ import (
type Server struct {
luaRouter *routers.LuaRouter
staticRouter *routers.StaticRouter
luaRunner *runner.Runner
luaRunner *runner.LuaRunner
httpServer *http.Server
loggingEnabled bool
debugMode bool // Controls whether to show error details
@ -28,7 +28,7 @@ type Server struct {
}
// New creates a new HTTP server with optimized connection settings
func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, runner *runner.Runner,
func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, runner *runner.LuaRunner,
loggingEnabled bool, debugMode bool, overrideDir string, config *config.Config) *Server {
server := &Server{
@ -61,12 +61,13 @@ func New(luaRouter *routers.LuaRouter, staticRouter *routers.StaticRouter, runne
// ListenAndServe starts the server on the given address
func (s *Server) ListenAndServe(addr string) error {
s.httpServer.Addr = addr
logger.ServerCont("Catch the swell at http://localhost%s", addr)
logger.Info("Server listening at http://localhost%s", addr)
return s.httpServer.ListenAndServe()
}
// Shutdown gracefully shuts down the server
func (s *Server) Shutdown(ctx context.Context) error {
logger.Info("Server shutting down...")
return s.httpServer.Shutdown(ctx)
}

View File

@ -4,7 +4,6 @@ import (
"fmt"
"io"
"os"
"strings"
"sync"
"sync/atomic"
"time"
@ -63,16 +62,12 @@ var (
// Logger handles logging operations
type Logger struct {
writer io.Writer
level int
useColors bool
timeFormat string
showTimestamp bool // Whether to show timestamp
mu sync.Mutex // Mutex for thread-safe writing
debugMode atomic.Bool // Force debug logging regardless of level
indentCache string // Cached indent string for continuations
indentSize int // Size of the indent for continuations
lastLevel int // Last log level used, for continuations
writer io.Writer
level int
useColors bool
timeFormat string
mu sync.Mutex // Mutex for thread-safe writing
debugMode atomic.Bool // Force debug logging regardless of level
// Simple rate limiting
logCount atomic.Int64 // Number of logs in current window
@ -86,42 +81,38 @@ type Logger struct {
// GetLogger returns the global logger instance, creating it if needed
func GetLogger() *Logger {
globalLoggerOnce.Do(func() {
globalLogger = newLogger(LevelInfo, true, true)
globalLogger = newLogger(LevelInfo, true)
})
return globalLogger
}
// InitGlobalLogger initializes the global logger with custom settings
func InitGlobalLogger(minLevel int, useColors bool, showTimestamp bool) {
// Reset the global logger instance
globalLogger = newLogger(minLevel, useColors, showTimestamp)
func InitGlobalLogger(minLevel int, useColors bool) {
globalLoggerOnce.Do(func() {
globalLogger = newLogger(minLevel, useColors)
})
}
// newLogger creates a new logger instance (internal use)
func newLogger(minLevel int, useColors bool, showTimestamp bool) *Logger {
func newLogger(minLevel int, useColors bool) *Logger {
logger := &Logger{
writer: os.Stdout,
level: minLevel,
useColors: useColors,
timeFormat: timeFormat,
showTimestamp: showTimestamp,
maxLogsPerSec: defaultMaxLogs,
limitDuration: defaultRateLimitTime,
lastLevel: -1, // Initialize to invalid level
}
// Initialize counters
logger.resetCounters()
// Calculate the base indent size
logger.updateIndentCache()
return logger
}
// New creates a new logger (deprecated - use GetLogger() instead)
func New(minLevel int, useColors bool, showTimestamp bool) *Logger {
return newLogger(minLevel, useColors, showTimestamp)
func New(minLevel int, useColors bool) *Logger {
return newLogger(minLevel, useColors)
}
// resetCounters resets the rate limiting counters
@ -144,47 +135,9 @@ func (l *Logger) TimeFormat() string {
return l.timeFormat
}
// updateIndentCache recalculates and updates the indent cache
func (l *Logger) updateIndentCache() {
tagWidth := 7
if l.showTimestamp {
// Format: "15:04:05 DEBUG "
timeWidth := len(time.Now().Format(l.timeFormat))
l.indentSize = timeWidth + 1 + tagWidth + 1
} else {
// Format: "DEBUG "
l.indentSize = tagWidth + 1
}
l.indentCache = strings.Repeat(" ", l.indentSize)
}
// SetTimeFormat changes the time format string and updates the indent cache
// SetTimeFormat changes the time format string
func (l *Logger) SetTimeFormat(format string) {
l.mu.Lock()
defer l.mu.Unlock()
l.timeFormat = format
l.updateIndentCache()
}
// EnableTimestamp enables timestamp display
func (l *Logger) EnableTimestamp() {
l.mu.Lock()
defer l.mu.Unlock()
l.showTimestamp = true
l.updateIndentCache()
}
// DisableTimestamp disables timestamp display
func (l *Logger) DisableTimestamp() {
l.mu.Lock()
defer l.mu.Unlock()
l.showTimestamp = false
l.updateIndentCache()
}
// SetLevel changes the minimum log level
@ -218,49 +171,24 @@ func (l *Logger) IsDebugEnabled() bool {
}
// writeMessage writes a formatted log message directly to the writer
func (l *Logger) writeMessage(level int, message string, rawMode bool, continuation bool) {
func (l *Logger) writeMessage(level int, message string, rawMode bool) {
var logLine string
if rawMode {
// Raw mode - message is already formatted, just append newline
logLine = message + "\n"
} else if continuation {
// Continuation format - just indent and message
if l.useColors {
// For colored output, use the color of the last level
props := levelProps[l.lastLevel]
logLine = fmt.Sprintf("%s%s%s\n",
l.indentCache, props.color, message+colorReset)
} else {
logLine = fmt.Sprintf("%s%s\n", l.indentCache, message)
}
} else {
// Standard format with level tag and optional timestamp
// Standard format with timestamp, level tag, and message
now := time.Now().Format(l.timeFormat)
props := levelProps[level]
if l.showTimestamp {
now := time.Now().Format(l.timeFormat)
if l.useColors {
logLine = fmt.Sprintf("%s%s%s %s[%s]%s %s\n",
colorGray, now, colorReset, props.color, props.tag, colorReset, message)
} else {
logLine = fmt.Sprintf("%s [%s] %s\n",
now, props.tag, message)
}
if l.useColors {
logLine = fmt.Sprintf("%s%s%s %s%s%s %s\n",
colorGray, now, colorReset, props.color, props.tag, colorReset, message)
} else {
// No timestamp, just level tag and message
if l.useColors {
logLine = fmt.Sprintf("%s[%s]%s %s\n",
props.color, props.tag, colorReset, message)
} else {
logLine = fmt.Sprintf("[%s] %s\n",
props.tag, message)
}
logLine = fmt.Sprintf("%s %s %s\n",
now, props.tag, message)
}
// Store the level for continuations
l.lastLevel = level
}
// Synchronously write the log message
@ -321,7 +249,7 @@ func (l *Logger) checkRateLimit(level int) bool {
// Log a warning about rate limiting
l.writeMessage(LevelServer,
fmt.Sprintf("Rate limiting logger temporarily due to high demand (%d logs/sec exceeded)", count),
false, false)
false)
return false
}
@ -350,7 +278,7 @@ func (l *Logger) log(level int, format string, args ...any) {
message = format
}
l.writeMessage(level, message, false, false)
l.writeMessage(level, message, false)
// Exit on fatal errors
if level == LevelFatal {
@ -358,31 +286,6 @@ func (l *Logger) log(level int, format string, args ...any) {
}
}
// continuation handles continuation log messages (messages that continue from a previous log)
func (l *Logger) continuation(level int, format string, args ...any) {
// Check if we should log this message
// Either level is high enough OR (it's a debug message AND debug mode is enabled)
if level < l.level && !(level == LevelDebug && l.debugMode.Load()) {
return
}
// Check rate limiting
if !l.checkRateLimit(level) {
return
}
// Format message
var message string
if len(args) > 0 {
message = fmt.Sprintf(format, args...)
} else {
message = format
}
// Use the continuation format
l.writeMessage(level, message, false, true)
}
// LogRaw logs a message with raw formatting, bypassing the standard format
func (l *Logger) LogRaw(format string, args ...any) {
// Use info level for filtering
@ -408,7 +311,7 @@ func (l *Logger) LogRaw(format string, args ...any) {
message = removeAnsiColors(message)
}
l.writeMessage(LevelInfo, message, true, false)
l.writeMessage(LevelInfo, message, true)
}
// Simple helper to remove ANSI color codes
@ -440,63 +343,32 @@ func (l *Logger) Debug(format string, args ...any) {
l.log(LevelDebug, format, args...)
}
// DebugCont logs a debug message as a continuation of the previous log
func (l *Logger) DebugCont(format string, args ...any) {
l.continuation(LevelDebug, format, args...)
}
// Info logs an informational message
func (l *Logger) Info(format string, args ...any) {
l.log(LevelInfo, format, args...)
}
// InfoCont logs an informational message as a continuation of the previous log
func (l *Logger) InfoCont(format string, args ...any) {
l.continuation(LevelInfo, format, args...)
}
// Warning logs a warning message
func (l *Logger) Warning(format string, args ...any) {
l.log(LevelWarning, format, args...)
}
// WarningCont logs a warning message as a continuation of the previous log
func (l *Logger) WarningCont(format string, args ...any) {
l.continuation(LevelWarning, format, args...)
}
// Error logs an error message
func (l *Logger) Error(format string, args ...any) {
l.log(LevelError, format, args...)
}
// ErrorCont logs an error message as a continuation of the previous log
func (l *Logger) ErrorCont(format string, args ...any) {
l.continuation(LevelError, format, args...)
}
// Fatal logs a fatal error message and exits
func (l *Logger) Fatal(format string, args ...any) {
l.log(LevelFatal, format, args...)
// No need for os.Exit here as it's handled in log()
}
// FatalCont logs a fatal error message as a continuation of the previous log and exits
func (l *Logger) FatalCont(format string, args ...any) {
l.continuation(LevelFatal, format, args...)
os.Exit(1)
}
// Server logs a server message
func (l *Logger) Server(format string, args ...any) {
l.log(LevelServer, format, args...)
}
// ServerCont logs a server message as a continuation of the previous log
func (l *Logger) ServerCont(format string, args ...any) {
l.continuation(LevelServer, format, args...)
}
// Global helper functions that use the global logger
// Debug logs a debug message to the global logger
@ -504,61 +376,31 @@ func Debug(format string, args ...any) {
GetLogger().Debug(format, args...)
}
// DebugCont logs a debug message as a continuation of the previous log to the global logger
func DebugCont(format string, args ...any) {
GetLogger().DebugCont(format, args...)
}
// Info logs an informational message to the global logger
func Info(format string, args ...any) {
GetLogger().Info(format, args...)
}
// InfoCont logs an informational message as a continuation of the previous log to the global logger
func InfoCont(format string, args ...any) {
GetLogger().InfoCont(format, args...)
}
// Warning logs a warning message to the global logger
func Warning(format string, args ...any) {
GetLogger().Warning(format, args...)
}
// WarningCont logs a warning message as a continuation of the previous log to the global logger
func WarningCont(format string, args ...any) {
GetLogger().WarningCont(format, args...)
}
// Error logs an error message to the global logger
func Error(format string, args ...any) {
GetLogger().Error(format, args...)
}
// ErrorCont logs an error message as a continuation of the previous log to the global logger
func ErrorCont(format string, args ...any) {
GetLogger().ErrorCont(format, args...)
}
// Fatal logs a fatal error message to the global logger and exits
func Fatal(format string, args ...any) {
GetLogger().Fatal(format, args...)
}
// FatalCont logs a fatal error message as a continuation of the previous log to the global logger and exits
func FatalCont(format string, args ...any) {
GetLogger().FatalCont(format, args...)
}
// Server logs a server message to the global logger
func Server(format string, args ...any) {
GetLogger().Server(format, args...)
}
// ServerCont logs a server message as a continuation of the previous log to the global logger
func ServerCont(format string, args ...any) {
GetLogger().ServerCont(format, args...)
}
// LogRaw logs a raw message to the global logger
func LogRaw(format string, args ...any) {
GetLogger().LogRaw(format, args...)
@ -593,57 +435,3 @@ func DisableDebug() {
func IsDebugEnabled() bool {
return GetLogger().IsDebugEnabled()
}
// EnableTimestamp enables timestamp display
func EnableTimestamp() {
GetLogger().EnableTimestamp()
}
// DisableTimestamp disables timestamp display
func DisableTimestamp() {
GetLogger().DisableTimestamp()
}
// LogSpacer adds a horizontal line separator to the log output
func (l *Logger) LogSpacer() {
l.mu.Lock()
defer l.mu.Unlock()
// Calculate spacer width
tagWidth := 7 // Standard width of tag area "[DEBUG]"
var spacer string
if l.showTimestamp {
// Format: "15:04:05 [DEBUG] ----"
timeWidth := len(time.Now().Format(l.timeFormat))
tagSpacer := strings.Repeat("-", tagWidth)
restSpacer := strings.Repeat("-", 20) // Fixed width for the rest
if l.useColors {
spacer = fmt.Sprintf("%s%s%s %s%s%s %s\n",
colorGray, strings.Repeat("-", timeWidth), colorReset,
colorCyan, tagSpacer, colorReset, restSpacer)
} else {
spacer = fmt.Sprintf("%s %s %s\n",
strings.Repeat("-", timeWidth), tagSpacer, restSpacer)
}
} else {
// No timestamp: "[DEBUG] ----"
tagSpacer := strings.Repeat("-", tagWidth)
restSpacer := strings.Repeat("-", 20) // Fixed width for the rest
if l.useColors {
spacer = fmt.Sprintf("%s%s%s %s\n",
colorCyan, tagSpacer, colorReset, restSpacer)
} else {
spacer = fmt.Sprintf("%s %s\n", tagSpacer, restSpacer)
}
}
_, _ = fmt.Fprint(l.writer, spacer)
}
// LogSpacer adds a horizontal line separator to the global logger
func LogSpacer() {
GetLogger().LogSpacer()
}

View File

@ -244,7 +244,7 @@ func (r *LuaRouter) matchPath(current *node, segments []string, params *Params,
}
// compileHandler compiles a Lua file to bytecode
func (r *LuaRouter) compileHandler(n *node, _ string) error {
func (r *LuaRouter) compileHandler(n *node, urlPath string) error {
if n.handler == "" {
return nil
}

View File

@ -44,10 +44,16 @@ type StaticRouter struct {
bufferPool sync.Pool // Buffer pool for compression
urlPrefix string // URL prefix for static assets
log bool // Whether to log debug info
logger *logger.Logger // Logger instance
}
// NewStaticRouterWithLogger creates a new StaticRouter instance
// NewStaticRouter creates a new StaticRouter instance
func NewStaticRouter(rootDir string) (*StaticRouter, error) {
return NewStaticRouterWithLogger(rootDir, logger.New(logger.LevelInfo, true))
}
// NewStaticRouterWithLogger creates a new StaticRouter instance with a custom logger
func NewStaticRouterWithLogger(rootDir string, log *logger.Logger) (*StaticRouter, error) {
// Verify root directory exists
info, err := os.Stat(rootDir)
if err != nil {
@ -68,6 +74,7 @@ func NewStaticRouter(rootDir string) (*StaticRouter, error) {
fileServer: http.FileServer(http.Dir(rootDir)),
urlPrefix: "/static", // Default prefix for static assets
log: false, // Debug logging off by default
logger: log,
bufferPool: sync.Pool{
New: func() any {
return new(bytes.Buffer)
@ -170,13 +177,13 @@ func (r *StaticRouter) ServeHTTP(w http.ResponseWriter, req *http.Request) {
// Try to serve from cache if client accepts gzip
if acceptsGzip && r.serveFromCache(w, &newReq, fileURLPath) {
if r.log {
logger.Debug("[StaticRouter] CACHE HIT: %s", origPath)
r.logger.Debug("[StaticRouter] CACHE HIT: %s", origPath)
}
return
}
if r.log {
logger.Debug("[StaticRouter] CACHE MISS: %s", origPath)
r.logger.Debug("[StaticRouter] CACHE MISS: %s", origPath)
}
// Fall back to standard file serving
@ -231,7 +238,7 @@ func (r *StaticRouter) Match(urlPath string) (string, bool) {
_, err := os.Stat(filePath)
if r.log && err == nil {
logger.Debug("[StaticRouter] MATCH: %s -> %s", urlPath, filePath)
r.logger.Debug("[StaticRouter] MATCH: %s -> %s", urlPath, filePath)
}
return filePath, err == nil
@ -436,7 +443,7 @@ func (r *StaticRouter) PreloadCommonFiles() {
}
if r.log {
logger.Debug("[StaticRouter] Preloading common files from %s", r.rootDir)
r.logger.Debug("[StaticRouter] Preloading common files from %s", r.rootDir)
}
count := 0
@ -470,7 +477,7 @@ func (r *StaticRouter) PreloadCommonFiles() {
})
if r.log {
logger.Debug("[StaticRouter] Preloaded %d files", count)
r.logger.Debug("[StaticRouter] Preloaded %d files", count)
}
}

View File

@ -4,11 +4,8 @@ import "sync"
// Context represents execution context for a Lua script
type Context struct {
// Values stores any context values (route params, HTTP request info, etc.)
// Generic map for any context values (route params, HTTP request info, etc.)
Values map[string]any
// internal mutex for concurrent access
mu sync.RWMutex
}
// Context pool to reduce allocations
@ -27,59 +24,19 @@ func NewContext() *Context {
// Release returns the context to the pool after clearing its values
func (c *Context) Release() {
c.mu.Lock()
defer c.mu.Unlock()
// Clear all values to prevent data leakage
for k := range c.Values {
delete(c.Values, k)
}
contextPool.Put(c)
}
// Set adds a value to the context
func (c *Context) Set(key string, value any) {
c.mu.Lock()
defer c.mu.Unlock()
c.Values[key] = value
}
// Get retrieves a value from the context
func (c *Context) Get(key string) any {
c.mu.RLock()
defer c.mu.RUnlock()
return c.Values[key]
}
// Contains checks if a key exists in the context
func (c *Context) Contains(key string) bool {
c.mu.RLock()
defer c.mu.RUnlock()
_, exists := c.Values[key]
return exists
}
// Delete removes a value from the context
func (c *Context) Delete(key string) {
c.mu.Lock()
defer c.mu.Unlock()
delete(c.Values, key)
}
// All returns a copy of all values in the context
func (c *Context) All() map[string]any {
c.mu.RLock()
defer c.mu.RUnlock()
result := make(map[string]any, len(c.Values))
for k, v := range c.Values {
result[k] = v
}
return result
}

View File

@ -9,15 +9,12 @@ import (
"git.sharkk.net/Sky/Moonshark/core/logger"
)
// StateInitFunc is a function that initializes a module in a Lua state
type StateInitFunc func(*luajit.State) error
// CoreModuleRegistry manages the initialization and reloading of core modules
type CoreModuleRegistry struct {
modules map[string]StateInitFunc // Module initializers
initOrder []string // Explicit initialization order
dependencies map[string][]string // Module dependencies
initializedFlag map[string]bool // Track which modules are initialized
modules map[string]StateInitFunc
initOrder []string // Explicit initialization order
dependencies map[string][]string // Module dependencies
initializedFlag map[string]bool // Track which modules are initialized
mu sync.RWMutex
debug bool
}
@ -41,14 +38,7 @@ func (r *CoreModuleRegistry) EnableDebug() {
// debugLog prints debug messages if enabled
func (r *CoreModuleRegistry) debugLog(format string, args ...interface{}) {
if r.debug {
logger.Debug("CoreRegistry "+format, args...)
}
}
// debugLogCont prints continuation debug messages if enabled
func (r *CoreModuleRegistry) debugLogCont(format string, args ...interface{}) {
if r.debug {
logger.DebugCont(format, args...)
logger.Debug("[CoreModuleRegistry] "+format, args...)
}
}
@ -56,18 +46,22 @@ func (r *CoreModuleRegistry) debugLogCont(format string, args ...interface{}) {
func (r *CoreModuleRegistry) Register(name string, initFunc StateInitFunc) {
r.mu.Lock()
defer r.mu.Unlock()
r.modules[name] = initFunc
// Add to initialization order if not already there
found := false
for _, n := range r.initOrder {
if n == name {
return // Already registered, silently continue
found = true
break
}
}
r.initOrder = append(r.initOrder, name)
r.debugLog("registered module %s", name)
if !found {
r.initOrder = append(r.initOrder, name)
}
r.debugLog("Registered module: %s", name)
}
// RegisterWithDependencies registers a module with explicit dependencies
@ -79,17 +73,19 @@ func (r *CoreModuleRegistry) RegisterWithDependencies(name string, initFunc Stat
r.dependencies[name] = dependencies
// Add to initialization order if not already there
found := false
for _, n := range r.initOrder {
if n == name {
return // Already registered, silently continue
found = true
break
}
}
r.initOrder = append(r.initOrder, name)
r.debugLog("registered module %s", name)
if len(dependencies) > 0 {
r.debugLogCont("Dependencies: %v", dependencies)
if !found {
r.initOrder = append(r.initOrder, name)
}
r.debugLog("Registered module %s with dependencies: %v", name, dependencies)
}
// SetInitOrder sets explicit initialization order
@ -97,32 +93,17 @@ func (r *CoreModuleRegistry) SetInitOrder(order []string) {
r.mu.Lock()
defer r.mu.Unlock()
// Create new init order
newOrder := make([]string, 0, len(order))
// First add all known modules that are in the specified order
for _, name := range order {
if _, exists := r.modules[name]; exists {
// Check for duplicates
isDuplicate := false
for _, existing := range newOrder {
if existing == name {
isDuplicate = true
break
}
}
if !isDuplicate {
newOrder = append(newOrder, name)
}
r.initOrder = append(r.initOrder, name)
}
}
// Then add any modules not in the specified order
for name := range r.modules {
// Check if module already in the new order
found := false
for _, n := range newOrder {
for _, n := range r.initOrder {
if n == name {
found = true
break
@ -130,43 +111,36 @@ func (r *CoreModuleRegistry) SetInitOrder(order []string) {
}
if !found {
newOrder = append(newOrder, name)
r.initOrder = append(r.initOrder, name)
}
}
r.initOrder = newOrder
r.debugLog("Set initialization order: %v", r.initOrder)
}
// Initialize initializes all registered modules
func (r *CoreModuleRegistry) Initialize(state *luajit.State, stateIndex int) error {
func (r *CoreModuleRegistry) Initialize(state *luajit.State) error {
r.mu.RLock()
defer r.mu.RUnlock()
verbose := stateIndex == 0
if verbose {
r.debugLog("initializing %d modules...", len(r.initOrder))
}
r.debugLog("Initializing all modules...")
// Clear initialization flags
r.initializedFlag = make(map[string]bool)
// Initialize modules in order, respecting dependencies
for _, name := range r.initOrder {
if err := r.initializeModule(state, name, []string{}, verbose); err != nil {
if err := r.initializeModule(state, name, []string{}); err != nil {
return err
}
}
if verbose {
r.debugLogCont("All modules initialized successfully")
}
r.debugLog("All modules initialized successfully")
return nil
}
// initializeModule initializes a module and its dependencies
func (r *CoreModuleRegistry) initializeModule(state *luajit.State, name string,
initStack []string, verbose bool) error {
func (r *CoreModuleRegistry) initializeModule(state *luajit.State, name string, initStack []string) error {
// Check if already initialized
if r.initializedFlag[name] {
return nil
@ -188,27 +162,23 @@ func (r *CoreModuleRegistry) initializeModule(state *luajit.State, name string,
// Initialize dependencies first
deps := r.dependencies[name]
if len(deps) > 0 {
for _, dep := range deps {
newStack := append(initStack, name)
for _, dep := range deps {
if err := r.initializeModule(state, dep, newStack, verbose); err != nil {
return err
}
if err := r.initializeModule(state, dep, newStack); err != nil {
return err
}
}
err := initFunc(state)
if err != nil {
// Always log failures regardless of verbose setting
r.debugLogCont("Initializing module %s... failure: %v", name, err)
// Initialize this module
r.debugLog("Initializing module: %s", name)
if err := initFunc(state); err != nil {
r.debugLog("Failed to initialize module %s: %v", name, err)
return fmt.Errorf("failed to initialize module %s: %w", name, err)
}
// Mark as initialized
r.initializedFlag[name] = true
if verbose {
r.debugLogCont("Initializing module %s... success", name)
}
r.debugLog("Module %s initialized successfully", name)
return nil
}
@ -221,8 +191,7 @@ func (r *CoreModuleRegistry) InitializeModule(state *luajit.State, name string)
// Clear initialization flag for this module
r.initializedFlag[name] = false
// Always use verbose logging for explicit module initialization
return r.initializeModule(state, name, []string{}, true)
return r.initializeModule(state, name, []string{})
}
// ModuleNames returns a list of all registered module names
@ -263,11 +232,17 @@ var GlobalRegistry = NewCoreModuleRegistry()
// Initialize global registry with core modules
func init() {
GlobalRegistry.EnableDebug() // Enable debugging by default
logger.Debug("[ModuleRegistry] Registering core modules...")
// Register modules
GlobalRegistry.Register("go", GoModuleInitFunc())
// Register HTTP module (no dependencies)
GlobalRegistry.Register("http", HTTPModuleInitFunc())
// Register cookie module (depends on http)
GlobalRegistry.RegisterWithDependencies("cookie", CookieModuleInitFunc(), []string{"http"})
// Register CSRF module (depends on go)
GlobalRegistry.RegisterWithDependencies("csrf", CSRFModuleInitFunc(), []string{"go"})
// Set explicit initialization order
@ -278,10 +253,11 @@ func init() {
"csrf", // Fourth: CSRF protection (uses go and possibly session)
})
logger.DebugCont("Core modules registered successfully")
logger.Debug("[CoreModuleRegistry] Core modules registered in init()")
}
// RegisterCoreModule is a helper to register a core module with the global registry
// RegisterCoreModule is a helper to register a core module
// with the global registry
func RegisterCoreModule(name string, initFunc StateInitFunc) {
GlobalRegistry.Register(name, initFunc)
}

View File

@ -179,7 +179,7 @@ func ValidateCSRFToken(state *luajit.State, ctx *Context) bool {
// WithCSRFProtection creates a runner option to add CSRF protection
func WithCSRFProtection() RunnerOption {
return func(r *Runner) {
return func(r *LuaRunner) {
r.AddInitHook(func(state *luajit.State, ctx *Context) error {
// Get request method
method, ok := ctx.Get("method").(string)

View File

@ -55,3 +55,8 @@ func GoModuleInitFunc() StateInitFunc {
return RegisterModule(state, "go", GoModuleFunctions())
}
}
// Initialize the core module during startup
func init() {
RegisterCoreModule("go", GoModuleInitFunc())
}

View File

@ -371,8 +371,7 @@ func HTTPModuleInitFunc() StateInitFunc {
// CRITICAL: Register the native Go function first
// This must be done BEFORE any Lua code that references it
if err := state.RegisterGoFunction(httpRequestFuncName, httpRequest); err != nil {
logger.Error("[HTTP Module] Failed to register __http_request function")
logger.ErrorCont("%v", err)
logger.Error("[HTTP Module] Failed to register __http_request function: %v\n", err)
return err
}
@ -381,8 +380,7 @@ func HTTPModuleInitFunc() StateInitFunc {
// Initialize Lua HTTP module
if err := state.DoString(LuaHTTPModule); err != nil {
logger.Error("[HTTP Module] Failed to initialize HTTP module Lua code")
logger.ErrorCont("%v", err)
logger.Error("[HTTP Module] Failed to initialize HTTP module Lua code: %v\n", err)
return err
}
@ -485,7 +483,7 @@ func GetHTTPResponse(state *luajit.State) (*HTTPResponse, bool) {
// WithHTTPClientConfig creates a runner option to configure the HTTP client
func WithHTTPClientConfig(config HTTPClientConfig) RunnerOption {
return func(r *Runner) {
return func(r *LuaRunner) {
// Store the config to be applied during initialization
r.AddModule("__http_client_config", map[string]any{
"max_timeout": float64(config.MaxTimeout / time.Second),
@ -512,14 +510,14 @@ func verifyHTTPClient(state *luajit.State) {
// Get the client table
state.GetGlobal("http")
if !state.IsTable(-1) {
logger.Warning("[HTTP Module] 'http' is not a table")
logger.Warning("[HTTP Module] 'http' is not a table\n")
state.Pop(1)
return
}
state.GetField(-1, "client")
if !state.IsTable(-1) {
logger.Warning("[HTTP Module] 'http.client' is not a table")
logger.Warning("[HTTP Module] 'http.client' is not a table\n")
state.Pop(2)
return
}
@ -527,14 +525,18 @@ func verifyHTTPClient(state *luajit.State) {
// Check for get function
state.GetField(-1, "get")
if !state.IsFunction(-1) {
logger.Warning("[HTTP Module] 'http.client.get' is not a function")
logger.Warning("[HTTP Module] 'http.client.get' is not a function\n")
} else {
logger.Debug("[HTTP Module] 'http.client.get' is properly registered\n")
}
state.Pop(1)
// Check for the request function
state.GetField(-1, "request")
if !state.IsFunction(-1) {
logger.Warning("[HTTP Module] 'http.client.request' is not a function")
logger.Warning("[HTTP Module] 'http.client.request' is not a function\n")
} else {
logger.Debug("[HTTP Module] 'http.client.request' is properly registered\n")
}
state.Pop(3) // Pop request, client, http
}

535
core/runner/LuaRunner.go Normal file
View File

@ -0,0 +1,535 @@
package runner
import (
"context"
"errors"
"path/filepath"
"runtime"
"sync"
"sync/atomic"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"git.sharkk.net/Sky/Moonshark/core/logger"
)
// 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
// RunnerOption defines a functional option for configuring the LuaRunner
type RunnerOption func(*LuaRunner)
// JobResult represents the result of a Lua script execution
type JobResult struct {
Value any // Return value from Lua
Error error // Error if any
}
// StateWrapper wraps a Lua state with its sandbox
type StateWrapper struct {
state *luajit.State // The Lua state
sandbox *Sandbox // Associated sandbox
index int // Index for debugging
}
// InitHook is a function that runs before executing a script
type InitHook func(*luajit.State, *Context) error
// FinalizeHook is a function that runs after executing a script
type FinalizeHook func(*luajit.State, *Context, any) error
// LuaRunner runs Lua scripts using a pool of Lua states
type LuaRunner struct {
states []*StateWrapper // Pool of Lua states
stateSem chan int // Semaphore with state indexes
poolSize int // Size of the state pool
initFunc StateInitFunc // Optional function to initialize Lua states
moduleLoader *NativeModuleLoader // Native module loader for require
isRunning atomic.Bool // Flag indicating if the runner is active
mu sync.RWMutex // Mutex for thread safety
debug bool // Enable debug logging
initHooks []InitHook // Hooks to run before script execution
finalizeHooks []FinalizeHook // Hooks to run after script execution
}
// WithPoolSize sets the state pool size
func WithPoolSize(size int) RunnerOption {
return func(r *LuaRunner) {
if size > 0 {
r.poolSize = size
}
}
}
// WithInitFunc sets the init function for the Lua state
func WithInitFunc(initFunc StateInitFunc) RunnerOption {
return func(r *LuaRunner) {
r.initFunc = initFunc
}
}
// WithLibDirs sets additional library directories
func WithLibDirs(dirs ...string) RunnerOption {
return func(r *LuaRunner) {
if r.moduleLoader == nil || r.moduleLoader.config == nil {
r.moduleLoader = NewNativeModuleLoader(&RequireConfig{
LibDirs: dirs,
})
} else {
r.moduleLoader.config.LibDirs = dirs
}
}
}
// WithDebugEnabled enables debug output
func WithDebugEnabled() RunnerOption {
return func(r *LuaRunner) {
r.debug = true
}
}
// NewRunner creates a new LuaRunner with a pool of states
func NewRunner(options ...RunnerOption) (*LuaRunner, error) {
// Default configuration
runner := &LuaRunner{
poolSize: runtime.GOMAXPROCS(0),
debug: false,
initHooks: make([]InitHook, 0),
finalizeHooks: make([]FinalizeHook, 0),
}
// Apply options
for _, opt := range options {
opt(runner)
}
// Set up module loader if not already initialized
if runner.moduleLoader == nil {
requireConfig := &RequireConfig{
ScriptDir: "",
LibDirs: []string{},
}
runner.moduleLoader = NewNativeModuleLoader(requireConfig)
}
// Initialize states and semaphore
runner.states = make([]*StateWrapper, runner.poolSize)
runner.stateSem = make(chan int, runner.poolSize)
// Create and initialize all states
for i := 0; i < runner.poolSize; i++ {
wrapper, err := runner.initState(i)
if err != nil {
runner.Close() // Clean up already created states
return nil, err
}
runner.states[i] = wrapper
runner.stateSem <- i // Add index to semaphore
}
runner.isRunning.Store(true)
return runner, nil
}
// debugLog logs a message if debug mode is enabled
func (r *LuaRunner) debugLog(format string, args ...interface{}) {
if r.debug {
logger.Debug("[LuaRunner] "+format, args...)
}
}
// initState creates and initializes a new state
func (r *LuaRunner) initState(index int) (*StateWrapper, error) {
r.debugLog("Initializing Lua state %d", index)
// Create a new state
state := luajit.New()
if state == nil {
return nil, errors.New("failed to create Lua state")
}
r.debugLog("Created new Lua state %d", index)
// Create sandbox
sandbox := NewSandbox()
if r.debug {
sandbox.EnableDebug()
}
// Set up require paths and mechanism
if err := r.moduleLoader.SetupRequire(state); err != nil {
r.debugLog("Failed to set up require for state %d: %v", index, err)
state.Cleanup()
state.Close()
return nil, ErrInitFailed
}
r.debugLog("Require system initialized for state %d", index)
// Initialize all core modules from the registry
if err := GlobalRegistry.Initialize(state); err != nil {
r.debugLog("Failed to initialize core modules for state %d: %v", index, err)
state.Cleanup()
state.Close()
return nil, ErrInitFailed
}
r.debugLog("Core modules initialized for state %d", index)
// Set up sandbox after core modules are initialized
if err := sandbox.Setup(state); err != nil {
r.debugLog("Failed to set up sandbox for state %d: %v", index, err)
state.Cleanup()
state.Close()
return nil, ErrInitFailed
}
r.debugLog("Sandbox environment set up for state %d", index)
// Preload all modules into package.loaded
if err := r.moduleLoader.PreloadAllModules(state); err != nil {
r.debugLog("Failed to preload modules for state %d: %v", index, err)
state.Cleanup()
state.Close()
return nil, errors.New("failed to preload modules")
}
r.debugLog("All modules preloaded for state %d", index)
// Run init function if provided
if r.initFunc != nil {
if err := r.initFunc(state); err != nil {
r.debugLog("Custom init function failed for state %d: %v", index, err)
state.Cleanup()
state.Close()
return nil, ErrInitFailed
}
r.debugLog("Custom init function completed for state %d", index)
}
r.debugLog("State %d initialization complete", index)
return &StateWrapper{
state: state,
sandbox: sandbox,
index: index,
}, nil
}
// AddInitHook adds a hook to be called before script execution
func (r *LuaRunner) AddInitHook(hook InitHook) {
r.mu.Lock()
defer r.mu.Unlock()
r.initHooks = append(r.initHooks, hook)
}
// AddFinalizeHook adds a hook to be called after script execution
func (r *LuaRunner) AddFinalizeHook(hook FinalizeHook) {
r.mu.Lock()
defer r.mu.Unlock()
r.finalizeHooks = append(r.finalizeHooks, hook)
}
// RunWithContext executes a Lua script with context and timeout
func (r *LuaRunner) RunWithContext(ctx context.Context, bytecode []byte, execCtx *Context, scriptPath string) (any, error) {
if !r.isRunning.Load() {
return nil, ErrRunnerClosed
}
// Create a result channel
resultChan := make(chan JobResult, 1)
// Get a state index with timeout
var stateIndex int
select {
case stateIndex = <-r.stateSem:
// Got a state
case <-ctx.Done():
return nil, ctx.Err()
}
// Launch a goroutine to execute the job
go func() {
// Make sure to return the state to the pool when done
defer func() {
// Only return if runner is still open
if r.isRunning.Load() {
select {
case r.stateSem <- stateIndex:
// State returned to pool
default:
// Pool is full or closed (shouldn't happen)
}
}
}()
// Execute the job
var result JobResult
r.mu.RLock()
state := r.states[stateIndex]
// Copy hooks to ensure we don't hold the lock during execution
initHooks := make([]InitHook, len(r.initHooks))
copy(initHooks, r.initHooks)
finalizeHooks := make([]FinalizeHook, len(r.finalizeHooks))
copy(finalizeHooks, r.finalizeHooks)
r.mu.RUnlock()
if state == nil {
result = JobResult{nil, errors.New("state is not initialized")}
} else {
// Set script directory for module resolution
if scriptPath != "" {
r.mu.Lock()
r.moduleLoader.config.ScriptDir = filepath.Dir(scriptPath)
r.mu.Unlock()
}
// Run init hooks
for _, hook := range initHooks {
if err := hook(state.state, execCtx); err != nil {
result = JobResult{nil, err}
// Send result and return early
select {
case resultChan <- result:
default:
}
return
}
}
// Convert context
var ctxMap map[string]any
if execCtx != nil {
ctxMap = execCtx.Values
}
// Execute in sandbox
value, err := state.sandbox.Execute(state.state, bytecode, ctxMap)
// Run finalize hooks
for _, hook := range finalizeHooks {
hookErr := hook(state.state, execCtx, value)
if hookErr != nil && err == nil {
// Only override nil errors
err = hookErr
}
}
result = JobResult{value, err}
}
// Send result
select {
case resultChan <- result:
// Result sent
default:
// Result channel closed or full (shouldn't happen with buffered channel)
}
}()
// Wait for result with context
select {
case result := <-resultChan:
return result.Value, result.Error
case <-ctx.Done():
// Note: we can't cancel the Lua execution, but we can stop waiting for it
// The state will be returned to the pool when the goroutine completes
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)
// Drain the semaphore (non-blocking)
for {
select {
case <-r.stateSem:
// Drained one slot
default:
// Empty
goto drained
}
}
drained:
// Clean up all states
for i := 0; i < len(r.states); i++ {
if r.states[i] != nil {
r.states[i].state.Cleanup()
r.states[i].state.Close()
r.states[i] = nil
}
}
return nil
}
// NotifyFileChanged handles file change notifications from watchers
func (r *LuaRunner) NotifyFileChanged(filePath string) bool {
r.debugLog("File change detected: %s", filePath)
r.mu.Lock()
defer r.mu.Unlock()
// Check if runner is closed
if !r.isRunning.Load() {
return false
}
// Create a new semaphore
newSem := make(chan int, cap(r.stateSem))
// Drain the current semaphore (non-blocking)
for {
select {
case <-r.stateSem:
// Drained one slot
default:
// Empty
goto drained
}
}
drained:
r.stateSem = newSem
// Reinitialize all states
success := true
for i := 0; i < len(r.states); i++ {
// Clean up old state
if r.states[i] != nil {
r.states[i].state.Cleanup()
r.states[i].state.Close()
}
// Initialize new state
wrapper, err := r.initState(i)
if err != nil {
r.debugLog("Failed to reinitialize state %d: %v", i, err)
success = false
r.states[i] = nil
continue
}
r.states[i] = wrapper
// Add to semaphore
select {
case newSem <- i:
// Added to semaphore
default:
// Semaphore full (shouldn't happen)
}
}
return success
}
// ResetModuleCache clears non-core modules from package.loaded in all states
func (r *LuaRunner) ResetModuleCache() {
if r.moduleLoader != nil {
r.debugLog("Resetting module cache in all states")
r.mu.RLock()
defer r.mu.RUnlock()
for i := 0; i < len(r.states); i++ {
if r.states[i] != nil && r.states[i].state != nil {
r.moduleLoader.ResetModules(r.states[i].state)
}
}
}
}
// ReloadAllModules reloads all modules into package.loaded in all states
func (r *LuaRunner) ReloadAllModules() error {
if r.moduleLoader != nil {
r.debugLog("Reloading all modules in all states")
r.mu.RLock()
defer r.mu.RUnlock()
for i := 0; i < len(r.states); i++ {
if r.states[i] != nil && r.states[i].state != nil {
if err := r.moduleLoader.PreloadAllModules(r.states[i].state); err != nil {
return err
}
}
}
}
return nil
}
// RefreshModuleByName invalidates a specific module in package.loaded in all states
func (r *LuaRunner) RefreshModuleByName(modName string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
success := true
for i := 0; i < len(r.states); i++ {
if r.states[i] != nil && r.states[i].state != nil {
r.debugLog("Refreshing module %s in state %d", modName, i)
if err := r.states[i].state.DoString(`package.loaded["` + modName + `"] = nil`); err != nil {
success = false
}
}
}
return success
}
// AddModule adds a module to all sandbox environments
func (r *LuaRunner) AddModule(name string, module any) {
r.debugLog("Adding module %s to all sandboxes", name)
r.mu.RLock()
defer r.mu.RUnlock()
for i := 0; i < len(r.states); i++ {
if r.states[i] != nil && r.states[i].sandbox != nil {
r.states[i].sandbox.AddModule(name, module)
}
}
}
// GetModuleCount returns the number of loaded modules in the first state
func (r *LuaRunner) GetModuleCount() int {
r.mu.RLock()
defer r.mu.RUnlock()
count := 0
// Get count from the first available state
for i := 0; i < len(r.states); i++ {
if r.states[i] != nil && r.states[i].state != nil {
// Execute a Lua snippet to count modules
if res, err := r.states[i].state.ExecuteWithResult(`
local count = 0
for _ in pairs(package.loaded) do
count = count + 1
end
return count
`); err == nil {
if num, ok := res.(float64); ok {
count = int(num)
}
}
break
}
}
return count
}

View File

@ -1,488 +0,0 @@
package runner
import (
"os"
"path/filepath"
"strings"
"sync"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"git.sharkk.net/Sky/Moonshark/core/logger"
)
// ModuleConfig holds configuration for Lua's module loading system
type ModuleConfig struct {
ScriptDir string // Base directory for script being executed
LibDirs []string // Additional library directories
}
// ModuleInfo stores information about a loaded module
type ModuleInfo struct {
Name string
Path string
IsCore bool
Bytecode []byte
}
// ModuleLoader manages module loading and caching
type ModuleLoader struct {
config *ModuleConfig
registry *ModuleRegistry
pathCache map[string]string // Cache module paths for fast lookups
bytecodeCache map[string][]byte // Cache of compiled bytecode
debug bool
mu sync.RWMutex
}
// ModuleRegistry keeps track of Lua modules for file watching
type ModuleRegistry struct {
// Maps file paths to module names
pathToModule sync.Map
// Maps module names to file paths
moduleToPath sync.Map
}
// NewModuleRegistry creates a new module registry
func NewModuleRegistry() *ModuleRegistry {
return &ModuleRegistry{}
}
// Register adds a module path to the registry
func (r *ModuleRegistry) Register(path string, name string) {
r.pathToModule.Store(path, name)
r.moduleToPath.Store(name, path)
}
// GetModuleName retrieves a module name by path
func (r *ModuleRegistry) GetModuleName(path string) (string, bool) {
value, ok := r.pathToModule.Load(path)
if !ok {
return "", false
}
return value.(string), true
}
// GetModulePath retrieves a path by module name
func (r *ModuleRegistry) GetModulePath(name string) (string, bool) {
value, ok := r.moduleToPath.Load(name)
if !ok {
return "", false
}
return value.(string), true
}
// NewModuleLoader creates a new module loader
func NewModuleLoader(config *ModuleConfig) *ModuleLoader {
if config == nil {
config = &ModuleConfig{
ScriptDir: "",
LibDirs: []string{},
}
}
return &ModuleLoader{
config: config,
registry: NewModuleRegistry(),
pathCache: make(map[string]string),
bytecodeCache: make(map[string][]byte),
debug: false,
}
}
// EnableDebug turns on debug logging
func (l *ModuleLoader) EnableDebug() {
l.debug = true
}
// debugLog logs a message if debug is enabled
func (l *ModuleLoader) debugLog(format string, args ...interface{}) {
if l.debug {
logger.Debug("[ModuleLoader] "+format, args...)
}
}
// SetScriptDir sets the script directory
func (l *ModuleLoader) SetScriptDir(dir string) {
l.mu.Lock()
defer l.mu.Unlock()
l.config.ScriptDir = dir
}
// SetupRequire configures the require system in a Lua state
func (l *ModuleLoader) SetupRequire(state *luajit.State) error {
l.mu.RLock()
defer l.mu.RUnlock()
// Initialize our module registry in Lua
err := state.DoString(`
-- Initialize global module registry
__module_paths = {}
-- Setup fast module loading system
__module_bytecode = {}
-- Create module preload table
package.preload = package.preload or {}
-- Setup module state registry
__ready_modules = {}
`)
if err != nil {
return err
}
// Set up package.path based on search paths
paths := l.getSearchPaths()
pathStr := strings.Join(paths, ";")
escapedPathStr := escapeLuaString(pathStr)
return state.DoString(`package.path = "` + escapedPathStr + `"`)
}
// getSearchPaths returns a list of Lua search paths
func (l *ModuleLoader) getSearchPaths() []string {
absPaths := []string{}
seen := map[string]bool{}
// Add script directory (highest priority)
if l.config.ScriptDir != "" {
absPath, err := filepath.Abs(l.config.ScriptDir)
if err == nil && !seen[absPath] {
absPaths = append(absPaths, filepath.Join(absPath, "?.lua"))
seen[absPath] = true
}
}
// Add lib directories
for _, dir := range l.config.LibDirs {
if dir == "" {
continue
}
absPath, err := filepath.Abs(dir)
if err == nil && !seen[absPath] {
absPaths = append(absPaths, filepath.Join(absPath, "?.lua"))
seen[absPath] = true
}
}
return absPaths
}
// PreloadModules preloads modules from library directories
func (l *ModuleLoader) PreloadModules(state *luajit.State) error {
l.mu.Lock()
defer l.mu.Unlock()
// Reset caches
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
}
for name in pairs(package.loaded) do
if not core_modules[name] then
package.loaded[name] = nil
end
end
-- Reset preload table
package.preload = {}
`); err != nil {
return err
}
// Scan and preload modules from all library directories
for _, dir := range l.config.LibDirs {
if dir == "" {
continue
}
absDir, err := filepath.Abs(dir)
if err != nil {
continue
}
// 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), ".")
// Register in our caches
l.pathCache[modName] = path
l.registry.Register(path, modName)
// Load file content
content, err := os.ReadFile(path)
if err != nil {
return nil
}
// Compile to bytecode
bytecode, err := state.CompileBytecode(string(content), path)
if err != nil {
return nil
}
// Cache bytecode
l.bytecodeCache[modName] = bytecode
// Register in Lua
escapedPath := escapeLuaString(path)
escapedName := escapeLuaString(modName)
if err := state.DoString(`__module_paths["` + escapedName + `"] = "` + escapedPath + `"`); err != nil {
return nil
}
// Load bytecode into Lua state
if err := state.LoadBytecode(bytecode, path); err != nil {
return nil
}
// Add to package.preload
luaCode := `
local modname = "` + escapedName + `"
local chunk = ...
package.preload[modname] = chunk
__ready_modules[modname] = true
`
if err := state.DoString(luaCode); err != nil {
state.Pop(1) // Remove chunk from stack
return nil
}
state.Pop(1) // Remove chunk from stack
return nil
})
if err != nil {
return err
}
}
// Install optimized require implementation
return state.DoString(`
-- Setup environment-aware require function
function __setup_require(env)
-- Create require function specific to this environment
env.require = function(modname)
-- Check if already loaded
if package.loaded[modname] then
return package.loaded[modname]
end
-- Check preloaded modules
if __ready_modules[modname] then
local loader = package.preload[modname]
if loader then
-- Set environment for loader
setfenv(loader, env)
-- Execute and store result
local result = loader()
if result == nil then
result = true
end
package.loaded[modname] = result
return result
end
end
-- Direct file load as fallback
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
local file_path = path:gsub("?", modname:gsub("%.", "/"))
local chunk, err = loadfile(file_path)
if chunk then
setfenv(chunk, env)
local result = chunk()
if result == nil then
result = true
end
package.loaded[modname] = result
return result
end
table.insert(errors, "\tno file '" .. file_path .. "'")
end
error("module '" .. modname .. "' not found:\n" .. table.concat(errors, "\n"), 2)
end
return env
end
`)
}
// GetModuleByPath finds the module name for a file path
func (l *ModuleLoader) GetModuleByPath(path string) (string, bool) {
l.mu.RLock()
defer l.mu.RUnlock()
// Clean path for proper comparison
path = filepath.Clean(path)
// Try direct lookup from registry
modName, found := l.registry.GetModuleName(path)
if found {
return modName, true
}
// Try to find by relative path from lib dirs
for _, dir := range l.config.LibDirs {
absDir, err := filepath.Abs(dir)
if err != nil {
continue
}
relPath, err := filepath.Rel(absDir, path)
if err != nil || strings.HasPrefix(relPath, "..") {
continue
}
if strings.HasSuffix(relPath, ".lua") {
modName = strings.TrimSuffix(relPath, ".lua")
modName = strings.ReplaceAll(modName, string(filepath.Separator), ".")
return modName, true
}
}
return "", false
}
// ReloadModule reloads a module from disk
func (l *ModuleLoader) ReloadModule(state *luajit.State, name string) (bool, error) {
l.mu.Lock()
defer l.mu.Unlock()
// Get module path
path, ok := l.registry.GetModulePath(name)
if !ok {
for modName, modPath := range l.pathCache {
if modName == name {
path = modPath
ok = true
break
}
}
}
if !ok || path == "" {
return false, nil
}
// Invalidate module in Lua
err := state.DoString(`
package.loaded["` + name + `"] = nil
__ready_modules["` + name + `"] = nil
if package.preload then
package.preload["` + name + `"] = nil
end
`)
if err != nil {
return false, err
}
// Check if file still exists
if _, err := os.Stat(path); os.IsNotExist(err) {
// File was deleted, just invalidate
delete(l.pathCache, name)
delete(l.bytecodeCache, name)
l.registry.moduleToPath.Delete(name)
l.registry.pathToModule.Delete(path)
return true, nil
}
// Read updated file
content, err := os.ReadFile(path)
if err != nil {
return false, err
}
// Compile to bytecode
bytecode, err := state.CompileBytecode(string(content), path)
if err != nil {
return false, err
}
// Update cache
l.bytecodeCache[name] = bytecode
// Load bytecode into state
if err := state.LoadBytecode(bytecode, path); err != nil {
return false, err
}
// Update preload
luaCode := `
local modname = "` + name + `"
package.loaded[modname] = nil
package.preload[modname] = ...
__ready_modules[modname] = true
`
if err := state.DoString(luaCode); err != nil {
state.Pop(1) // Remove chunk from stack
return false, err
}
state.Pop(1) // Remove chunk from stack
return true, nil
}
// ResetModules clears non-core modules from package.loaded
func (l *ModuleLoader) ResetModules(state *luajit.State) error {
return state.DoString(`
local core_modules = {
string = true, table = true, math = true, os = true,
package = true, io = true, coroutine = true, debug = true, _G = true
}
for name in pairs(package.loaded) do
if not core_modules[name] then
package.loaded[name] = nil
end
end
`)
}

View File

@ -2,7 +2,6 @@ package runner
import (
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"git.sharkk.net/Sky/Moonshark/core/logger"
)
// ModuleFunc is a function that returns a map of module functions
@ -20,7 +19,7 @@ func RegisterModule(state *luajit.State, name string, funcs map[string]luajit.Go
// Push function
if err := state.PushGoFunction(f); err != nil {
state.Pop(1) // Pop table
state.Pop(2) // Pop table and function name
return err
}
@ -38,7 +37,6 @@ func ModuleInitFunc(modules map[string]ModuleFunc) StateInitFunc {
return func(state *luajit.State) error {
for name, moduleFunc := range modules {
if err := RegisterModule(state, name, moduleFunc()); err != nil {
logger.Error("Failed to register module %s: %v", name, err)
return err
}
}
@ -59,27 +57,3 @@ func CombineInitFuncs(funcs ...StateInitFunc) StateInitFunc {
return nil
}
}
// RegisterLuaCode registers a Lua code snippet as a module
func RegisterLuaCode(state *luajit.State, code string) error {
return state.DoString(code)
}
// RegisterLuaCodeInitFunc returns a StateInitFunc that registers Lua code
func RegisterLuaCodeInitFunc(code string) StateInitFunc {
return func(state *luajit.State) error {
return RegisterLuaCode(state, code)
}
}
// RegisterLuaModuleInitFunc returns a StateInitFunc that registers a Lua module
func RegisterLuaModuleInitFunc(name string, code string) StateInitFunc {
return func(state *luajit.State) error {
// Create name = {} global
state.NewTable()
state.SetGlobal(name)
// Then run the module code which will populate it
return state.DoString(code)
}
}

View File

@ -22,6 +22,46 @@ type NativeModuleLoader struct {
mu sync.RWMutex
}
// ModuleRegistry keeps track of Lua modules for file watching
type ModuleRegistry struct {
// Maps file paths to module names
pathToModule sync.Map
// Maps module names to file paths (for direct access)
moduleToPath sync.Map
}
// NewModuleRegistry creates a new module registry
func NewModuleRegistry() *ModuleRegistry {
return &ModuleRegistry{
pathToModule: sync.Map{},
moduleToPath: sync.Map{},
}
}
// Register adds a module path to the registry
func (r *ModuleRegistry) Register(path string, name string) {
r.pathToModule.Store(path, name)
r.moduleToPath.Store(name, path)
}
// GetModuleName retrieves a module name by path
func (r *ModuleRegistry) GetModuleName(path string) (string, bool) {
value, ok := r.pathToModule.Load(path)
if !ok {
return "", false
}
return value.(string), true
}
// GetModulePath retrieves a path by module name
func (r *ModuleRegistry) GetModulePath(name string) (string, bool) {
value, ok := r.moduleToPath.Load(name)
if !ok {
return "", false
}
return value.(string), true
}
// NewNativeModuleLoader creates a new native module loader
func NewNativeModuleLoader(config *RequireConfig) *NativeModuleLoader {
return &NativeModuleLoader{

View File

@ -1,595 +0,0 @@
package runner
import (
"context"
"errors"
"path/filepath"
"runtime"
"sync"
"sync/atomic"
"time"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"git.sharkk.net/Sky/Moonshark/core/logger"
)
// Common errors
var (
ErrRunnerClosed = errors.New("lua runner is closed")
ErrInitFailed = errors.New("initialization failed")
ErrStateNotReady = errors.New("lua state not ready")
ErrTimeout = errors.New("operation timed out")
)
// RunnerOption defines a functional option for configuring the Runner
type RunnerOption func(*Runner)
// State wraps a Lua state with its sandbox
type State struct {
L *luajit.State // The Lua state
sandbox *Sandbox // Associated sandbox
index int // Index for debugging
inUse bool // Whether the state is currently in use
initTime time.Time // When this state was initialized
}
// InitHook runs before executing a script
type InitHook func(*luajit.State, *Context) error
// FinalizeHook runs after executing a script
type FinalizeHook func(*luajit.State, *Context, any) error
// Runner runs Lua scripts using a pool of Lua states
type Runner struct {
states []*State // All states managed by this runner
statePool chan int // Pool of available state indexes
poolSize int // Size of the state pool
moduleLoader *ModuleLoader // Module loader
isRunning atomic.Bool // Whether the runner is active
mu sync.RWMutex // Mutex for thread safety
debug bool // Enable debug logging
initHooks []InitHook // Hooks run before script execution
finalizeHooks []FinalizeHook // Hooks run after script execution
scriptDir string // Current script directory
}
// WithPoolSize sets the state pool size
func WithPoolSize(size int) RunnerOption {
return func(r *Runner) {
if size > 0 {
r.poolSize = size
}
}
}
// WithDebugEnabled enables debug output
func WithDebugEnabled() RunnerOption {
return func(r *Runner) {
r.debug = true
}
}
// WithLibDirs sets additional library directories
func WithLibDirs(dirs ...string) RunnerOption {
return func(r *Runner) {
if r.moduleLoader == nil {
r.moduleLoader = NewModuleLoader(&ModuleConfig{
LibDirs: dirs,
})
} else {
r.moduleLoader.config.LibDirs = dirs
}
}
}
// WithInitHook adds a hook to run before script execution
func WithInitHook(hook InitHook) RunnerOption {
return func(r *Runner) {
r.initHooks = append(r.initHooks, hook)
}
}
// WithFinalizeHook adds a hook to run after script execution
func WithFinalizeHook(hook FinalizeHook) RunnerOption {
return func(r *Runner) {
r.finalizeHooks = append(r.finalizeHooks, hook)
}
}
// NewRunner creates a new Runner with a pool of states
func NewRunner(options ...RunnerOption) (*Runner, error) {
// Default configuration
runner := &Runner{
poolSize: runtime.GOMAXPROCS(0),
debug: false,
initHooks: make([]InitHook, 0, 4),
finalizeHooks: make([]FinalizeHook, 0, 4),
}
// Apply options
for _, opt := range options {
opt(runner)
}
// Set up module loader if not already initialized
if runner.moduleLoader == nil {
config := &ModuleConfig{
ScriptDir: "",
LibDirs: []string{},
}
runner.moduleLoader = NewModuleLoader(config)
}
// Initialize states and pool
runner.states = make([]*State, runner.poolSize)
runner.statePool = make(chan int, runner.poolSize)
// Create and initialize all states
if err := runner.initializeStates(); err != nil {
runner.Close() // Clean up already created states
return nil, err
}
runner.isRunning.Store(true)
return runner, nil
}
// debugLog logs a message if debug mode is enabled
func (r *Runner) debugLog(format string, args ...interface{}) {
if r.debug {
logger.Debug("Runner "+format, args...)
}
}
func (r *Runner) debugLogCont(format string, args ...interface{}) {
if r.debug {
logger.DebugCont(format, args...)
}
}
// initializeStates creates and initializes all states in the pool
func (r *Runner) initializeStates() error {
r.debugLog("is initializing %d states", r.poolSize)
// Create main template state first with full logging
templateState, err := r.createState(0)
if err != nil {
return err
}
r.states[0] = templateState
r.statePool <- 0 // Add index to the pool
// Create remaining states with minimal logging
successCount := 1
for i := 1; i < r.poolSize; i++ {
state, err := r.createState(i)
if err != nil {
return err
}
r.states[i] = state
r.statePool <- i // Add index to the pool
successCount++
}
r.debugLog("has built %d/%d states successfully", successCount, r.poolSize)
return nil
}
// createState initializes a new Lua state
func (r *Runner) createState(index int) (*State, error) {
verbose := index == 0
if verbose {
r.debugLog("Creating Lua state %d", index)
}
// Create a new state
L := luajit.New()
if L == nil {
return nil, errors.New("failed to create Lua state")
}
// Create sandbox
sandbox := NewSandbox()
if r.debug && verbose {
sandbox.EnableDebug()
}
// Set up require system
if err := r.moduleLoader.SetupRequire(L); err != nil {
if verbose {
r.debugLogCont("Failed to set up require for state %d: %v", index, err)
}
L.Cleanup()
L.Close()
return nil, ErrInitFailed
}
// Initialize all core modules from the registry
if err := GlobalRegistry.Initialize(L, index); err != nil {
if verbose {
r.debugLogCont("Failed to initialize core modules for state %d: %v", index, err)
}
L.Cleanup()
L.Close()
return nil, ErrInitFailed
}
// Set up sandbox after core modules are initialized
if err := sandbox.Setup(L, index); err != nil {
if verbose {
r.debugLogCont("Failed to set up sandbox for state %d: %v", index, err)
}
L.Cleanup()
L.Close()
return nil, ErrInitFailed
}
// Preload all modules
if err := r.moduleLoader.PreloadModules(L); err != nil {
if verbose {
r.debugLogCont("Failed to preload modules for state %d: %v", index, err)
}
L.Cleanup()
L.Close()
return nil, errors.New("failed to preload modules")
}
state := &State{
L: L,
sandbox: sandbox,
index: index,
inUse: false,
initTime: time.Now(),
}
if verbose {
r.debugLog("State %d created successfully", index)
}
return state, nil
}
// Execute runs a script with context
func (r *Runner) Execute(ctx context.Context, bytecode []byte, execCtx *Context, scriptPath string) (any, error) {
if !r.isRunning.Load() {
return nil, ErrRunnerClosed
}
// Set script directory if provided
if scriptPath != "" {
r.mu.Lock()
r.scriptDir = filepath.Dir(scriptPath)
r.moduleLoader.SetScriptDir(r.scriptDir)
r.mu.Unlock()
}
// Get a state index from the pool with timeout
var stateIndex int
select {
case stateIndex = <-r.statePool:
// Got a state
case <-ctx.Done():
return nil, ctx.Err()
}
// Get the actual state
r.mu.RLock()
state := r.states[stateIndex]
r.mu.RUnlock()
if state == nil {
// This shouldn't happen, but recover gracefully
r.statePool <- stateIndex
return nil, ErrStateNotReady
}
// Mark state as in use
state.inUse = true
// Ensure state is returned to pool when done
defer func() {
state.inUse = false
if r.isRunning.Load() {
select {
case r.statePool <- stateIndex:
// State returned to pool
default:
// Pool is full or closed (shouldn't happen)
}
}
}()
// Copy hooks to avoid holding lock during execution
r.mu.RLock()
initHooks := make([]InitHook, len(r.initHooks))
copy(initHooks, r.initHooks)
finalizeHooks := make([]FinalizeHook, len(r.finalizeHooks))
copy(finalizeHooks, r.finalizeHooks)
r.mu.RUnlock()
// Run init hooks
for _, hook := range initHooks {
if err := hook(state.L, execCtx); err != nil {
return nil, err
}
}
// Prepare context values
var ctxValues map[string]any
if execCtx != nil {
ctxValues = execCtx.Values
}
// Execute in sandbox
result, err := state.sandbox.Execute(state.L, bytecode, ctxValues)
// Run finalize hooks
for _, hook := range finalizeHooks {
if hookErr := hook(state.L, execCtx, result); hookErr != nil && err == nil {
err = hookErr
}
}
return result, err
}
// Run executes a Lua script (convenience wrapper)
func (r *Runner) Run(bytecode []byte, execCtx *Context, scriptPath string) (any, error) {
return r.Execute(context.Background(), bytecode, execCtx, scriptPath)
}
// Close gracefully shuts down the Runner
func (r *Runner) Close() error {
r.mu.Lock()
defer r.mu.Unlock()
if !r.isRunning.Load() {
return ErrRunnerClosed
}
r.isRunning.Store(false)
r.debugLog("Closing Runner and destroying all states")
// Drain the state pool
r.drainStatePool()
// Clean up all states
for i, state := range r.states {
if state != nil {
state.L.Cleanup()
state.L.Close()
r.states[i] = nil
}
}
return nil
}
// drainStatePool removes all states from the pool
func (r *Runner) drainStatePool() {
for {
select {
case <-r.statePool:
// Drain one state
default:
// Pool is empty
return
}
}
}
// RefreshStates rebuilds all states in the pool
func (r *Runner) RefreshStates() error {
r.mu.Lock()
defer r.mu.Unlock()
if !r.isRunning.Load() {
return ErrRunnerClosed
}
r.debugLog("Refreshing all Lua states")
// Drain all states from the pool
r.drainStatePool()
// Destroy all existing states
for i, state := range r.states {
if state != nil {
if state.inUse {
r.debugLog("Warning: attempting to refresh state %d that is in use", i)
}
state.L.Cleanup()
state.L.Close()
r.states[i] = nil
}
}
// Reinitialize all states
if err := r.initializeStates(); err != nil {
return err
}
r.debugLog("All states refreshed successfully")
return nil
}
// NotifyFileChanged handles file change notifications
func (r *Runner) NotifyFileChanged(filePath string) bool {
r.debugLog("File change detected: %s", filePath)
// Check if it's a module file
module, isModule := r.moduleLoader.GetModuleByPath(filePath)
if isModule {
r.debugLog("File is a module: %s", module)
return r.RefreshModule(module)
}
// For non-module files, refresh all states
if err := r.RefreshStates(); err != nil {
r.debugLog("Failed to refresh states: %v", err)
return false
}
return true
}
// RefreshModule refreshes a specific module across all states
func (r *Runner) RefreshModule(moduleName string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
if !r.isRunning.Load() {
return false
}
r.debugLog("Refreshing module: %s", moduleName)
// Check if it's a core module
coreName, isCore := GlobalRegistry.MatchModuleName(moduleName)
success := true
for _, state := range r.states {
if state == nil {
continue
}
// Skip states that are in use
if state.inUse {
r.debugLog("Skipping refresh for state %d (in use)", state.index)
success = false
continue
}
// Invalidate module in Lua
if err := state.L.DoString(`package.loaded["` + moduleName + `"] = nil`); err != nil {
r.debugLog("Failed to invalidate module %s in state %d: %v",
moduleName, state.index, err)
success = false
continue
}
// For core modules, reinitialize them
if isCore {
if err := GlobalRegistry.InitializeModule(state.L, coreName); err != nil {
r.debugLog("Failed to reinitialize core module %s in state %d: %v",
coreName, state.index, err)
success = false
}
}
}
if success {
r.debugLog("Module %s refreshed successfully in all states", moduleName)
} else {
r.debugLog("Module %s refresh had some failures", moduleName)
}
return success
}
// AddModule adds a module to all sandbox environments
func (r *Runner) AddModule(name string, module any) {
r.debugLog("Adding module %s to all sandboxes", name)
r.mu.RLock()
defer r.mu.RUnlock()
for _, state := range r.states {
if state != nil && state.sandbox != nil && !state.inUse {
state.sandbox.AddModule(name, module)
}
}
}
// AddInitHook adds a hook to be called before script execution
func (r *Runner) AddInitHook(hook InitHook) {
r.mu.Lock()
defer r.mu.Unlock()
r.initHooks = append(r.initHooks, hook)
}
// AddFinalizeHook adds a hook to be called after script execution
func (r *Runner) AddFinalizeHook(hook FinalizeHook) {
r.mu.Lock()
defer r.mu.Unlock()
r.finalizeHooks = append(r.finalizeHooks, hook)
}
// ResetModuleCache clears the module cache in all states
func (r *Runner) ResetModuleCache() {
r.mu.RLock()
defer r.mu.RUnlock()
if !r.isRunning.Load() {
return
}
r.debugLog("Resetting module cache in all states")
for _, state := range r.states {
if state != nil && !state.inUse {
r.moduleLoader.ResetModules(state.L)
}
}
}
// GetStateCount returns the number of initialized states
func (r *Runner) GetStateCount() int {
r.mu.RLock()
defer r.mu.RUnlock()
count := 0
for _, state := range r.states {
if state != nil {
count++
}
}
return count
}
// GetActiveStateCount returns the number of states currently in use
func (r *Runner) GetActiveStateCount() int {
r.mu.RLock()
defer r.mu.RUnlock()
count := 0
for _, state := range r.states {
if state != nil && state.inUse {
count++
}
}
return count
}
// GetModuleCount returns the number of loaded modules in the first available state
func (r *Runner) GetModuleCount() int {
r.mu.RLock()
defer r.mu.RUnlock()
if !r.isRunning.Load() {
return 0
}
// Find first available state
for _, state := range r.states {
if state != nil && !state.inUse {
// Execute a Lua snippet to count modules
if res, err := state.L.ExecuteWithResult(`
local count = 0
for _ in pairs(package.loaded) do
count = count + 1
end
return count
`); err == nil {
if num, ok := res.(float64); ok {
return int(num)
}
}
break
}
}
return 0
}

View File

@ -2,135 +2,119 @@ package runner
import (
"fmt"
"sync"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"git.sharkk.net/Sky/Moonshark/core/logger"
)
// Sandbox provides a secure execution environment for Lua scripts
// Sandbox manages a simplified Lua environment
type Sandbox struct {
modules map[string]any // Custom modules for environment
debug bool // Enable debug output
mu sync.RWMutex // Protects modules
}
// NewSandbox creates a new sandbox environment
// NewSandbox creates a new sandbox
func NewSandbox() *Sandbox {
return &Sandbox{
modules: make(map[string]any, 8), // Pre-allocate with reasonable capacity
modules: make(map[string]any),
debug: false,
}
}
// EnableDebug turns on debug logging
// EnableDebug turns on debug output
func (s *Sandbox) EnableDebug() {
s.debug = true
}
// debugLog logs a message if debug mode is enabled
func (s *Sandbox) debugLog(format string, args ...interface{}) {
if s.debug {
logger.Debug("Sandbox "+format, args...)
}
}
// debugLog logs a message if debug mode is enabled
func (s *Sandbox) debugLogCont(format string, args ...interface{}) {
if s.debug {
logger.DebugCont(format, args...)
}
}
// AddModule adds a module to the sandbox environment
func (s *Sandbox) AddModule(name string, module any) {
s.mu.Lock()
defer s.mu.Unlock()
s.modules[name] = module
s.debugLog("Added module: %s", name)
}
// debugLog prints debug messages if debug is enabled
func (s *Sandbox) debugLog(format string, args ...interface{}) {
if s.debug {
logger.Debug("[Sandbox Debug] "+format, args...)
}
}
// Setup initializes the sandbox in a Lua state
func (s *Sandbox) Setup(state *luajit.State, stateIndex int) error {
verbose := stateIndex == 0
if verbose {
s.debugLog("is setting up...")
}
func (s *Sandbox) Setup(state *luajit.State) error {
s.debugLog("Setting up sandbox environment")
// Register modules in the global environment
s.mu.RLock()
for name, module := range s.modules {
if verbose {
s.debugLog("is registering module: %s", name)
}
s.debugLog("Registering module: %s", name)
if err := state.PushValue(module); err != nil {
s.mu.RUnlock()
if verbose {
s.debugLog("failed to register module %s: %v", name, err)
}
s.debugLog("Failed to register module %s: %v", name, err)
return err
}
state.SetGlobal(name)
}
s.mu.RUnlock()
// Initialize environment setup
// Initialize simple environment setup
err := state.DoString(`
-- Global tables for response handling
__http_responses = __http_responses or {}
-- Global tables for response handling
__http_responses = __http_responses or {}
-- Create environment inheriting from _G
function __create_env(ctx)
-- Create environment with metatable inheriting from _G
local env = setmetatable({}, {__index = _G})
-- Simple environment creation
function __create_env(ctx)
-- Create environment inheriting from _G
local env = setmetatable({}, {__index = _G})
-- Add context if provided
if ctx then
env.ctx = ctx
end
-- Add context if provided
if ctx then
env.ctx = ctx
end
-- Add proper require function to this environment
if __setup_require then
__setup_require(env)
end
return env
end
return env
end
-- Execute script with clean environment
function __execute_script(fn, ctx)
-- Clear previous responses
__http_responses[1] = nil
-- Execute script with clean environment
function __execute_script(fn, ctx)
-- Clear previous responses
__http_responses[1] = nil
-- Create environment
local env = __create_env(ctx)
-- Create environment
local env = __create_env(ctx)
-- Set environment for function
setfenv(fn, env)
-- Set environment for function
setfenv(fn, env)
-- Execute with protected call
local ok, result = pcall(fn)
if not ok then
error(result, 0)
end
-- Execute with protected call
local ok, result = pcall(fn)
if not ok then
error(result, 0)
end
return result
end
`)
return result
end
`)
if err != nil {
if verbose {
s.debugLog("failed to set up...")
s.debugLogCont("%v", err)
}
s.debugLog("Failed to set up sandbox: %v", err)
return err
}
if verbose {
s.debugLogCont("Complete")
}
s.debugLog("Sandbox setup complete")
// Verify HTTP module is accessible
httpResult, _ := state.ExecuteWithResult(`
if type(http) == "table" and
type(http.client) == "table" and
type(http.client.get) == "function" then
return "HTTP module verified OK"
else
local status = {
http = type(http),
client = type(http) == "table" and type(http.client) or "N/A",
get = type(http) == "table" and type(http.client) == "table" and type(http.client.get) or "N/A"
}
return status
end
`)
s.debugLog("HTTP verification result: %v", httpResult)
return nil
}
@ -139,7 +123,7 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx map[string]a
// Load bytecode
if err := state.LoadBytecode(bytecode, "script"); err != nil {
s.debugLog("Failed to load bytecode: %v", err)
return nil, fmt.Errorf("failed to load script: %w", err)
return nil, err
}
// Prepare context
@ -148,9 +132,9 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx map[string]a
for k, v := range ctx {
state.PushString(k)
if err := state.PushValue(v); err != nil {
state.Pop(2) // Pop key and table
state.Pop(2)
s.debugLog("Failed to push context value %s: %v", k, err)
return nil, fmt.Errorf("failed to prepare context: %w", err)
return nil, err
}
state.SetTable(-3)
}
@ -161,33 +145,32 @@ func (s *Sandbox) Execute(state *luajit.State, bytecode []byte, ctx map[string]a
// Get execution function
state.GetGlobal("__execute_script")
if !state.IsFunction(-1) {
state.Pop(2) // Pop context and non-function
state.Pop(2) // Pop nil and non-function
s.debugLog("__execute_script is not a function")
return nil, fmt.Errorf("sandbox execution function not found")
}
// Stack setup for call: __execute_script, bytecode function, context
state.PushCopy(-3) // bytecode function (copy from -3)
state.PushCopy(-3) // context (copy from -3)
// Push arguments
state.PushCopy(-3) // bytecode function
state.PushCopy(-3) // context
// Clean up duplicate references
state.Remove(-5) // Remove original bytecode function
state.Remove(-4) // Remove original context
// Clean up stack
state.Remove(-5) // original bytecode
state.Remove(-4) // original context
// Call with 2 args (function, context), 1 result
// Call with 2 args, 1 result
if err := state.Call(2, 1); err != nil {
s.debugLog("Execution failed: %v", err)
return nil, fmt.Errorf("script execution failed: %w", err)
return nil, err
}
// Get result
result, err := state.ToValue(-1)
state.Pop(1) // Pop result
state.Pop(1)
// Check for HTTP response
httpResponse, hasResponse := GetHTTPResponse(state)
if hasResponse {
// Add the script result as the response body
httpResponse.Body = result
return httpResponse, nil
}

View File

@ -36,7 +36,7 @@ func (h *SessionHandler) debug(format string, args ...interface{}) {
// WithSessionManager creates a RunnerOption to add session support
func WithSessionManager(manager *sessions.SessionManager) RunnerOption {
return func(r *Runner) {
return func(r *LuaRunner) {
handler := NewSessionHandler(manager)
// Register the session module

View File

@ -39,6 +39,7 @@ func WatchDirectory(config DirectoryWatcherConfig, manager *WatcherManager) (*Wa
manager: manager,
}
logger.Debug("Started watching directory: %s", config.Dir)
return w, nil
}
@ -57,7 +58,7 @@ func (w *Watcher) Close() error {
// WatchLuaRouter sets up a watcher for a LuaRouter's routes directory; also updates
// the LuaRunner so that the state can be rebuilt
func WatchLuaRouter(router *routers.LuaRouter, runner *runner.Runner, routesDir string) (*Watcher, error) {
func WatchLuaRouter(router *routers.LuaRouter, runner *runner.LuaRunner, routesDir string) (*Watcher, error) {
manager := GetWatcherManager(true)
runnerRefresh := func() error {
@ -103,7 +104,7 @@ func WatchStaticRouter(router *routers.StaticRouter, staticDir string) (*Watcher
}
// WatchLuaModules sets up watchers for Lua module directories
func WatchLuaModules(luaRunner *runner.Runner, libDirs []string) ([]*Watcher, error) {
func WatchLuaModules(luaRunner *runner.LuaRunner, libDirs []string) ([]*Watcher, error) {
manager := GetWatcherManager(true)
watchers := make([]*Watcher, 0, len(libDirs))
@ -114,7 +115,8 @@ func WatchLuaModules(luaRunner *runner.Runner, libDirs []string) ([]*Watcher, er
callback := func() error {
logger.Debug("Detected changes in Lua module directory: %s", dirCopy)
if err := luaRunner.RefreshStates(); err != nil {
// Reload modules from this directory
if err := luaRunner.ReloadAllModules(); err != nil {
logger.Warning("Error reloading modules: %v", err)
}

View File

@ -67,7 +67,7 @@ func (m *WatcherManager) AddWatcher(watcher *DirectoryWatcher) {
defer m.mu.Unlock()
m.watchers[watcher.dir] = watcher
logger.Debug("WatcherManager added watcher for %s", watcher.dir)
logger.Debug("[WatcherManager] Added watcher for directory: %s", watcher.dir)
}
// RemoveWatcher unregisters a directory watcher
@ -76,7 +76,7 @@ func (m *WatcherManager) RemoveWatcher(dir string) {
defer m.mu.Unlock()
delete(m.watchers, dir)
logger.Debug("WatcherManager removed watcher for %s", dir)
logger.Debug("[WatcherManager] Removed watcher for directory: %s", dir)
}
// pollLoop is the main polling loop that checks all watched directories

11
main.go
View File

@ -1,7 +1,6 @@
package main
import (
"fmt"
"os"
"os/signal"
"syscall"
@ -10,25 +9,29 @@ import (
)
func main() {
logger.InitGlobalLogger(logger.LevelDebug, true, false)
logger.Server("Moonshark is starting to prowl 🦈")
// Initialize global logger
logger.InitGlobalLogger(logger.LevelDebug, true)
logger.Server("Starting Moonshark server")
// Create and initialize server
server, err := NewMoonshark("config.lua")
if err != nil {
logger.Fatal("Failed to initialize server: %v", err)
}
// Start server
if err := server.Start(); err != nil {
logger.Fatal("Failed to start server: %v", err)
}
// Wait for interrupt signal
stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt, syscall.SIGTERM)
<-stop
fmt.Print("\n")
logger.Server("Shutdown signal received")
// Shutdown server
if err := server.Shutdown(); err != nil {
logger.Error("Error during shutdown: %v", err)
os.Exit(1)