rewrite router, server and runner

This commit is contained in:
Sky Johnson 2025-06-04 19:06:47 -05:00
parent d6e24f0185
commit e3ee503c31
13 changed files with 763 additions and 1458 deletions

View File

@ -1,3 +1,4 @@
// server.go - Simplified HTTP server
package http package http
import ( import (
@ -9,7 +10,6 @@ import (
"Moonshark/router" "Moonshark/router"
"Moonshark/runner" "Moonshark/runner"
"Moonshark/runner/lualibs"
"Moonshark/sessions" "Moonshark/sessions"
"Moonshark/utils" "Moonshark/utils"
"Moonshark/utils/color" "Moonshark/utils/color"
@ -17,46 +17,35 @@ import (
"Moonshark/utils/logger" "Moonshark/utils/logger"
"Moonshark/utils/metadata" "Moonshark/utils/metadata"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
var ( var (
//methodGET = []byte("GET")
methodPOST = []byte("POST")
methodPUT = []byte("PUT")
methodPATCH = []byte("PATCH")
debugPath = []byte("/debug/stats") debugPath = []byte("/debug/stats")
staticMethods = map[string]bool{"GET": true, "HEAD": true, "OPTIONS": true}
cached404, cached500 []byte
cacheMu sync.RWMutex
emptyMap = make(map[string]any)
) )
type Server struct { type Server struct {
luaRouter *router.LuaRouter luaRouter *router.Router
staticHandler fasthttp.RequestHandler staticHandler fasthttp.RequestHandler
staticFS *fasthttp.FS
luaRunner *runner.Runner luaRunner *runner.Runner
fasthttpServer *fasthttp.Server fasthttpServer *fasthttp.Server
debugMode bool
cfg *config.Config
sessionManager *sessions.SessionManager sessionManager *sessions.SessionManager
errorConfig utils.ErrorPageConfig cfg *config.Config
ctxPool sync.Pool debugMode bool
paramsPool sync.Pool
staticPrefix string
staticPrefixBytes []byte staticPrefixBytes []byte
// Cached error pages
cached404 []byte
cached500 []byte
errorCacheMu sync.RWMutex
} }
func New(luaRouter *router.LuaRouter, runner *runner.Runner, cfg *config.Config, debugMode bool) *Server { func New(luaRouter *router.Router, runner *runner.Runner, cfg *config.Config, debugMode bool) *Server {
staticPrefix := cfg.Server.StaticPrefix staticPrefix := cfg.Server.StaticPrefix
if !strings.HasPrefix(staticPrefix, "/") { if !strings.HasPrefix(staticPrefix, "/") {
staticPrefix = "/" + staticPrefix staticPrefix = "/" + staticPrefix
} }
if !strings.HasSuffix(staticPrefix, "/") { if !strings.HasSuffix(staticPrefix, "/") {
staticPrefix = staticPrefix + "/" staticPrefix += "/"
} }
s := &Server{ s := &Server{
@ -65,42 +54,31 @@ func New(luaRouter *router.LuaRouter, runner *runner.Runner, cfg *config.Config,
debugMode: debugMode, debugMode: debugMode,
cfg: cfg, cfg: cfg,
sessionManager: sessions.GlobalSessionManager, sessionManager: sessions.GlobalSessionManager,
staticPrefix: staticPrefix,
staticPrefixBytes: []byte(staticPrefix), staticPrefixBytes: []byte(staticPrefix),
errorConfig: utils.ErrorPageConfig{
OverrideDir: cfg.Dirs.Override,
DebugMode: debugMode,
},
ctxPool: sync.Pool{
New: func() any {
return make(map[string]any, 6)
},
},
paramsPool: sync.Pool{
New: func() any {
return make(map[string]any, 4)
},
},
} }
// Pre-cache error pages // Cache error pages
s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, "")) errorConfig := utils.ErrorPageConfig{
s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error")) OverrideDir: cfg.Dirs.Override,
DebugMode: debugMode,
}
cacheMu.Lock()
cached404 = []byte(utils.NotFoundPage(errorConfig, ""))
cached500 = []byte(utils.InternalErrorPage(errorConfig, "", "Internal Server Error"))
cacheMu.Unlock()
// Setup static file serving // Setup static file serving
if cfg.Dirs.Static != "" { if cfg.Dirs.Static != "" {
s.staticFS = &fasthttp.FS{ staticFS := &fasthttp.FS{
Root: cfg.Dirs.Static, Root: cfg.Dirs.Static,
IndexNames: []string{"index.html"}, IndexNames: []string{"index.html"},
GenerateIndexPages: false,
AcceptByteRange: true, AcceptByteRange: true,
Compress: true, Compress: true,
CompressedFileSuffix: ".gz", CompressedFileSuffix: ".gz",
CompressBrotli: true, CompressBrotli: true,
CompressZstd: true,
PathRewrite: fasthttp.NewPathPrefixStripper(len(staticPrefix) - 1), PathRewrite: fasthttp.NewPathPrefixStripper(len(staticPrefix) - 1),
} }
s.staticHandler = s.staticFS.NewRequestHandler() s.staticHandler = staticFS.NewRequestHandler()
} }
s.fasthttpServer = &fasthttp.Server{ s.fasthttpServer = &fasthttp.Server{
@ -111,12 +89,9 @@ func New(luaRouter *router.LuaRouter, runner *runner.Runner, cfg *config.Config,
IdleTimeout: 120 * time.Second, IdleTimeout: 120 * time.Second,
MaxRequestBodySize: 16 << 20, MaxRequestBodySize: 16 << 20,
TCPKeepalive: true, TCPKeepalive: true,
TCPKeepalivePeriod: 60 * time.Second,
ReduceMemoryUsage: true, ReduceMemoryUsage: true,
DisablePreParseMultipartForm: true,
DisableHeaderNamesNormalizing: true,
NoDefaultServerHeader: true,
StreamRequestBody: true, StreamRequestBody: true,
NoDefaultServerHeader: true,
} }
return s return s
@ -133,138 +108,66 @@ func (s *Server) Shutdown(ctx context.Context) error {
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) { func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
start := time.Now() start := time.Now()
methodBytes := ctx.Method() method := string(ctx.Method())
pathBytes := ctx.Path() path := string(ctx.Path())
if s.debugMode && bytes.Equal(pathBytes, debugPath) { // Debug stats endpoint
if s.debugMode && bytes.Equal(ctx.Path(), debugPath) {
s.handleDebugStats(ctx) s.handleDebugStats(ctx)
if s.cfg.Server.HTTPLogging { s.logRequest(ctx, method, path, time.Since(start))
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
}
return return
} }
if s.staticHandler != nil && bytes.HasPrefix(pathBytes, s.staticPrefixBytes) { // Static file serving
if s.staticHandler != nil && bytes.HasPrefix(ctx.Path(), s.staticPrefixBytes) {
s.staticHandler(ctx) s.staticHandler(ctx)
if s.cfg.Server.HTTPLogging { s.logRequest(ctx, method, path, time.Since(start))
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
}
return return
} }
bytecode, scriptPath, routeErr, params, found := s.luaRouter.GetRouteInfo(methodBytes, pathBytes) // Route lookup
bytecode, params, found := s.luaRouter.Lookup(method, path)
if found { if !found {
if len(bytecode) == 0 || routeErr != nil { s.send404(ctx)
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, routeErr) s.logRequest(ctx, method, path, time.Since(start))
} else { return
s.handleLuaRoute(ctx, bytecode, scriptPath, params, methodBytes, pathBytes)
}
} else {
s.send404(ctx, pathBytes)
} }
if s.cfg.Server.HTTPLogging { if len(bytecode) == 0 {
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start)) s.send500(ctx, nil)
} s.logRequest(ctx, method, path, time.Since(start))
return
} }
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, // Get session
params *router.Params, methodBytes, pathBytes []byte) {
luaCtx := runner.NewHTTPContext(ctx)
defer luaCtx.Release()
if lualibs.GetGlobalEnvManager() != nil {
luaCtx.Set("env", lualibs.GetGlobalEnvManager().GetAll())
}
sessionMap := s.ctxPool.Get().(map[string]any)
defer func() {
for k := range sessionMap {
delete(sessionMap, k)
}
s.ctxPool.Put(sessionMap)
}()
session := s.sessionManager.GetSessionFromRequest(ctx) session := s.sessionManager.GetSessionFromRequest(ctx)
// Advance flash data (move current flash to old, clear old) // Execute Lua script
session.AdvanceFlash() response, err := s.luaRunner.ExecuteHTTP(bytecode, ctx, params, session)
sessionMap["id"] = session.ID
// Get session data and flash data
if !session.IsEmpty() {
sessionMap["data"] = session.GetAll() // This now includes flash data
sessionMap["flash"] = session.GetAllFlash()
} else {
sessionMap["data"] = emptyMap
sessionMap["flash"] = emptyMap
}
// Set basic context
luaCtx.Set("method", string(methodBytes))
luaCtx.Set("path", string(pathBytes))
luaCtx.Set("host", string(ctx.Host()))
luaCtx.Set("session", sessionMap)
// Add headers to context
headers := make(map[string]any)
ctx.Request.Header.VisitAll(func(key, value []byte) {
headers[string(key)] = string(value)
})
luaCtx.Set("headers", headers)
// Handle params
if params != nil && params.Count > 0 {
paramMap := s.paramsPool.Get().(map[string]any)
for i := range params.Count {
paramMap[params.Keys[i]] = params.Values[i]
}
luaCtx.Set("params", paramMap)
defer func() {
for k := range paramMap {
delete(paramMap, k)
}
s.paramsPool.Put(paramMap)
}()
} else {
luaCtx.Set("params", emptyMap)
}
// Parse form data for POST/PUT/PATCH
if bytes.Equal(methodBytes, methodPOST) ||
bytes.Equal(methodBytes, methodPUT) ||
bytes.Equal(methodBytes, methodPATCH) {
if formData, err := ParseForm(ctx); err == nil {
luaCtx.Set("form", formData)
} else {
if s.debugMode {
logger.Warnf("Error parsing form: %v", err)
}
luaCtx.Set("form", emptyMap)
}
} else {
luaCtx.Set("form", emptyMap)
}
response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath)
if err != nil { if err != nil {
logger.Errorf("Lua execution error: %v", err) logger.Errorf("Lua execution error: %v", err)
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, err) s.send500(ctx, err)
s.logRequest(ctx, method, path, time.Since(start))
return return
} }
// Handle session updates including flash data // Apply response
if len(response.SessionData) > 0 { s.applyResponse(ctx, response, session)
if _, clearAll := response.SessionData["__clear_all"]; clearAll { runner.ReleaseResponse(response)
session.Clear()
session.ClearFlash() // Also clear flash data s.logRequest(ctx, method, path, time.Since(start))
delete(response.SessionData, "__clear_all")
} }
for k, v := range response.SessionData { func (s *Server) applyResponse(ctx *fasthttp.RequestCtx, resp *runner.Response, session *sessions.Session) {
// Handle session updates
if len(resp.SessionData) > 0 {
if _, clearAll := resp.SessionData["__clear_all"]; clearAll {
session.Clear()
session.ClearFlash()
delete(resp.SessionData, "__clear_all")
}
for k, v := range resp.SessionData {
if v == "__DELETE__" { if v == "__DELETE__" {
session.Delete(k) session.Delete(k)
} else { } else {
@ -273,91 +176,61 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
} }
} }
// Handle flash data from response // Handle flash data
if flashData, ok := response.Metadata["flash"].(map[string]any); ok { if flashData, ok := resp.Metadata["flash"].(map[string]any); ok {
for k, v := range flashData { for k, v := range flashData {
if err := session.FlashSafe(k, v); err != nil && s.debugMode { if err := session.FlashSafe(k, v); err != nil && s.debugMode {
logger.Warnf("Error setting flash data %s: %v", k, err) logger.Warnf("Error setting flash data %s: %v", k, err)
} }
} }
delete(response.Metadata, "flash") // Remove from metadata after processing
} }
// Apply session cookie
s.sessionManager.ApplySessionCookie(ctx, session) s.sessionManager.ApplySessionCookie(ctx, session)
runner.ApplyResponse(response, ctx)
runner.ReleaseResponse(response) // Apply HTTP response
runner.ApplyResponse(resp, ctx)
} }
func (s *Server) send404(ctx *fasthttp.RequestCtx, pathBytes []byte) { func (s *Server) send404(ctx *fasthttp.RequestCtx) {
ctx.SetContentType("text/html; charset=utf-8") ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusNotFound) ctx.SetStatusCode(fasthttp.StatusNotFound)
cacheMu.RLock()
// Use cached 404 for common case ctx.SetBody(cached404)
if len(pathBytes) == 1 && pathBytes[0] == '/' { cacheMu.RUnlock()
s.errorCacheMu.RLock()
ctx.SetBody(s.cached404)
s.errorCacheMu.RUnlock()
} else {
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, string(pathBytes))))
}
} }
func (s *Server) sendError(ctx *fasthttp.RequestCtx, status int, pathBytes []byte, err error) { func (s *Server) send500(ctx *fasthttp.RequestCtx, err error) {
ctx.SetContentType("text/html; charset=utf-8") ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(status) ctx.SetStatusCode(fasthttp.StatusInternalServerError)
if err == nil { if err == nil {
s.errorCacheMu.RLock() cacheMu.RLock()
ctx.SetBody(s.cached500) ctx.SetBody(cached500)
s.errorCacheMu.RUnlock() cacheMu.RUnlock()
return
}
var errorMessage string
if luaErr, ok := err.(*luajit.LuaError); ok {
// Use just the message if stack trace is empty
if luaErr.StackTrace == "" {
errorMessage = luaErr.Message
} else { } else {
errorMessage = err.Error() // Full error with stack trace errorConfig := utils.ErrorPageConfig{
OverrideDir: s.cfg.Dirs.Override,
DebugMode: s.debugMode,
} }
} else { ctx.SetBody([]byte(utils.InternalErrorPage(errorConfig, string(ctx.Path()), err.Error())))
errorMessage = err.Error()
} }
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, string(pathBytes), errorMessage)))
} }
func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) { func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
stats := utils.CollectSystemStats(s.cfg) stats := utils.CollectSystemStats(s.cfg)
routeCount, bytecodeBytes := s.luaRouter.GetRouteStats()
stats.Components = utils.ComponentStats{ stats.Components = utils.ComponentStats{
RouteCount: routeCount, RouteCount: 0, // TODO: Get from router
BytecodeBytes: bytecodeBytes, BytecodeBytes: 0, // TODO: Get from router
SessionStats: sessions.GlobalSessionManager.GetCacheStats(), SessionStats: s.sessionManager.GetCacheStats(),
} }
ctx.SetContentType("text/html; charset=utf-8") ctx.SetContentType("text/html; charset=utf-8")
ctx.SetStatusCode(fasthttp.StatusOK) ctx.SetStatusCode(fasthttp.StatusOK)
ctx.SetBody([]byte(utils.DebugStatsPage(stats))) ctx.SetBody([]byte(utils.DebugStatsPage(stats)))
} }
// SetStaticCaching enables/disables static file caching func (s *Server) logRequest(ctx *fasthttp.RequestCtx, method, path string, duration time.Duration) {
func (s *Server) SetStaticCaching(duration time.Duration) { if s.cfg.Server.HTTPLogging {
if s.staticFS != nil { logger.Request(ctx.Response.StatusCode(), method, path, duration)
s.staticFS.CacheDuration = duration
s.staticHandler = s.staticFS.NewRequestHandler()
} }
} }
// GetStaticPrefix returns the URL prefix for static files
func (s *Server) GetStaticPrefix() string {
return s.staticPrefix
}
// UpdateErrorCache refreshes cached error pages
func (s *Server) UpdateErrorCache() {
s.errorCacheMu.Lock()
s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, ""))
s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error"))
s.errorCacheMu.Unlock()
}

