rewrite router, server and runner
This commit is contained in:
parent
d6e24f0185
commit
e3ee503c31
317
http/server.go
317
http/server.go
@ -1,3 +1,4 @@
|
||||
// server.go - Simplified HTTP server
|
||||
package http
|
||||
|
||||
import (
|
||||
@ -9,7 +10,6 @@ import (
|
||||
|
||||
"Moonshark/router"
|
||||
"Moonshark/runner"
|
||||
"Moonshark/runner/lualibs"
|
||||
"Moonshark/sessions"
|
||||
"Moonshark/utils"
|
||||
"Moonshark/utils/color"
|
||||
@ -17,46 +17,35 @@ import (
|
||||
"Moonshark/utils/logger"
|
||||
"Moonshark/utils/metadata"
|
||||
|
||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
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 {
|
||||
luaRouter *router.LuaRouter
|
||||
luaRouter *router.Router
|
||||
staticHandler fasthttp.RequestHandler
|
||||
staticFS *fasthttp.FS
|
||||
luaRunner *runner.Runner
|
||||
fasthttpServer *fasthttp.Server
|
||||
debugMode bool
|
||||
cfg *config.Config
|
||||
sessionManager *sessions.SessionManager
|
||||
errorConfig utils.ErrorPageConfig
|
||||
ctxPool sync.Pool
|
||||
paramsPool sync.Pool
|
||||
staticPrefix string
|
||||
cfg *config.Config
|
||||
debugMode bool
|
||||
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
|
||||
if !strings.HasPrefix(staticPrefix, "/") {
|
||||
staticPrefix = "/" + staticPrefix
|
||||
}
|
||||
if !strings.HasSuffix(staticPrefix, "/") {
|
||||
staticPrefix = staticPrefix + "/"
|
||||
staticPrefix += "/"
|
||||
}
|
||||
|
||||
s := &Server{
|
||||
@ -65,58 +54,44 @@ func New(luaRouter *router.LuaRouter, runner *runner.Runner, cfg *config.Config,
|
||||
debugMode: debugMode,
|
||||
cfg: cfg,
|
||||
sessionManager: sessions.GlobalSessionManager,
|
||||
staticPrefix: 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
|
||||
s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, ""))
|
||||
s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error"))
|
||||
// Cache error pages
|
||||
errorConfig := utils.ErrorPageConfig{
|
||||
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
|
||||
if cfg.Dirs.Static != "" {
|
||||
s.staticFS = &fasthttp.FS{
|
||||
staticFS := &fasthttp.FS{
|
||||
Root: cfg.Dirs.Static,
|
||||
IndexNames: []string{"index.html"},
|
||||
GenerateIndexPages: false,
|
||||
AcceptByteRange: true,
|
||||
Compress: true,
|
||||
CompressedFileSuffix: ".gz",
|
||||
CompressBrotli: true,
|
||||
CompressZstd: true,
|
||||
PathRewrite: fasthttp.NewPathPrefixStripper(len(staticPrefix) - 1),
|
||||
}
|
||||
s.staticHandler = s.staticFS.NewRequestHandler()
|
||||
s.staticHandler = staticFS.NewRequestHandler()
|
||||
}
|
||||
|
||||
s.fasthttpServer = &fasthttp.Server{
|
||||
Handler: s.handleRequest,
|
||||
Name: "Moonshark/" + metadata.Version,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxRequestBodySize: 16 << 20,
|
||||
TCPKeepalive: true,
|
||||
TCPKeepalivePeriod: 60 * time.Second,
|
||||
ReduceMemoryUsage: true,
|
||||
DisablePreParseMultipartForm: true,
|
||||
DisableHeaderNamesNormalizing: true,
|
||||
NoDefaultServerHeader: true,
|
||||
StreamRequestBody: true,
|
||||
Handler: s.handleRequest,
|
||||
Name: "Moonshark/" + metadata.Version,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 120 * time.Second,
|
||||
MaxRequestBodySize: 16 << 20,
|
||||
TCPKeepalive: true,
|
||||
ReduceMemoryUsage: true,
|
||||
StreamRequestBody: true,
|
||||
NoDefaultServerHeader: true,
|
||||
}
|
||||
|
||||
return s
|
||||
@ -133,138 +108,66 @@ func (s *Server) Shutdown(ctx context.Context) error {
|
||||
|
||||
func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) {
|
||||
start := time.Now()
|
||||
methodBytes := ctx.Method()
|
||||
pathBytes := ctx.Path()
|
||||
method := string(ctx.Method())
|
||||
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)
|
||||
if s.cfg.Server.HTTPLogging {
|
||||
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
|
||||
}
|
||||
s.logRequest(ctx, method, path, time.Since(start))
|
||||
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)
|
||||
if s.cfg.Server.HTTPLogging {
|
||||
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
|
||||
}
|
||||
s.logRequest(ctx, method, path, time.Since(start))
|
||||
return
|
||||
}
|
||||
|
||||
bytecode, scriptPath, routeErr, params, found := s.luaRouter.GetRouteInfo(methodBytes, pathBytes)
|
||||
|
||||
if found {
|
||||
if len(bytecode) == 0 || routeErr != nil {
|
||||
s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, routeErr)
|
||||
} else {
|
||||
s.handleLuaRoute(ctx, bytecode, scriptPath, params, methodBytes, pathBytes)
|
||||
}
|
||||
} else {
|
||||
s.send404(ctx, pathBytes)
|
||||
// Route lookup
|
||||
bytecode, params, found := s.luaRouter.Lookup(method, path)
|
||||
if !found {
|
||||
s.send404(ctx)
|
||||
s.logRequest(ctx, method, path, time.Since(start))
|
||||
return
|
||||
}
|
||||
|
||||
if s.cfg.Server.HTTPLogging {
|
||||
logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start))
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string,
|
||||
params *router.Params, methodBytes, pathBytes []byte) {
|
||||
|
||||
luaCtx := runner.NewHTTPContext(ctx)
|
||||
defer luaCtx.Release()
|
||||
|
||||
if lualibs.GetGlobalEnvManager() != nil {
|
||||
luaCtx.Set("env", lualibs.GetGlobalEnvManager().GetAll())
|
||||
if len(bytecode) == 0 {
|
||||
s.send500(ctx, nil)
|
||||
s.logRequest(ctx, method, path, time.Since(start))
|
||||
return
|
||||
}
|
||||
|
||||
sessionMap := s.ctxPool.Get().(map[string]any)
|
||||
defer func() {
|
||||
for k := range sessionMap {
|
||||
delete(sessionMap, k)
|
||||
}
|
||||
s.ctxPool.Put(sessionMap)
|
||||
}()
|
||||
|
||||
// Get session
|
||||
session := s.sessionManager.GetSessionFromRequest(ctx)
|
||||
|
||||
// Advance flash data (move current flash to old, clear old)
|
||||
session.AdvanceFlash()
|
||||
|
||||
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)
|
||||
// Execute Lua script
|
||||
response, err := s.luaRunner.ExecuteHTTP(bytecode, ctx, params, session)
|
||||
if err != nil {
|
||||
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
|
||||
}
|
||||
|
||||
// Handle session updates including flash data
|
||||
if len(response.SessionData) > 0 {
|
||||
if _, clearAll := response.SessionData["__clear_all"]; clearAll {
|
||||
// Apply response
|
||||
s.applyResponse(ctx, response, session)
|
||||
runner.ReleaseResponse(response)
|
||||
|
||||
s.logRequest(ctx, method, path, time.Since(start))
|
||||
}
|
||||
|
||||
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() // Also clear flash data
|
||||
delete(response.SessionData, "__clear_all")
|
||||
session.ClearFlash()
|
||||
delete(resp.SessionData, "__clear_all")
|
||||
}
|
||||
|
||||
for k, v := range response.SessionData {
|
||||
for k, v := range resp.SessionData {
|
||||
if v == "__DELETE__" {
|
||||
session.Delete(k)
|
||||
} else {
|
||||
@ -273,91 +176,61 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip
|
||||
}
|
||||
}
|
||||
|
||||
// Handle flash data from response
|
||||
if flashData, ok := response.Metadata["flash"].(map[string]any); ok {
|
||||
// Handle flash data
|
||||
if flashData, ok := resp.Metadata["flash"].(map[string]any); ok {
|
||||
for k, v := range flashData {
|
||||
if err := session.FlashSafe(k, v); err != nil && s.debugMode {
|
||||
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)
|
||||
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.SetStatusCode(fasthttp.StatusNotFound)
|
||||
|
||||
// Use cached 404 for common case
|
||||
if len(pathBytes) == 1 && pathBytes[0] == '/' {
|
||||
s.errorCacheMu.RLock()
|
||||
ctx.SetBody(s.cached404)
|
||||
s.errorCacheMu.RUnlock()
|
||||
} else {
|
||||
ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, string(pathBytes))))
|
||||
}
|
||||
cacheMu.RLock()
|
||||
ctx.SetBody(cached404)
|
||||
cacheMu.RUnlock()
|
||||
}
|
||||
|
||||
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.SetStatusCode(status)
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
|
||||
if err == nil {
|
||||
s.errorCacheMu.RLock()
|
||||
ctx.SetBody(s.cached500)
|
||||
s.errorCacheMu.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 {
|
||||
errorMessage = err.Error() // Full error with stack trace
|
||||
}
|
||||
cacheMu.RLock()
|
||||
ctx.SetBody(cached500)
|
||||
cacheMu.RUnlock()
|
||||
} else {
|
||||
errorMessage = err.Error()
|
||||
errorConfig := utils.ErrorPageConfig{
|
||||
OverrideDir: s.cfg.Dirs.Override,
|
||||
DebugMode: s.debugMode,
|
||||
}
|
||||
ctx.SetBody([]byte(utils.InternalErrorPage(errorConfig, string(ctx.Path()), err.Error())))
|
||||
}
|
||||
|
||||
ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, string(pathBytes), errorMessage)))
|
||||
}
|
||||
|
||||
func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) {
|
||||
stats := utils.CollectSystemStats(s.cfg)
|
||||
routeCount, bytecodeBytes := s.luaRouter.GetRouteStats()
|
||||
stats.Components = utils.ComponentStats{
|
||||
RouteCount: routeCount,
|
||||
BytecodeBytes: bytecodeBytes,
|
||||
SessionStats: sessions.GlobalSessionManager.GetCacheStats(),
|
||||
RouteCount: 0, // TODO: Get from router
|
||||
BytecodeBytes: 0, // TODO: Get from router
|
||||
SessionStats: s.sessionManager.GetCacheStats(),
|
||||
}
|
||||
ctx.SetContentType("text/html; charset=utf-8")
|
||||
ctx.SetStatusCode(fasthttp.StatusOK)
|
||||
ctx.SetBody([]byte(utils.DebugStatsPage(stats)))
|
||||
}
|
||||
|
||||
// SetStaticCaching enables/disables static file caching
|
||||
func (s *Server) SetStaticCaching(duration time.Duration) {
|
||||
if s.staticFS != nil {
|
||||
s.staticFS.CacheDuration = duration
|
||||
s.staticHandler = s.staticFS.NewRequestHandler()
|
||||
func (s *Server) logRequest(ctx *fasthttp.RequestCtx, method, path string, duration time.Duration) {
|
||||
if s.cfg.Server.HTTPLogging {
|
||||
logger.Request(ctx.Response.StatusCode(), method, path, duration)
|
||||
}
|
||||
}
|
||||
|
||||
// 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()
|
||||
}
|
||||
|
@ -10,14 +10,11 @@ import (
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var (
|
||||
emptyMap = make(map[string]any)
|
||||
formDataPool = sync.Pool{
|
||||
New: func() any {
|
||||
return make(map[string]any, 16)
|
||||
},
|
||||
}
|
||||
)
|
||||
var formDataPool = sync.Pool{
|
||||
New: func() any {
|
||||
return make(map[string]any, 16)
|
||||
},
|
||||
}
|
||||
|
||||
func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any {
|
||||
args := ctx.QueryArgs()
|
||||
|
34
moonshark.go
34
moonshark.go
@ -8,6 +8,7 @@ import (
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"syscall"
|
||||
"time"
|
||||
@ -28,7 +29,7 @@ import (
|
||||
|
||||
type Moonshark struct {
|
||||
Config *config.Config
|
||||
LuaRouter *router.LuaRouter
|
||||
LuaRouter *router.Router
|
||||
LuaRunner *runner.Runner
|
||||
HTTPServer *http.Server
|
||||
cleanupFuncs []func() error
|
||||
@ -95,6 +96,9 @@ func newMoonshark(cfg *config.Config, debug, scriptMode bool) (*Moonshark, error
|
||||
if scriptMode {
|
||||
poolSize = 1
|
||||
}
|
||||
if poolSize == 0 {
|
||||
poolSize = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
// Initialize runner first (needed for both modes)
|
||||
if err := s.initRunner(poolSize); err != nil {
|
||||
@ -112,16 +116,8 @@ func newMoonshark(cfg *config.Config, debug, scriptMode bool) (*Moonshark, error
|
||||
}
|
||||
|
||||
s.setupWatchers()
|
||||
|
||||
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
|
||||
if dirExists(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)
|
||||
|
||||
var err error
|
||||
s.LuaRunner, err = runner.NewRunner(
|
||||
runner.WithPoolSize(poolSize),
|
||||
runner.WithLibDirs(s.Config.Dirs.Libs...),
|
||||
runner.WithFsDir(s.Config.Dirs.FS),
|
||||
runner.WithDataDir(s.Config.Dirs.Data),
|
||||
)
|
||||
s.LuaRunner, err = runner.NewRunner(poolSize, s.Config.Dirs.Data, s.Config.Dirs.FS)
|
||||
if err != nil {
|
||||
return fmt.Errorf("lua runner init failed: %v", err)
|
||||
}
|
||||
@ -172,18 +163,9 @@ func (s *Moonshark) initRouter() 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 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))
|
||||
|
@ -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
|
||||
})
|
||||
}
|
@ -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,
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
@ -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()
|
||||
}
|
187
router/match.go
187
router/match.go
@ -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))
|
||||
}
|
190
router/node.go
190
router/node.go
@ -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
|
||||
}
|
@ -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++
|
||||
}
|
||||
}
|
545
router/router.go
545
router/router.go
@ -3,44 +3,54 @@ package router
|
||||
import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||
"github.com/VictoriaMetrics/fastcache"
|
||||
)
|
||||
|
||||
// Default cache sizes
|
||||
const (
|
||||
defaultBytecodeMaxBytes = 32 * 1024 * 1024 // 32MB for bytecode cache
|
||||
defaultRouteMaxBytes = 8 * 1024 * 1024 // 8MB for route match cache
|
||||
)
|
||||
|
||||
// LuaRouter is a filesystem-based HTTP router for Lua files
|
||||
type LuaRouter struct {
|
||||
routesDir string // Root directory containing route files
|
||||
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
|
||||
// node represents a node in the radix trie
|
||||
type node struct {
|
||||
segment string
|
||||
bytecode []byte
|
||||
scriptPath string
|
||||
children []*node
|
||||
isDynamic bool
|
||||
isWildcard bool
|
||||
maxParams uint8
|
||||
}
|
||||
|
||||
// NewLuaRouter creates a new LuaRouter instance
|
||||
func NewLuaRouter(routesDir string) (*LuaRouter, error) {
|
||||
// Router is a filesystem-based HTTP router for Lua files with bytecode caching
|
||||
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)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -49,108 +59,443 @@ func NewLuaRouter(routesDir string) (*LuaRouter, error) {
|
||||
return nil, errors.New("routes path is not a directory")
|
||||
}
|
||||
|
||||
// Create shared Lua state
|
||||
compileState := luajit.New()
|
||||
if compileState == nil {
|
||||
return nil, errors.New("failed to create Lua compile state")
|
||||
}
|
||||
|
||||
r := &LuaRouter{
|
||||
r := &Router{
|
||||
routesDir: routesDir,
|
||||
routes: make(map[string]*node),
|
||||
failedRoutes: make(map[string]*RouteError),
|
||||
middlewareFiles: make(map[string][]string),
|
||||
routeCache: fastcache.New(defaultRouteMaxBytes),
|
||||
bytecodeCache: fastcache.New(defaultBytecodeMaxBytes),
|
||||
middlewareCache: make(map[string][]byte),
|
||||
sourceCache: make(map[string][]byte),
|
||||
sourceMtimes: make(map[string]time.Time),
|
||||
get: &node{},
|
||||
post: &node{},
|
||||
put: &node{},
|
||||
patch: &node{},
|
||||
delete: &node{},
|
||||
bytecodeCache: fastcache.New(32 * 1024 * 1024), // 32MB
|
||||
compileState: compileState,
|
||||
paramsBuffer: make([]string, 64),
|
||||
middlewareFiles: make(map[string][]string),
|
||||
}
|
||||
|
||||
methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"}
|
||||
for _, method := range methods {
|
||||
r.routes[method] = &node{
|
||||
staticChild: make(map[string]*node),
|
||||
}
|
||||
}
|
||||
|
||||
err = r.buildRoutes()
|
||||
|
||||
if len(r.failedRoutes) > 0 {
|
||||
return r, ErrRoutesCompilationErrors
|
||||
}
|
||||
|
||||
return r, err
|
||||
return r, r.buildRoutes()
|
||||
}
|
||||
|
||||
// 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),
|
||||
}
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
r.failedRoutes = make(map[string]*RouteError)
|
||||
// buildRoutes scans the routes directory and builds the routing tree
|
||||
func (r *Router) buildRoutes() error {
|
||||
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()
|
||||
// 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 len(r.failedRoutes) > 0 {
|
||||
return ErrRoutesCompilationErrors
|
||||
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, "\\", "/")
|
||||
}
|
||||
|
||||
r.middlewareFiles[fsPath] = append(r.middlewareFiles[fsPath], path)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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)
|
||||
})
|
||||
}
|
||||
|
||||
// ReportFailedRoutes returns a list of routes that failed to compile
|
||||
func (r *LuaRouter) ReportFailedRoutes() []*RouteError {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
// parseURLPath strips group segments from filesystem path
|
||||
func (r *Router) parseURLPath(fsPath string) string {
|
||||
segments := strings.Split(strings.Trim(fsPath, "/"), "/")
|
||||
var urlSegments []string
|
||||
|
||||
result := make([]*RouteError, 0, len(r.failedRoutes))
|
||||
for _, re := range r.failedRoutes {
|
||||
result = append(result, re)
|
||||
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)
|
||||
}
|
||||
|
||||
return result
|
||||
if len(urlSegments) == 0 {
|
||||
return "/"
|
||||
}
|
||||
return "/" + strings.Join(urlSegments, "/")
|
||||
}
|
||||
|
||||
// Close cleans up the router and its resources
|
||||
func (r *LuaRouter) Close() {
|
||||
r.compileStateMu.Lock()
|
||||
// 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 {
|
||||
r.compileState.Close()
|
||||
r.compileState = nil
|
||||
}
|
||||
r.compileStateMu.Unlock()
|
||||
r.compileMu.Unlock()
|
||||
}
|
||||
|
||||
// GetRouteStats returns statistics about the router
|
||||
func (r *LuaRouter) GetRouteStats() (int, int64) {
|
||||
r.mu.RLock()
|
||||
defer r.mu.RUnlock()
|
||||
|
||||
routeCount := 0
|
||||
bytecodeBytes := int64(0)
|
||||
|
||||
for _, root := range r.routes {
|
||||
count, bytes := countNodesAndBytecode(root)
|
||||
routeCount += count
|
||||
bytecodeBytes += bytes
|
||||
// Helper functions from cache.go
|
||||
func hashString(s string) uint64 {
|
||||
h := uint64(5381)
|
||||
for i := 0; i < len(s); i++ {
|
||||
h = ((h << 5) + h) + uint64(s[i])
|
||||
}
|
||||
|
||||
return routeCount, bytecodeBytes
|
||||
return h
|
||||
}
|
||||
|
||||
type NodeWithError struct {
|
||||
ScriptPath string
|
||||
Error error
|
||||
func uint64ToBytes(n uint64) []byte {
|
||||
b := make([]byte, 8)
|
||||
b[0] = byte(n)
|
||||
b[1] = byte(n >> 8)
|
||||
b[2] = byte(n >> 16)
|
||||
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
|
||||
}
|
||||
|
536
runner/runner.go
536
runner/runner.go
@ -1,237 +1,106 @@
|
||||
// runner.go - Simplified interface
|
||||
package runner
|
||||
|
||||
import (
|
||||
"Moonshark/runner/lualibs"
|
||||
"Moonshark/utils/color"
|
||||
"Moonshark/utils/logger"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"Moonshark/router"
|
||||
"Moonshark/runner/lualibs"
|
||||
"Moonshark/sessions"
|
||||
"Moonshark/utils/logger"
|
||||
|
||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||
"github.com/goccy/go-json"
|
||||
"github.com/valyala/bytebufferpool"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
// Common errors
|
||||
var emptyMap = make(map[string]any)
|
||||
|
||||
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")
|
||||
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 {
|
||||
L *luajit.State // The Lua state
|
||||
sandbox *Sandbox // Associated sandbox
|
||||
index int // Index for debugging
|
||||
inUse atomic.Bool // Whether the state is currently in use
|
||||
L *luajit.State
|
||||
sandbox *Sandbox
|
||||
index int
|
||||
inUse atomic.Bool
|
||||
}
|
||||
|
||||
// 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
|
||||
dataDir string // Data directory for SQLite databases
|
||||
fsDir string // Virtual filesystem directory
|
||||
isRunning atomic.Bool // Whether the runner is active
|
||||
mu sync.RWMutex // Mutex for thread safety
|
||||
scriptDir string // Current script directory
|
||||
states []*State
|
||||
statePool chan int
|
||||
poolSize int
|
||||
moduleLoader *ModuleLoader
|
||||
isRunning atomic.Bool
|
||||
mu sync.RWMutex
|
||||
scriptDir string
|
||||
|
||||
// Pre-allocated pools for HTTP processing
|
||||
ctxPool sync.Pool
|
||||
paramsPool sync.Pool
|
||||
}
|
||||
|
||||
// WithPoolSize sets the state pool size
|
||||
func WithPoolSize(size int) RunnerOption {
|
||||
return func(r *Runner) {
|
||||
if size > 0 {
|
||||
r.poolSize = size
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithDataDir sets the data directory for SQLite databases
|
||||
func WithDataDir(dataDir string) RunnerOption {
|
||||
return func(r *Runner) {
|
||||
if dataDir != "" {
|
||||
r.dataDir = dataDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// WithFsDir sets the virtual filesystem directory
|
||||
func WithFsDir(fsDir string) RunnerOption {
|
||||
return func(r *Runner) {
|
||||
if fsDir != "" {
|
||||
r.fsDir = fsDir
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NewRunner creates a new Runner with a pool of states
|
||||
func NewRunner(options ...RunnerOption) (*Runner, error) {
|
||||
// Default configuration
|
||||
runner := &Runner{
|
||||
poolSize: runtime.GOMAXPROCS(0),
|
||||
dataDir: "data",
|
||||
fsDir: "fs",
|
||||
func NewRunner(poolSize int, dataDir, fsDir string) (*Runner, error) {
|
||||
if poolSize <= 0 {
|
||||
poolSize = runtime.GOMAXPROCS(0)
|
||||
}
|
||||
|
||||
// Apply options
|
||||
for _, opt := range options {
|
||||
opt(runner)
|
||||
r := &Runner{
|
||||
poolSize: poolSize,
|
||||
moduleLoader: NewModuleLoader(&ModuleConfig{}),
|
||||
ctxPool: sync.Pool{
|
||||
New: func() any { return make(map[string]any, 8) },
|
||||
},
|
||||
paramsPool: sync.Pool{
|
||||
New: func() any { return make(map[string]any, 4) },
|
||||
},
|
||||
}
|
||||
|
||||
// Set up module loader if not already initialized
|
||||
if runner.moduleLoader == nil {
|
||||
config := &ModuleConfig{
|
||||
ScriptDir: "",
|
||||
LibDirs: []string{},
|
||||
}
|
||||
runner.moduleLoader = NewModuleLoader(config)
|
||||
}
|
||||
lualibs.InitSQLite(dataDir)
|
||||
lualibs.InitFS(fsDir)
|
||||
lualibs.SetSQLitePoolSize(poolSize)
|
||||
|
||||
lualibs.InitSQLite(runner.dataDir)
|
||||
lualibs.InitFS(runner.fsDir)
|
||||
r.states = make([]*State, poolSize)
|
||||
r.statePool = make(chan int, poolSize)
|
||||
|
||||
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 {
|
||||
if err := r.initStates(); err != nil {
|
||||
lualibs.CleanupSQLite()
|
||||
runner.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
runner.isRunning.Store(true)
|
||||
return runner, nil
|
||||
r.isRunning.Store(true)
|
||||
return r, nil
|
||||
}
|
||||
|
||||
// initializeStates creates and initializes all states in the pool
|
||||
func (r *Runner) initializeStates() error {
|
||||
logger.Infof("[LuaRunner] Creating %s states...", color.Yellow(strconv.Itoa(r.poolSize)))
|
||||
// Single entry point for HTTP execution
|
||||
func (r *Runner) ExecuteHTTP(bytecode []byte, httpCtx *fasthttp.RequestCtx,
|
||||
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() {
|
||||
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 from the pool
|
||||
// Get state with timeout
|
||||
var stateIndex int
|
||||
select {
|
||||
case stateIndex = <-r.statePool:
|
||||
// Got a state
|
||||
case <-ctx.Done():
|
||||
return nil, ctx.Err()
|
||||
case <-time.After(1 * time.Second):
|
||||
case <-time.After(time.Second):
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
|
||||
state := r.states[stateIndex]
|
||||
if state == nil {
|
||||
r.statePool <- stateIndex
|
||||
return nil, ErrStateNotReady
|
||||
}
|
||||
|
||||
// Use atomic operations
|
||||
state.inUse.Store(true)
|
||||
|
||||
defer func() {
|
||||
@ -240,26 +109,148 @@ func (r *Runner) Execute(ctx context.Context, bytecode []byte, execCtx *Context,
|
||||
select {
|
||||
case r.statePool <- stateIndex:
|
||||
default:
|
||||
// Pool is full or closed, state will be cleaned up by Close()
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Execute in sandbox
|
||||
response, err := state.sandbox.Execute(state.L, bytecode, execCtx)
|
||||
if err != nil {
|
||||
// Build Lua context directly from HTTP request
|
||||
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 {
|
||||
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 response, nil
|
||||
if err := r.moduleLoader.SetupRequire(L); err != nil {
|
||||
L.Cleanup()
|
||||
L.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if err := r.moduleLoader.PreloadModules(L); err != nil {
|
||||
L.Cleanup()
|
||||
L.Close()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &State{L: L, sandbox: sb, index: index}, nil
|
||||
}
|
||||
|
||||
// Run executes a Lua script with immediate context
|
||||
func (r *Runner) Run(bytecode []byte, execCtx *Context, scriptPath string) (*Response, 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()
|
||||
@ -267,22 +258,21 @@ func (r *Runner) Close() error {
|
||||
if !r.isRunning.Load() {
|
||||
return ErrRunnerClosed
|
||||
}
|
||||
|
||||
r.isRunning.Store(false)
|
||||
|
||||
// Drain all states from the pool
|
||||
// Drain pool
|
||||
for {
|
||||
select {
|
||||
case <-r.statePool:
|
||||
default:
|
||||
goto waitForInUse
|
||||
goto cleanup
|
||||
}
|
||||
}
|
||||
|
||||
waitForInUse:
|
||||
// Wait for in-use states to finish (with timeout)
|
||||
cleanup:
|
||||
// Wait for states to finish
|
||||
timeout := time.Now().Add(10 * time.Second)
|
||||
for {
|
||||
for time.Now().Before(timeout) {
|
||||
allIdle := true
|
||||
for _, state := range r.states {
|
||||
if state != nil && state.inUse.Load() {
|
||||
@ -290,25 +280,15 @@ waitForInUse:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allIdle {
|
||||
break
|
||||
}
|
||||
|
||||
if time.Now().After(timeout) {
|
||||
logger.Warnf("Timeout waiting for states to finish during shutdown, forcing close")
|
||||
break
|
||||
}
|
||||
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
|
||||
// Now safely close all states
|
||||
// Close 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
|
||||
@ -317,74 +297,33 @@ waitForInUse:
|
||||
|
||||
lualibs.CleanupFS()
|
||||
lualibs.CleanupSQLite()
|
||||
|
||||
logger.Debugf("Runner closed")
|
||||
return nil
|
||||
}
|
||||
|
||||
// RefreshStates rebuilds all states in the pool
|
||||
func (r *Runner) RefreshStates() error {
|
||||
r.mu.Lock()
|
||||
defer r.mu.Unlock()
|
||||
// parseForm extracts form data from HTTP request
|
||||
func parseForm(ctx *fasthttp.RequestCtx) map[string]any {
|
||||
form := make(map[string]any)
|
||||
|
||||
if !r.isRunning.Load() {
|
||||
return ErrRunnerClosed
|
||||
}
|
||||
// Parse POST form data
|
||||
ctx.PostArgs().VisitAll(func(key, value []byte) {
|
||||
form[string(key)] = string(value)
|
||||
})
|
||||
|
||||
logger.Infof("Runner is refreshing all states...")
|
||||
|
||||
// Drain all states from the pool
|
||||
for {
|
||||
select {
|
||||
case <-r.statePool:
|
||||
default:
|
||||
goto waitForInUse
|
||||
}
|
||||
}
|
||||
|
||||
waitForInUse:
|
||||
// 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
|
||||
// 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
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
if len(form) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Reinitialize all states
|
||||
if err := r.initializeStates(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
logger.Debugf("All states refreshed successfully")
|
||||
return nil
|
||||
return form
|
||||
}
|
||||
|
||||
// NotifyFileChanged alerts the runner about file changes
|
||||
@ -418,7 +357,6 @@ func (r *Runner) RefreshModule(moduleName string) bool {
|
||||
continue
|
||||
}
|
||||
|
||||
// Use the enhanced module loader refresh
|
||||
if err := r.moduleLoader.RefreshModule(state.L, moduleName); err != nil {
|
||||
success = false
|
||||
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
|
||||
}
|
||||
|
||||
// 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
|
||||
func (r *Runner) RunScriptFile(filePath string) (*Response, error) {
|
||||
if !r.isRunning.Load() {
|
||||
@ -523,10 +404,10 @@ func (r *Runner) RunScriptFile(filePath string) (*Response, error) {
|
||||
r.mu.Unlock()
|
||||
}()
|
||||
|
||||
// Get state from pool
|
||||
var stateIndex int
|
||||
select {
|
||||
case stateIndex = <-r.statePool:
|
||||
// Got a state
|
||||
case <-time.After(5 * time.Second):
|
||||
return nil, ErrTimeout
|
||||
}
|
||||
@ -544,24 +425,25 @@ func (r *Runner) RunScriptFile(filePath string) (*Response, error) {
|
||||
if r.isRunning.Load() {
|
||||
select {
|
||||
case r.statePool <- stateIndex:
|
||||
// State returned to pool
|
||||
default:
|
||||
// Pool is full or closed
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
// Compile script
|
||||
bytecode, err := state.L.CompileBytecode(string(content), filepath.Base(absPath))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("compilation error: %w", err)
|
||||
}
|
||||
|
||||
// Create simple context for script execution
|
||||
ctx := NewContext()
|
||||
defer ctx.Release()
|
||||
|
||||
ctx.Set("_script_path", absPath)
|
||||
ctx.Set("_script_dir", scriptDir)
|
||||
|
||||
// Execute script
|
||||
response, err := state.sandbox.Execute(state.L, bytecode, ctx)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("execution error: %w", err)
|
||||
|
@ -34,7 +34,7 @@ func ShutdownWatcherManager() {
|
||||
}
|
||||
|
||||
// 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()
|
||||
|
||||
config := DirectoryWatcherConfig{
|
||||
|
Loading…
x
Reference in New Issue
Block a user