View File

@ -10,14 +10,11 @@ import (
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
var ( var formDataPool = sync.Pool{
emptyMap = make(map[string]any)
formDataPool = sync.Pool{
New: func() any { New: func() any {
return make(map[string]any, 16) return make(map[string]any, 16)
}, },
} }
)
func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any { func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
args := ctx.QueryArgs() args := ctx.QueryArgs()

View File

@ -8,6 +8,7 @@ import (
"os" "os"
"os/signal" "os/signal"
"path/filepath" "path/filepath"
"runtime"
"strconv" "strconv"
"syscall" "syscall"
"time" "time"
@ -28,7 +29,7 @@ import (
type Moonshark struct { type Moonshark struct {
Config *config.Config Config *config.Config
LuaRouter *router.LuaRouter LuaRouter *router.Router
LuaRunner *runner.Runner LuaRunner *runner.Runner
HTTPServer *http.Server HTTPServer *http.Server
cleanupFuncs []func() error cleanupFuncs []func() error
@ -95,6 +96,9 @@ func newMoonshark(cfg *config.Config, debug, scriptMode bool) (*Moonshark, error
if scriptMode { if scriptMode {
poolSize = 1 poolSize = 1
} }
if poolSize == 0 {
poolSize = runtime.GOMAXPROCS(0)
}
// Initialize runner first (needed for both modes) // Initialize runner first (needed for both modes)
if err := s.initRunner(poolSize); err != nil { if err := s.initRunner(poolSize); err != nil {
@ -112,16 +116,8 @@ func newMoonshark(cfg *config.Config, debug, scriptMode bool) (*Moonshark, error
} }
s.setupWatchers() s.setupWatchers()
s.HTTPServer = http.New(s.LuaRouter, s.LuaRunner, cfg, debug) s.HTTPServer = http.New(s.LuaRouter, s.LuaRunner, cfg, debug)
// Set caching based on debug mode
if cfg.Server.Debug {
s.HTTPServer.SetStaticCaching(0)
} else {
s.HTTPServer.SetStaticCaching(time.Hour)
}
// Log static directory status // Log static directory status
if dirExists(cfg.Dirs.Static) { if dirExists(cfg.Dirs.Static) {
logger.Infof("Static files enabled: %s", color.Yellow(cfg.Dirs.Static)) logger.Infof("Static files enabled: %s", color.Yellow(cfg.Dirs.Static))
@ -152,12 +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( s.LuaRunner, err = runner.NewRunner(poolSize, s.Config.Dirs.Data, s.Config.Dirs.FS)
runner.WithPoolSize(poolSize),
runner.WithLibDirs(s.Config.Dirs.Libs...),
runner.WithFsDir(s.Config.Dirs.FS),
runner.WithDataDir(s.Config.Dirs.Data),
)
if err != nil { if err != nil {
return fmt.Errorf("lua runner init failed: %v", err) return fmt.Errorf("lua runner init failed: %v", err)
} }
@ -172,19 +163,10 @@ func (s *Moonshark) initRouter() error {
} }
var err error var err error
s.LuaRouter, err = router.NewLuaRouter(s.Config.Dirs.Routes) s.LuaRouter, err = router.New(s.Config.Dirs.Routes)
if err != nil { if err != nil {
if errors.Is(err, router.ErrRoutesCompilationErrors) {
logger.Warnf("Some routes failed to compile")
if failedRoutes := s.LuaRouter.ReportFailedRoutes(); len(failedRoutes) > 0 {
for _, re := range failedRoutes {
logger.Errorf("Route %s %s: %v", re.Method, re.Path, re.Err)
}
}
} else {
return fmt.Errorf("lua router init failed: %v", err) return fmt.Errorf("lua router init failed: %v", err)
} }
}
logger.Infof("LuaRouter is g2g! %s", color.Yellow(s.Config.Dirs.Routes)) logger.Infof("LuaRouter is g2g! %s", color.Yellow(s.Config.Dirs.Routes))
return nil return nil

View File

@ -1,90 +0,0 @@
package router
import (
"os"
"path/filepath"
"strings"
)
// buildRoutes scans the routes directory and builds the routing tree
func (r *LuaRouter) buildRoutes() error {
r.failedRoutes = make(map[string]*RouteError)
r.middlewareFiles = make(map[string][]string)
// First pass: collect all middleware files
err := filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") {
return err
}
if strings.TrimSuffix(info.Name(), ".lua") == "middleware" {
relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path))
if err != nil {
return err
}
fsPath := "/"
if relDir != "." {
fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/")
}
// Use filesystem path for middleware (includes groups)
r.middlewareFiles[fsPath] = append(r.middlewareFiles[fsPath], path)
}
return nil
})
if err != nil {
return err
}
// Second pass: build routes with combined middleware + handler
return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") {
return err
}
fileName := strings.TrimSuffix(info.Name(), ".lua")
// Skip middleware files (already processed)
if fileName == "middleware" {
return nil
}
relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path))
if err != nil {
return err
}
fsPath := "/"
if relDir != "." {
fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/")
}
pathInfo := parsePathWithGroups(fsPath)
// Handle index.lua files
if fileName == "index" {
for _, method := range []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} {
root := r.routes[method]
node := r.findOrCreateNode(root, pathInfo.urlPath)
node.indexFile = path
node.modTime = info.ModTime()
node.fsPath = pathInfo.fsPath
r.compileWithMiddleware(node, pathInfo.fsPath, path)
}
return nil
}
// Handle method files
method := strings.ToUpper(fileName)
root, exists := r.routes[method]
if !exists {
return nil
}
r.addRoute(root, pathInfo, path, info.ModTime())
return nil
})
}

View File

@ -1,62 +0,0 @@
package router
import (
"encoding/binary"
"hash/fnv"
"time"
"github.com/VictoriaMetrics/fastcache"
)
// hashString generates a hash for a string
func hashString(s string) uint64 {
h := fnv.New64a()
h.Write([]byte(s))
return h.Sum64()
}
// uint64ToBytes converts a uint64 to bytes for cache key
func uint64ToBytes(n uint64) []byte {
b := make([]byte, 8)
binary.LittleEndian.PutUint64(b, n)
return b
}
// getCacheKey generates a cache key for a method and path
func getCacheKey(method, path string) []byte {
key := hashString(method + ":" + path)
return uint64ToBytes(key)
}
// getBytecodeKey generates a cache key for a handler path
func getBytecodeKey(handlerPath string) []byte {
key := hashString(handlerPath)
return uint64ToBytes(key)
}
// ClearCache clears all caches
func (r *LuaRouter) ClearCache() {
r.routeCache.Reset()
r.bytecodeCache.Reset()
r.middlewareCache = make(map[string][]byte)
r.sourceCache = make(map[string][]byte)
r.sourceMtimes = make(map[string]time.Time)
}
// GetCacheStats returns statistics about the cache
func (r *LuaRouter) GetCacheStats() map[string]any {
var routeStats fastcache.Stats
var bytecodeStats fastcache.Stats
r.routeCache.UpdateStats(&routeStats)
r.bytecodeCache.UpdateStats(&bytecodeStats)
return map[string]any{
"routeEntries": routeStats.EntriesCount,
"routeBytes": routeStats.BytesSize,
"routeCollisions": routeStats.Collisions,
"bytecodeEntries": bytecodeStats.EntriesCount,
"bytecodeBytes": bytecodeStats.BytesSize,
"bytecodeCollisions": bytecodeStats.Collisions,
}
}

View File

@ -1,176 +0,0 @@
package router
import (
"os"
"strings"
"time"
)
// compileWithMiddleware combines middleware and handler source, then compiles
func (r *LuaRouter) compileWithMiddleware(n *node, fsPath, scriptPath string) error {
if scriptPath == "" {
return nil
}
// Check if we need to recompile by comparing modification times
sourceKey := r.getSourceCacheKey(fsPath, scriptPath)
needsRecompile := false
// Check handler modification time
handlerInfo, err := os.Stat(scriptPath)
if err != nil {
n.err = err
return err
}
lastCompiled, exists := r.sourceMtimes[sourceKey]
if !exists || handlerInfo.ModTime().After(lastCompiled) {
needsRecompile = true
}
// Check middleware modification times
if !needsRecompile {
middlewareChain := r.getMiddlewareChain(fsPath)
for _, mwPath := range middlewareChain {
mwInfo, err := os.Stat(mwPath)
if err != nil {
n.err = err
return err
}
if mwInfo.ModTime().After(lastCompiled) {
needsRecompile = true
break
}
}
}
// Use cached bytecode if available and fresh
if !needsRecompile {
if bytecode, exists := r.sourceCache[sourceKey]; exists {
bytecodeKey := getBytecodeKey(scriptPath)
r.bytecodeCache.Set(bytecodeKey, bytecode)
return nil
}
}
// Build combined source
combinedSource, err := r.buildCombinedSource(fsPath, scriptPath)
if err != nil {
n.err = err
return err
}
// Compile combined source using shared state
r.compileStateMu.Lock()
bytecode, err := r.compileState.CompileBytecode(combinedSource, scriptPath)
r.compileStateMu.Unlock()
if err != nil {
n.err = err
return err
}
// Cache everything
bytecodeKey := getBytecodeKey(scriptPath)
r.bytecodeCache.Set(bytecodeKey, bytecode)
r.sourceCache[sourceKey] = bytecode
r.sourceMtimes[sourceKey] = time.Now()
n.err = nil
return nil
}
// buildCombinedSource builds the combined middleware + handler source
func (r *LuaRouter) buildCombinedSource(fsPath, scriptPath string) (string, error) {
var combinedSource strings.Builder
// Get middleware chain using filesystem path
middlewareChain := r.getMiddlewareChain(fsPath)
// Add middleware in order
for _, mwPath := range middlewareChain {
content, err := r.getFileContent(mwPath)
if err != nil {
return "", err
}
combinedSource.WriteString("-- Middleware: ")
combinedSource.WriteString(mwPath)
combinedSource.WriteString("\n")
combinedSource.Write(content)
combinedSource.WriteString("\n")
}
// Add main handler
content, err := r.getFileContent(scriptPath)
if err != nil {
return "", err
}
combinedSource.WriteString("-- Handler: ")
combinedSource.WriteString(scriptPath)
combinedSource.WriteString("\n")
combinedSource.Write(content)
return combinedSource.String(), nil
}
// getFileContent reads file content with caching
func (r *LuaRouter) getFileContent(path string) ([]byte, error) {
// Check cache first
if content, exists := r.middlewareCache[path]; exists {
// Verify file hasn't changed
info, err := os.Stat(path)
if err == nil {
if cachedTime, exists := r.sourceMtimes[path]; exists && !info.ModTime().After(cachedTime) {
return content, nil
}
}
}
// Read from disk
content, err := os.ReadFile(path)
if err != nil {
return nil, err
}
// Cache it
r.middlewareCache[path] = content
r.sourceMtimes[path] = time.Now()
return content, nil
}
// getSourceCacheKey generates a unique key for combined source
func (r *LuaRouter) getSourceCacheKey(fsPath, scriptPath string) string {
middlewareChain := r.getMiddlewareChain(fsPath)
var keyParts []string
keyParts = append(keyParts, middlewareChain...)
keyParts = append(keyParts, scriptPath)
return strings.Join(keyParts, "|")
}
// getMiddlewareChain returns middleware files that apply to the given filesystem path
func (r *LuaRouter) getMiddlewareChain(fsPath string) []string {
var chain []string
// Collect middleware from root to specific path using filesystem path (includes groups)
pathParts := strings.Split(strings.Trim(fsPath, "/"), "/")
if pathParts[0] == "" {
pathParts = []string{}
}
// Add root middleware
if mw, exists := r.middlewareFiles["/"]; exists {
chain = append(chain, mw...)
}
// Add middleware from each path level (including groups)
currentPath := ""
for _, part := range pathParts {
currentPath += "/" + part
if mw, exists := r.middlewareFiles[currentPath]; exists {
chain = append(chain, mw...)
}
}
return chain
}

View File

@ -1,25 +0,0 @@
package router
import "errors"
var (
// ErrRoutesCompilationErrors indicates that some routes failed to compile
// but the router is still operational
ErrRoutesCompilationErrors = errors.New("some routes failed to compile")
)
// RouteError represents an error with a specific route
type RouteError struct {
Path string // The URL path
Method string // HTTP method
ScriptPath string // Path to the Lua script
Err error // The actual error
}
// Error returns the error message
func (re *RouteError) Error() string {
if re.Err == nil {
return "unknown route error"
}
return re.Err.Error()
}

View File

@ -1,187 +0,0 @@
package router
import (
"os"
"strings"
)
// Match finds a handler for the given method and path (URL path, excludes groups)
func (r *LuaRouter) Match(method, path string, params *Params) (*node, bool) {
params.Count = 0
r.mu.RLock()
root, exists := r.routes[method]
r.mu.RUnlock()
if !exists {
return nil, false
}
segments := strings.Split(strings.Trim(path, "/"), "/")
return r.matchPath(root, segments, params, 0)
}
// matchPath recursively matches a path against the routing tree
func (r *LuaRouter) matchPath(current *node, segments []string, params *Params, depth int) (*node, bool) {
// Filter empty segments
filteredSegments := segments[:0]
for _, segment := range segments {
if segment != "" {
filteredSegments = append(filteredSegments, segment)
}
}
segments = filteredSegments
if len(segments) == 0 {
if current.handler != "" {
return current, true
}
if current.indexFile != "" {
return current, true
}
return nil, false
}
segment := segments[0]
remaining := segments[1:]
// Try static child first
if child, exists := current.staticChild[segment]; exists {
if node, found := r.matchPath(child, remaining, params, depth+1); found {
return node, true
}
}
// Try parameter child
if current.paramChild != nil {
if params.Count < maxParams {
params.Keys[params.Count] = current.paramChild.paramName
params.Values[params.Count] = segment
params.Count++
}
if node, found := r.matchPath(current.paramChild, remaining, params, depth+1); found {
return node, true
}
params.Count--
}
// Fall back to index.lua
if current.indexFile != "" {
return current, true
}
return nil, false
}
// GetRouteInfo returns bytecode, script path, error, params, and found status
func (r *LuaRouter) GetRouteInfo(method, path []byte) ([]byte, string, error, *Params, bool) {
// Convert to string for internal processing
methodStr := string(method)
pathStr := string(path)
routeCacheKey := getCacheKey(methodStr, pathStr)
routeCacheData := r.routeCache.Get(nil, routeCacheKey)
// Fast path: found in cache
if len(routeCacheData) > 0 {
handlerPath := string(routeCacheData[8:])
bytecodeKey := routeCacheData[:8]
bytecode := r.bytecodeCache.Get(nil, bytecodeKey)
n, exists := r.nodeForHandler(handlerPath)
if !exists {
r.routeCache.Del(routeCacheKey)
return nil, "", nil, nil, false
}
// Check if recompilation needed
if len(bytecode) > 0 {
// For cached routes, we need to re-match to get params
params := &Params{}
r.Match(methodStr, pathStr, params)
return bytecode, handlerPath, n.err, params, true
}
// Recompile if needed
fileInfo, err := os.Stat(handlerPath)
if err != nil || fileInfo.ModTime().After(n.modTime) {
scriptPath := n.handler
if scriptPath == "" {
scriptPath = n.indexFile
}
fsPath := n.fsPath
if fsPath == "" {
fsPath = "/"
}
if err := r.compileWithMiddleware(n, fsPath, scriptPath); err != nil {
params := &Params{}
r.Match(methodStr, pathStr, params)
return nil, handlerPath, n.err, params, true
}
newBytecodeKey := getBytecodeKey(handlerPath)
bytecode = r.bytecodeCache.Get(nil, newBytecodeKey)
newCacheData := make([]byte, 8+len(handlerPath))
copy(newCacheData[:8], newBytecodeKey)
copy(newCacheData[8:], handlerPath)
r.routeCache.Set(routeCacheKey, newCacheData)
params := &Params{}
r.Match(methodStr, pathStr, params)
return bytecode, handlerPath, n.err, params, true
}
params := &Params{}
r.Match(methodStr, pathStr, params)
return bytecode, handlerPath, n.err, params, true
}
// Slow path: lookup and compile
params := &Params{}
node, found := r.Match(methodStr, pathStr, params)
if !found {
return nil, "", nil, nil, false
}
scriptPath := node.handler
if scriptPath == "" && node.indexFile != "" {
scriptPath = node.indexFile
}
if scriptPath == "" {
return nil, "", nil, nil, false
}
bytecodeKey := getBytecodeKey(scriptPath)
bytecode := r.bytecodeCache.Get(nil, bytecodeKey)
if len(bytecode) == 0 {
fsPath := node.fsPath
if fsPath == "" {
fsPath = "/"
}
if err := r.compileWithMiddleware(node, fsPath, scriptPath); err != nil {
return nil, scriptPath, node.err, params, true
}
bytecode = r.bytecodeCache.Get(nil, bytecodeKey)
}
// Cache the route
cacheData := make([]byte, 8+len(scriptPath))
copy(cacheData[:8], bytecodeKey)
copy(cacheData[8:], scriptPath)
r.routeCache.Set(routeCacheKey, cacheData)
return bytecode, scriptPath, node.err, params, true
}
// GetRouteInfoString is a convenience method that accepts strings
func (r *LuaRouter) GetRouteInfoString(method, path string) ([]byte, string, error, *Params, bool) {
return r.GetRouteInfo([]byte(method), []byte(path))
}

View File

@ -1,190 +0,0 @@
package router
import (
"path/filepath"
"strings"
"time"
)
// node represents a node in the radix trie
type node struct {
// Static children mapped by path segment
staticChild map[string]*node
// Parameter child for dynamic segments (e.g., :id)
paramChild *node
paramName string
// Handler information
handler string // Path to the handler file
indexFile string // Path to index.lua if exists
modTime time.Time // Modification time of the handler
fsPath string // Filesystem path (includes groups)
// Compilation error if any
err error
}
// pathInfo holds both URL path and filesystem path
type pathInfo struct {
urlPath string // URL path without groups (e.g., /users)
fsPath string // Filesystem path with groups (e.g., /(admin)/users)
}
// parsePathWithGroups parses a filesystem path, extracting groups
func parsePathWithGroups(fsPath string) *pathInfo {
segments := strings.Split(strings.Trim(fsPath, "/"), "/")
var urlSegments []string
for _, segment := range segments {
if segment == "" {
continue
}
// Skip group segments (enclosed in parentheses)
if strings.HasPrefix(segment, "(") && strings.HasSuffix(segment, ")") {
continue
}
urlSegments = append(urlSegments, segment)
}
urlPath := "/"
if len(urlSegments) > 0 {
urlPath = "/" + strings.Join(urlSegments, "/")
}
return &pathInfo{
urlPath: urlPath,
fsPath: fsPath,
}
}
// findOrCreateNode finds or creates a node at the given URL path
func (r *LuaRouter) findOrCreateNode(root *node, urlPath string) *node {
segments := strings.Split(strings.Trim(urlPath, "/"), "/")
if len(segments) == 1 && segments[0] == "" {
return root
}
current := root
for _, segment := range segments {
if segment == "" {
continue
}
// Check if it's a parameter
if strings.HasPrefix(segment, ":") {
paramName := segment[1:]
if current.paramChild == nil {
current.paramChild = &node{
staticChild: make(map[string]*node),
paramName: paramName,
}
}
current = current.paramChild
} else {
// Static segment
if _, exists := current.staticChild[segment]; !exists {
current.staticChild[segment] = &node{
staticChild: make(map[string]*node),
}
}
current = current.staticChild[segment]
}
}
return current
}
// addRoute adds a route to the tree
func (r *LuaRouter) addRoute(root *node, pathInfo *pathInfo, handlerPath string, modTime time.Time) {
node := r.findOrCreateNode(root, pathInfo.urlPath)
node.handler = handlerPath
node.modTime = modTime
node.fsPath = pathInfo.fsPath
// Compile the route with middleware
r.compileWithMiddleware(node, pathInfo.fsPath, handlerPath)
// Track failed routes
if node.err != nil {
key := filepath.Base(handlerPath) + ":" + pathInfo.urlPath
r.failedRoutes[key] = &RouteError{
Path: pathInfo.urlPath,
Method: strings.ToUpper(strings.TrimSuffix(filepath.Base(handlerPath), ".lua")),
ScriptPath: handlerPath,
Err: node.err,
}
}
}
// nodeForHandler finds a node by its handler path
func (r *LuaRouter) nodeForHandler(handlerPath string) (*node, bool) {
r.mu.RLock()
defer r.mu.RUnlock()
for _, root := range r.routes {
if node := findNodeByHandler(root, handlerPath); node != nil {
return node, true
}
}
return nil, false
}
// findNodeByHandler recursively searches for a node with the given handler path
func findNodeByHandler(n *node, handlerPath string) *node {
if n.handler == handlerPath || n.indexFile == handlerPath {
return n
}
// Search static children
for _, child := range n.staticChild {
if found := findNodeByHandler(child, handlerPath); found != nil {
return found
}
}
// Search param child
if n.paramChild != nil {
if found := findNodeByHandler(n.paramChild, handlerPath); found != nil {
return found
}
}
return nil
}
// countNodesAndBytecode counts nodes and bytecode size in the tree
func countNodesAndBytecode(n *node) (int, int64) {
if n == nil {
return 0, 0
}
count := 0
bytes := int64(0)
// Count this node if it has a handler
if n.handler != "" || n.indexFile != "" {
count = 1
// Estimate bytecode size (would need actual bytecode cache lookup for accuracy)
bytes = 1024 // Placeholder
}
// Count static children
for _, child := range n.staticChild {
c, b := countNodesAndBytecode(child)
count += c
bytes += b
}
// Count param child
if n.paramChild != nil {
c, b := countNodesAndBytecode(n.paramChild)
count += c
bytes += b
}
return count, bytes
}

View File

@ -1,44 +0,0 @@
package router
// Maximum number of URL parameters per route
const maxParams = 20
// Params holds URL parameters with fixed-size arrays to avoid allocations
type Params struct {
Keys [maxParams]string
Values [maxParams]string
Count int
}
// Get returns a parameter value by name
func (p *Params) Get(name string) string {
for i := range p.Count {
if p.Keys[i] == name {
return p.Values[i]
}
}
return ""
}
// Reset clears all parameters
func (p *Params) Reset() {
p.Count = 0
}
// Set adds or updates a parameter
func (p *Params) Set(name, value string) {
// Try to update existing
for i := range p.Count {
if p.Keys[i] == name {
p.Values[i] = value
return
}
}
// Add new if space available
if p.Count < maxParams {
p.Keys[p.Count] = name
p.Values[p.Count] = value
p.Count++
}
}

View File

@ -3,44 +3,54 @@ package router
import ( import (
"errors" "errors"
"os" "os"
"path/filepath"
"strings"
"sync" "sync"
"time"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go" luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"github.com/VictoriaMetrics/fastcache" "github.com/VictoriaMetrics/fastcache"
) )
// Default cache sizes // node represents a node in the radix trie
const ( type node struct {
defaultBytecodeMaxBytes = 32 * 1024 * 1024 // 32MB for bytecode cache segment string
defaultRouteMaxBytes = 8 * 1024 * 1024 // 8MB for route match cache bytecode []byte
) scriptPath string
children []*node
// LuaRouter is a filesystem-based HTTP router for Lua files isDynamic bool
type LuaRouter struct { isWildcard bool
routesDir string // Root directory containing route files maxParams uint8
routes map[string]*node // Method -> route tree
failedRoutes map[string]*RouteError // Track failed routes
mu sync.RWMutex // Lock for concurrent access to routes
routeCache *fastcache.Cache // Cache for route lookups
bytecodeCache *fastcache.Cache // Cache for compiled bytecode
// Middleware tracking for path hierarchy
middlewareFiles map[string][]string // filesystem path -> middleware file paths
// Caching fields
middlewareCache map[string][]byte // path -> content
sourceCache map[string][]byte // combined source cache key -> compiled bytecode
sourceMtimes map[string]time.Time // track modification times
// Shared Lua state for compilation
compileState *luajit.State
compileStateMu sync.Mutex // Protect concurrent access to Lua state
} }
// NewLuaRouter creates a new LuaRouter instance // Router is a filesystem-based HTTP router for Lua files with bytecode caching
func NewLuaRouter(routesDir string) (*LuaRouter, error) { type Router struct {
routesDir string
get, post, put, patch, delete *node
bytecodeCache *fastcache.Cache
compileState *luajit.State
compileMu sync.Mutex
paramsBuffer []string
middlewareFiles map[string][]string // filesystem path -> middleware file paths
}
// Params holds URL parameters
type Params struct {
Keys []string
Values []string
}
// Get returns a parameter value by name
func (p *Params) Get(name string) string {
for i, key := range p.Keys {
if key == name && i < len(p.Values) {
return p.Values[i]
}
}
return ""
}
// New creates a new Router instance
func New(routesDir string) (*Router, error) {
info, err := os.Stat(routesDir) info, err := os.Stat(routesDir)
if err != nil { if err != nil {
return nil, err return nil, err
@ -49,108 +59,443 @@ func NewLuaRouter(routesDir string) (*LuaRouter, error) {
return nil, errors.New("routes path is not a directory") return nil, errors.New("routes path is not a directory")
} }
// Create shared Lua state
compileState := luajit.New() compileState := luajit.New()
if compileState == nil { if compileState == nil {
return nil, errors.New("failed to create Lua compile state") return nil, errors.New("failed to create Lua compile state")
} }
r := &LuaRouter{ r := &Router{
routesDir: routesDir, routesDir: routesDir,
routes: make(map[string]*node), get: &node{},
failedRoutes: make(map[string]*RouteError), post: &node{},
middlewareFiles: make(map[string][]string), put: &node{},
routeCache: fastcache.New(defaultRouteMaxBytes), patch: &node{},
bytecodeCache: fastcache.New(defaultBytecodeMaxBytes), delete: &node{},
middlewareCache: make(map[string][]byte), bytecodeCache: fastcache.New(32 * 1024 * 1024), // 32MB
sourceCache: make(map[string][]byte),
sourceMtimes: make(map[string]time.Time),
compileState: compileState, compileState: compileState,
paramsBuffer: make([]string, 64),
middlewareFiles: make(map[string][]string),
} }
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} return r, r.buildRoutes()
for _, method := range methods { }
r.routes[method] = &node{
staticChild: make(map[string]*node), // methodNode returns the root node for a method
func (r *Router) methodNode(method string) *node {
switch method {
case "GET":
return r.get
case "POST":
return r.post
case "PUT":
return r.put
case "PATCH":
return r.patch
case "DELETE":
return r.delete
default:
return nil
} }
} }
err = r.buildRoutes() // buildRoutes scans the routes directory and builds the routing tree
func (r *Router) buildRoutes() error {
if len(r.failedRoutes) > 0 {
return r, ErrRoutesCompilationErrors
}
return r, err
}
// Refresh rebuilds the router by rescanning the routes directory
func (r *LuaRouter) Refresh() error {
r.mu.Lock()
defer r.mu.Unlock()
for method := range r.routes {
r.routes[method] = &node{
staticChild: make(map[string]*node),
}
}
r.failedRoutes = make(map[string]*RouteError)
r.middlewareFiles = make(map[string][]string) r.middlewareFiles = make(map[string][]string)
r.middlewareCache = make(map[string][]byte)
r.sourceCache = make(map[string][]byte)
r.sourceMtimes = make(map[string]time.Time)
err := r.buildRoutes()
if len(r.failedRoutes) > 0 {
return ErrRoutesCompilationErrors
}
// First pass: collect all middleware files
err := filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") {
return err return err
} }
// ReportFailedRoutes returns a list of routes that failed to compile if strings.TrimSuffix(info.Name(), ".lua") == "middleware" {
func (r *LuaRouter) ReportFailedRoutes() []*RouteError { relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path))
r.mu.RLock() if err != nil {
defer r.mu.RUnlock() return err
result := make([]*RouteError, 0, len(r.failedRoutes))
for _, re := range r.failedRoutes {
result = append(result, re)
} }
return result fsPath := "/"
if relDir != "." {
fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/")
} }
// Close cleans up the router and its resources r.middlewareFiles[fsPath] = append(r.middlewareFiles[fsPath], path)
func (r *LuaRouter) Close() { }
r.compileStateMu.Lock()
return nil
})
if err != nil {
return err
}
// Second pass: build routes
return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error {
if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") {
return err
}
fileName := strings.TrimSuffix(info.Name(), ".lua")
// Skip middleware files
if fileName == "middleware" {
return nil
}
// Get relative path from routes directory
relPath, err := filepath.Rel(r.routesDir, path)
if err != nil {
return err
}
// Get filesystem path (includes groups)
fsPath := "/" + strings.ReplaceAll(filepath.Dir(relPath), "\\", "/")
if fsPath == "/." {
fsPath = "/"
}
// Get URL path (excludes groups)
urlPath := r.parseURLPath(fsPath)
// Handle method files (get.lua, post.lua, etc.)
method := strings.ToUpper(fileName)
root := r.methodNode(method)
if root != nil {
return r.addRoute(root, urlPath, fsPath, path)
}
// Handle index files - register for all methods
if fileName == "index" {
for _, method := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} {
if root := r.methodNode(method); root != nil {
if err := r.addRoute(root, urlPath, fsPath, path); err != nil {
return err
}
}
}
return nil
}
// Handle named route files - register as GET by default
namedPath := urlPath
if urlPath == "/" {
namedPath = "/" + fileName
} else {
namedPath = urlPath + "/" + fileName
}
return r.addRoute(r.get, namedPath, fsPath, path)
})
}
// parseURLPath strips group segments from filesystem path
func (r *Router) parseURLPath(fsPath string) string {
segments := strings.Split(strings.Trim(fsPath, "/"), "/")
var urlSegments []string
for _, segment := range segments {
if segment == "" {
continue
}
// Skip group segments (enclosed in parentheses)
if strings.HasPrefix(segment, "(") && strings.HasSuffix(segment, ")") {
continue
}
urlSegments = append(urlSegments, segment)
}
if len(urlSegments) == 0 {
return "/"
}
return "/" + strings.Join(urlSegments, "/")
}
// getMiddlewareChain returns middleware files that apply to the given filesystem path
func (r *Router) getMiddlewareChain(fsPath string) []string {
var chain []string
pathParts := strings.Split(strings.Trim(fsPath, "/"), "/")
if pathParts[0] == "" {
pathParts = []string{}
}
// Add root middleware
if mw, exists := r.middlewareFiles["/"]; exists {
chain = append(chain, mw...)
}
// Add middleware from each path level (including groups)
currentPath := ""
for _, part := range pathParts {
currentPath += "/" + part
if mw, exists := r.middlewareFiles[currentPath]; exists {
chain = append(chain, mw...)
}
}
return chain
}
// buildCombinedSource combines middleware and handler source
func (r *Router) buildCombinedSource(fsPath, scriptPath string) (string, error) {
var combined strings.Builder
// Add middleware in order
middlewareChain := r.getMiddlewareChain(fsPath)
for _, mwPath := range middlewareChain {
content, err := os.ReadFile(mwPath)
if err != nil {
return "", err
}
combined.WriteString("-- Middleware: ")
combined.WriteString(mwPath)
combined.WriteString("\n")
combined.Write(content)
combined.WriteString("\n")
}
// Add main handler
content, err := os.ReadFile(scriptPath)
if err != nil {
return "", err
}
combined.WriteString("-- Handler: ")
combined.WriteString(scriptPath)
combined.WriteString("\n")
combined.Write(content)
return combined.String(), nil
}
// addRoute adds a new route to the trie with bytecode compilation
func (r *Router) addRoute(root *node, urlPath, fsPath, scriptPath string) error {
// Build combined source with middleware
combinedSource, err := r.buildCombinedSource(fsPath, scriptPath)
if err != nil {
return err
}
// Compile bytecode
r.compileMu.Lock()
bytecode, err := r.compileState.CompileBytecode(combinedSource, scriptPath)
r.compileMu.Unlock()
if err != nil {
return err
}
// Cache bytecode
cacheKey := hashString(scriptPath)
r.bytecodeCache.Set(uint64ToBytes(cacheKey), bytecode)
if urlPath == "/" {
root.bytecode = bytecode
root.scriptPath = scriptPath
return nil
}
current := root
pos := 0
paramCount := uint8(0)
for {
seg, newPos, more := readSegment(urlPath, pos)
if seg == "" {
break
}
isDyn := len(seg) > 2 && seg[0] == '[' && seg[len(seg)-1] == ']'
isWC := len(seg) > 0 && seg[0] == '*'
if isWC && more {
return errors.New("wildcard must be the last segment")
}
if isDyn || isWC {
paramCount++
}
// Find or create child
var child *node
for _, c := range current.children {
if c.segment == seg {
child = c
break
}
}
if child == nil {
child = &node{
segment: seg,
isDynamic: isDyn,
isWildcard: isWC,
}
current.children = append(current.children, child)
}
if child.maxParams < paramCount {
child.maxParams = paramCount
}
current = child
pos = newPos
}
current.bytecode = bytecode
current.scriptPath = scriptPath
return nil
}
// readSegment extracts the next path segment
func readSegment(path string, start int) (segment string, end int, hasMore bool) {
if start >= len(path) {
return "", start, false
}
if path[start] == '/' {
start++
}
if start >= len(path) {
return "", start, false
}
end = start
for end < len(path) && path[end] != '/' {
end++
}
return path[start:end], end, end < len(path)
}
// Lookup finds bytecode and parameters for a method and path
func (r *Router) Lookup(method, path string) ([]byte, *Params, bool) {
root := r.methodNode(method)
if root == nil {
return nil, nil, false
}
if path == "/" {
if root.bytecode != nil {
return root.bytecode, &Params{}, true
}
return nil, nil, false
}
// Prepare params buffer
buffer := r.paramsBuffer
if cap(buffer) < int(root.maxParams) {
buffer = make([]string, root.maxParams)
r.paramsBuffer = buffer
}
buffer = buffer[:0]
var keys []string
bytecode, paramCount, found := r.match(root, path, 0, &buffer, &keys)
if !found {
return nil, nil, false
}
params := &Params{
Keys: keys[:paramCount],
Values: buffer[:paramCount],
}
return bytecode, params, true
}
// match traverses the trie to find bytecode
func (r *Router) match(current *node, path string, start int, params *[]string, keys *[]string) ([]byte, int, bool) {
paramCount := 0
// Check wildcard first
for _, c := range current.children {
if c.isWildcard {
rem := path[start:]
if len(rem) > 0 && rem[0] == '/' {
rem = rem[1:]
}
*params = append(*params, rem)
*keys = append(*keys, strings.TrimPrefix(c.segment, "*"))
return c.bytecode, 1, c.bytecode != nil
}
}
seg, pos, more := readSegment(path, start)
if seg == "" {
return current.bytecode, 0, current.bytecode != nil
}
for _, c := range current.children {
if c.segment == seg || c.isDynamic {
if c.isDynamic {
*params = append(*params, seg)
paramName := c.segment[1 : len(c.segment)-1] // Remove [ ]
*keys = append(*keys, paramName)
paramCount++
}
if !more {
return c.bytecode, paramCount, c.bytecode != nil
}
bytecode, nestedCount, ok := r.match(c, path, pos, params, keys)
if ok {
return bytecode, paramCount + nestedCount, true
}
// Backtrack on failure
if c.isDynamic {
*params = (*params)[:len(*params)-1]
*keys = (*keys)[:len(*keys)-1]
}
}
}
return nil, 0, false
}
// GetBytecode gets cached bytecode by script path
func (r *Router) GetBytecode(scriptPath string) []byte {
cacheKey := hashString(scriptPath)
return r.bytecodeCache.Get(nil, uint64ToBytes(cacheKey))
}
// Refresh rebuilds the router
func (r *Router) Refresh() error {
r.get = &node{}
r.post = &node{}
r.put = &node{}
r.patch = &node{}
r.delete = &node{}
r.middlewareFiles = make(map[string][]string)
r.bytecodeCache.Reset()
return r.buildRoutes()
}
// Close cleans up resources
func (r *Router) Close() {
r.compileMu.Lock()
if r.compileState != nil { if r.compileState != nil {
r.compileState.Close() r.compileState.Close()
r.compileState = nil r.compileState = nil
} }
r.compileStateMu.Unlock() r.compileMu.Unlock()
} }
// GetRouteStats returns statistics about the router // Helper functions from cache.go
func (r *LuaRouter) GetRouteStats() (int, int64) { func hashString(s string) uint64 {
r.mu.RLock() h := uint64(5381)
defer r.mu.RUnlock() for i := 0; i < len(s); i++ {
h = ((h << 5) + h) + uint64(s[i])
routeCount := 0 }
bytecodeBytes := int64(0) return h
for _, root := range r.routes {
count, bytes := countNodesAndBytecode(root)
routeCount += count
bytecodeBytes += bytes
} }
return routeCount, bytecodeBytes func uint64ToBytes(n uint64) []byte {
} b := make([]byte, 8)
b[0] = byte(n)
type NodeWithError struct { b[1] = byte(n >> 8)
ScriptPath string b[2] = byte(n >> 16)
Error error b[3] = byte(n >> 24)
b[4] = byte(n >> 32)
b[5] = byte(n >> 40)
b[6] = byte(n >> 48)
b[7] = byte(n >> 56)
return b
} }

View File

@ -1,237 +1,106 @@
// runner.go - Simplified interface
package runner package runner
import ( import (
"Moonshark/runner/lualibs"
"Moonshark/utils/color"
"Moonshark/utils/logger"
"context"
"errors" "errors"
"fmt" "fmt"
"os" "os"
"path/filepath" "path/filepath"
"runtime" "runtime"
"strconv"
"strings" "strings"
"sync" "sync"
"sync/atomic" "sync/atomic"
"time" "time"
"Moonshark/router"
"Moonshark/runner/lualibs"
"Moonshark/sessions"
"Moonshark/utils/logger"
luajit "git.sharkk.net/Sky/LuaJIT-to-Go" luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
"github.com/goccy/go-json" "github.com/goccy/go-json"
"github.com/valyala/bytebufferpool" "github.com/valyala/bytebufferpool"
"github.com/valyala/fasthttp" "github.com/valyala/fasthttp"
) )
// Common errors var emptyMap = make(map[string]any)
var ( var (
ErrRunnerClosed = errors.New("lua runner is closed") 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") ErrTimeout = errors.New("operation timed out")
ErrStateNotReady = errors.New("lua state not ready")
) )
// RunnerOption defines a functional option for configuring the Runner
type RunnerOption func(*Runner)
// State wraps a Lua state with its sandbox
type State struct { type State struct {
L *luajit.State // The Lua state L *luajit.State
sandbox *Sandbox // Associated sandbox sandbox *Sandbox
index int // Index for debugging index int
inUse atomic.Bool // Whether the state is currently in use inUse atomic.Bool
} }
// Runner runs Lua scripts using a pool of Lua states
type Runner struct { type Runner struct {
states []*State // All states managed by this runner states []*State
statePool chan int // Pool of available state indexes statePool chan int
poolSize int // Size of the state pool poolSize int
moduleLoader *ModuleLoader // Module loader moduleLoader *ModuleLoader
dataDir string // Data directory for SQLite databases isRunning atomic.Bool
fsDir string // Virtual filesystem directory mu sync.RWMutex
isRunning atomic.Bool // Whether the runner is active scriptDir string
mu sync.RWMutex // Mutex for thread safety
scriptDir string // Current script directory // Pre-allocated pools for HTTP processing
ctxPool sync.Pool
paramsPool sync.Pool
} }
// WithPoolSize sets the state pool size func NewRunner(poolSize int, dataDir, fsDir string) (*Runner, error) {
func WithPoolSize(size int) RunnerOption { if poolSize <= 0 {
return func(r *Runner) { poolSize = runtime.GOMAXPROCS(0)
if size > 0 {
r.poolSize = size
}
}
} }
// WithLibDirs sets additional library directories r := &Runner{
func WithLibDirs(dirs ...string) RunnerOption { poolSize: poolSize,
return func(r *Runner) { moduleLoader: NewModuleLoader(&ModuleConfig{}),
if r.moduleLoader == nil { ctxPool: sync.Pool{
r.moduleLoader = NewModuleLoader(&ModuleConfig{ New: func() any { return make(map[string]any, 8) },
LibDirs: dirs, },
}) paramsPool: sync.Pool{
} else { New: func() any { return make(map[string]any, 4) },
r.moduleLoader.config.LibDirs = dirs },
}
}
} }
// WithDataDir sets the data directory for SQLite databases lualibs.InitSQLite(dataDir)
func WithDataDir(dataDir string) RunnerOption { lualibs.InitFS(fsDir)
return func(r *Runner) { lualibs.SetSQLitePoolSize(poolSize)
if dataDir != "" {
r.dataDir = dataDir
}
}
}
// WithFsDir sets the virtual filesystem directory r.states = make([]*State, poolSize)
func WithFsDir(fsDir string) RunnerOption { r.statePool = make(chan int, poolSize)
return func(r *Runner) {
if fsDir != "" {
r.fsDir = fsDir
}
}
}
// NewRunner creates a new Runner with a pool of states if err := r.initStates(); err != nil {
func NewRunner(options ...RunnerOption) (*Runner, error) {
// Default configuration
runner := &Runner{
poolSize: runtime.GOMAXPROCS(0),
dataDir: "data",
fsDir: "fs",
}
// 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)
}
lualibs.InitSQLite(runner.dataDir)
lualibs.InitFS(runner.fsDir)
lualibs.SetSQLitePoolSize(runner.poolSize)
// 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 {
lualibs.CleanupSQLite() lualibs.CleanupSQLite()
runner.Close()
return nil, err return nil, err
} }
runner.isRunning.Store(true) r.isRunning.Store(true)
return runner, nil return r, nil
} }
// initializeStates creates and initializes all states in the pool // Single entry point for HTTP execution
func (r *Runner) initializeStates() error { func (r *Runner) ExecuteHTTP(bytecode []byte, httpCtx *fasthttp.RequestCtx,
logger.Infof("[LuaRunner] Creating %s states...", color.Yellow(strconv.Itoa(r.poolSize))) params *router.Params, session *sessions.Session) (*Response, error) {
for i := range r.poolSize {
state, err := r.createState(i)
if err != nil {
return err
}
r.states[i] = state
r.statePool <- i // Add index to the pool
}
return nil
}
// createState initializes a new Lua state
func (r *Runner) createState(index int) (*State, error) {
verbose := index == 0
if verbose {
logger.Debugf("Creating Lua state %d", index)
}
L := luajit.New(true) // Explicitly open standard libraries
if L == nil {
return nil, errors.New("failed to create Lua state")
}
sb := NewSandbox()
// Set up sandbox
if err := sb.Setup(L, verbose); err != nil {
L.Cleanup()
L.Close()
return nil, ErrInitFailed
}
// Set up module loader
if err := r.moduleLoader.SetupRequire(L); err != nil {
L.Cleanup()
L.Close()
return nil, ErrInitFailed
}
// Preload modules
if err := r.moduleLoader.PreloadModules(L); err != nil {
L.Cleanup()
L.Close()
return nil, errors.New("failed to preload modules")
}
if verbose {
logger.Debugf("Lua state %d initialized successfully", index)
}
return &State{
L: L,
sandbox: sb,
index: index,
}, nil
}
// Execute runs a script in a sandbox with context
func (r *Runner) Execute(ctx context.Context, bytecode []byte, execCtx *Context, scriptPath string) (*Response, error) {
if !r.isRunning.Load() { if !r.isRunning.Load() {
return nil, ErrRunnerClosed return nil, ErrRunnerClosed
} }
// Set script directory if provided // Get state with timeout
if scriptPath != "" {
r.mu.Lock()
r.scriptDir = filepath.Dir(scriptPath)
r.moduleLoader.SetScriptDir(r.scriptDir)
r.mu.Unlock()
}
// Get a state from the pool
var stateIndex int var stateIndex int
select { select {
case stateIndex = <-r.statePool: case stateIndex = <-r.statePool:
// Got a state case <-time.After(time.Second):
case <-ctx.Done():
return nil, ctx.Err()
case <-time.After(1 * time.Second):
return nil, ErrTimeout return nil, ErrTimeout
} }
state := r.states[stateIndex] state := r.states[stateIndex]
if state == nil {
r.statePool <- stateIndex
return nil, ErrStateNotReady
}
// Use atomic operations
state.inUse.Store(true) state.inUse.Store(true)
defer func() { defer func() {
@ -240,26 +109,148 @@ func (r *Runner) Execute(ctx context.Context, bytecode []byte, execCtx *Context,
select { select {
case r.statePool <- stateIndex: case r.statePool <- stateIndex:
default: default:
// Pool is full or closed, state will be cleaned up by Close()
} }
} }
}() }()
// Execute in sandbox // Build Lua context directly from HTTP request
response, err := state.sandbox.Execute(state.L, bytecode, execCtx) luaCtx := r.buildHTTPContext(httpCtx, params, session)
defer r.releaseHTTPContext(luaCtx)
return state.sandbox.Execute(state.L, bytecode, luaCtx)
}
// Build Lua context from HTTP request
func (r *Runner) buildHTTPContext(ctx *fasthttp.RequestCtx, params *router.Params, session *sessions.Session) *Context {
luaCtx := NewContext()
// Basic request info
luaCtx.Set("method", string(ctx.Method()))
luaCtx.Set("path", string(ctx.Path()))
luaCtx.Set("host", string(ctx.Host()))
// Headers
headers := r.ctxPool.Get().(map[string]any)
ctx.Request.Header.VisitAll(func(key, value []byte) {
headers[string(key)] = string(value)
})
luaCtx.Set("headers", headers)
// Route parameters
if params != nil && len(params.Keys) > 0 {
paramMap := r.paramsPool.Get().(map[string]any)
for i, key := range params.Keys {
if i < len(params.Values) {
paramMap[key] = params.Values[i]
}
}
luaCtx.Set("params", paramMap)
} else {
luaCtx.Set("params", emptyMap)
}
// Form data for POST/PUT/PATCH
method := ctx.Method()
if string(method) == "POST" || string(method) == "PUT" || string(method) == "PATCH" {
if formData := parseForm(ctx); formData != nil {
luaCtx.Set("form", formData)
} else {
luaCtx.Set("form", emptyMap)
}
} else {
luaCtx.Set("form", emptyMap)
}
// Session data
sessionMap := r.ctxPool.Get().(map[string]any)
session.AdvanceFlash()
sessionMap["id"] = session.ID
if !session.IsEmpty() {
sessionMap["data"] = session.GetAll()
sessionMap["flash"] = session.GetAllFlash()
} else {
sessionMap["data"] = emptyMap
sessionMap["flash"] = emptyMap
}
luaCtx.Set("session", sessionMap)
// Environment variables
if envMgr := lualibs.GetGlobalEnvManager(); envMgr != nil {
luaCtx.Set("env", envMgr.GetAll())
}
return luaCtx
}
func (r *Runner) releaseHTTPContext(luaCtx *Context) {
// Return pooled maps
if headers, ok := luaCtx.Get("headers").(map[string]any); ok {
for k := range headers {
delete(headers, k)
}
r.ctxPool.Put(headers)
}
if params, ok := luaCtx.Get("params").(map[string]any); ok && len(params) > 0 {
for k := range params {
delete(params, k)
}
r.paramsPool.Put(params)
}
if sessionMap, ok := luaCtx.Get("session").(map[string]any); ok {
for k := range sessionMap {
delete(sessionMap, k)
}
r.ctxPool.Put(sessionMap)
}
luaCtx.Release()
}
func (r *Runner) initStates() error {
logger.Infof("[LuaRunner] Creating %d states...", r.poolSize)
for i := range r.poolSize {
state, err := r.createState(i)
if err != nil { if err != nil {
return err
}
r.states[i] = state
r.statePool <- i
}
return nil
}
func (r *Runner) createState(index int) (*State, error) {
L := luajit.New(true)
if L == nil {
return nil, errors.New("failed to create Lua state")
}
sb := NewSandbox()
if err := sb.Setup(L, index == 0); err != nil {
L.Cleanup()
L.Close()
return nil, err return nil, err
} }
return response, nil if err := r.moduleLoader.SetupRequire(L); err != nil {
L.Cleanup()
L.Close()
return nil, err
} }
// Run executes a Lua script with immediate context if err := r.moduleLoader.PreloadModules(L); err != nil {
func (r *Runner) Run(bytecode []byte, execCtx *Context, scriptPath string) (*Response, error) { L.Cleanup()
return r.Execute(context.Background(), bytecode, execCtx, scriptPath) L.Close()
return nil, err
}
return &State{L: L, sandbox: sb, index: index}, nil
} }
// Close gracefully shuts down the Runner
func (r *Runner) Close() error { func (r *Runner) Close() error {
r.mu.Lock() r.mu.Lock()
defer r.mu.Unlock() defer r.mu.Unlock()
@ -267,22 +258,21 @@ func (r *Runner) Close() error {
if !r.isRunning.Load() { if !r.isRunning.Load() {
return ErrRunnerClosed return ErrRunnerClosed
} }
r.isRunning.Store(false) r.isRunning.Store(false)
// Drain all states from the pool // Drain pool
for { for {
select { select {
case <-r.statePool: case <-r.statePool:
default: default:
goto waitForInUse goto cleanup
} }
} }
waitForInUse: cleanup:
// Wait for in-use states to finish (with timeout) // Wait for states to finish
timeout := time.Now().Add(10 * time.Second) timeout := time.Now().Add(10 * time.Second)
for { for time.Now().Before(timeout) {
allIdle := true allIdle := true
for _, state := range r.states { for _, state := range r.states {
if state != nil && state.inUse.Load() { if state != nil && state.inUse.Load() {
@ -290,25 +280,15 @@ waitForInUse:
break break
} }
} }
if allIdle { if allIdle {
break break
} }
if time.Now().After(timeout) {
logger.Warnf("Timeout waiting for states to finish during shutdown, forcing close")
break
}
time.Sleep(10 * time.Millisecond) time.Sleep(10 * time.Millisecond)
} }
// Now safely close all states // Close states
for i, state := range r.states { for i, state := range r.states {
if state != nil { if state != nil {
if state.inUse.Load() {
logger.Warnf("Force closing state %d that is still in use", i)
}
state.L.Cleanup() state.L.Cleanup()
state.L.Close() state.L.Close()
r.states[i] = nil r.states[i] = nil
@ -317,75 +297,34 @@ waitForInUse:
lualibs.CleanupFS() lualibs.CleanupFS()
lualibs.CleanupSQLite() lualibs.CleanupSQLite()
logger.Debugf("Runner closed")
return nil return nil
} }
// RefreshStates rebuilds all states in the pool // parseForm extracts form data from HTTP request
func (r *Runner) RefreshStates() error { func parseForm(ctx *fasthttp.RequestCtx) map[string]any {
r.mu.Lock() form := make(map[string]any)
defer r.mu.Unlock()
if !r.isRunning.Load() { // Parse POST form data
return ErrRunnerClosed ctx.PostArgs().VisitAll(func(key, value []byte) {
form[string(key)] = string(value)
})
// Parse multipart form if present
if multipartForm, err := ctx.MultipartForm(); err == nil {
for key, values := range multipartForm.Value {
if len(values) == 1 {
form[key] = values[0]
} else {
form[key] = values
} }
logger.Infof("Runner is refreshing all states...")
// Drain all states from the pool
for {
select {
case <-r.statePool:
default:
goto waitForInUse
} }
} }
waitForInUse: if len(form) == 0 {
// Wait for in-use states to finish (with timeout)
timeout := time.Now().Add(10 * time.Second)
for {
allIdle := true
for _, state := range r.states {
if state != nil && state.inUse.Load() {
allIdle = false
break
}
}
if allIdle {
break
}
if time.Now().After(timeout) {
logger.Warnf("Timeout waiting for states to finish, forcing refresh")
break
}
time.Sleep(10 * time.Millisecond)
}
// Now safely destroy all states
for i, state := range r.states {
if state != nil {
if state.inUse.Load() {
logger.Warnf("Force closing state %d that is still in use", i)
}
state.L.Cleanup()
state.L.Close()
r.states[i] = nil
}
}
// Reinitialize all states
if err := r.initializeStates(); err != nil {
return err
}
logger.Debugf("All states refreshed successfully")
return nil return nil
} }
return form
}
// NotifyFileChanged alerts the runner about file changes // NotifyFileChanged alerts the runner about file changes
func (r *Runner) NotifyFileChanged(filePath string) bool { func (r *Runner) NotifyFileChanged(filePath string) bool {
@ -418,7 +357,6 @@ func (r *Runner) RefreshModule(moduleName string) bool {
continue continue
} }
// Use the enhanced module loader refresh
if err := r.moduleLoader.RefreshModule(state.L, moduleName); err != nil { if err := r.moduleLoader.RefreshModule(state.L, moduleName); err != nil {
success = false success = false
logger.Debugf("Failed to refresh module %s in state %d: %v", moduleName, state.index, err) logger.Debugf("Failed to refresh module %s in state %d: %v", moduleName, state.index, err)
@ -432,63 +370,6 @@ func (r *Runner) RefreshModule(moduleName string) bool {
return success return success
} }
// RefreshModuleByPath refreshes a module by its file path
func (r *Runner) RefreshModuleByPath(filePath string) bool {
r.mu.RLock()
defer r.mu.RUnlock()
if !r.isRunning.Load() {
return false
}
logger.Debugf("Refreshing module by path: %s", filePath)
success := true
for _, state := range r.states {
if state == nil || state.inUse.Load() {
continue
}
// Use the enhanced module loader refresh by path
if err := r.moduleLoader.RefreshModuleByPath(state.L, filePath); err != nil {
success = false
logger.Debugf("Failed to refresh module at %s in state %d: %v", filePath, state.index, err)
}
}
return success
}
// 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.Load() {
count++
}
}
return count
}
// RunScriptFile loads, compiles and executes a Lua script file // RunScriptFile loads, compiles and executes a Lua script file
func (r *Runner) RunScriptFile(filePath string) (*Response, error) { func (r *Runner) RunScriptFile(filePath string) (*Response, error) {
if !r.isRunning.Load() { if !r.isRunning.Load() {
@ -523,10 +404,10 @@ func (r *Runner) RunScriptFile(filePath string) (*Response, error) {
r.mu.Unlock() r.mu.Unlock()
}() }()
// Get state from pool
var stateIndex int var stateIndex int
select { select {
case stateIndex = <-r.statePool: case stateIndex = <-r.statePool:
// Got a state
case <-time.After(5 * time.Second): case <-time.After(5 * time.Second):
return nil, ErrTimeout return nil, ErrTimeout
} }
@ -544,24 +425,25 @@ func (r *Runner) RunScriptFile(filePath string) (*Response, error) {
if r.isRunning.Load() { if r.isRunning.Load() {
select { select {
case r.statePool <- stateIndex: case r.statePool <- stateIndex:
// State returned to pool
default: default:
// Pool is full or closed
} }
} }
}() }()
// Compile script
bytecode, err := state.L.CompileBytecode(string(content), filepath.Base(absPath)) bytecode, err := state.L.CompileBytecode(string(content), filepath.Base(absPath))
if err != nil { if err != nil {
return nil, fmt.Errorf("compilation error: %w", err) return nil, fmt.Errorf("compilation error: %w", err)
} }
// Create simple context for script execution
ctx := NewContext() ctx := NewContext()
defer ctx.Release() defer ctx.Release()
ctx.Set("_script_path", absPath) ctx.Set("_script_path", absPath)
ctx.Set("_script_dir", scriptDir) ctx.Set("_script_dir", scriptDir)
// Execute script
response, err := state.sandbox.Execute(state.L, bytecode, ctx) response, err := state.sandbox.Execute(state.L, bytecode, ctx)
if err != nil { if err != nil {
return nil, fmt.Errorf("execution error: %w", err) return nil, fmt.Errorf("execution error: %w", err)

View File

@ -34,7 +34,7 @@ func ShutdownWatcherManager() {
} }
// WatchLuaRouter sets up a watcher for a LuaRouter's routes directory // WatchLuaRouter sets up a watcher for a LuaRouter's routes directory
func WatchLuaRouter(router *router.LuaRouter, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) { func WatchLuaRouter(router *router.Router, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) {
manager := GetWatcherManager() manager := GetWatcherManager()
config := DirectoryWatcherConfig{ config := DirectoryWatcherConfig{