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
|
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")
|
debugPath = []byte("/debug/stats")
|
||||||
methodPOST = []byte("POST")
|
staticMethods = map[string]bool{"GET": true, "HEAD": true, "OPTIONS": true}
|
||||||
methodPUT = []byte("PUT")
|
cached404, cached500 []byte
|
||||||
methodPATCH = []byte("PATCH")
|
cacheMu sync.RWMutex
|
||||||
debugPath = []byte("/debug/stats")
|
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,58 +54,44 @@ 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{
|
||||||
Handler: s.handleRequest,
|
Handler: s.handleRequest,
|
||||||
Name: "Moonshark/" + metadata.Version,
|
Name: "Moonshark/" + metadata.Version,
|
||||||
ReadTimeout: 30 * time.Second,
|
ReadTimeout: 30 * time.Second,
|
||||||
WriteTimeout: 30 * time.Second,
|
WriteTimeout: 30 * time.Second,
|
||||||
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,
|
StreamRequestBody: true,
|
||||||
DisablePreParseMultipartForm: true,
|
NoDefaultServerHeader: true,
|
||||||
DisableHeaderNamesNormalizing: true,
|
|
||||||
NoDefaultServerHeader: true,
|
|
||||||
StreamRequestBody: 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,
|
|
||||||
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)
|
// Get session
|
||||||
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)
|
||||||
|
|
||||||
|
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.Clear()
|
||||||
session.ClearFlash() // Also clear flash data
|
session.ClearFlash()
|
||||||
delete(response.SessionData, "__clear_all")
|
delete(resp.SessionData, "__clear_all")
|
||||||
}
|
}
|
||||||
|
|
||||||
for k, v := range response.SessionData {
|
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 {
|
|
||||||
errorMessage = err.Error() // Full error with stack trace
|
|
||||||
}
|
|
||||||
} else {
|
} 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) {
|
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()
|
|
||||||
}
|
|
||||||
|
@ -10,14 +10,11 @@ import (
|
|||||||
"github.com/valyala/fasthttp"
|
"github.com/valyala/fasthttp"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var formDataPool = sync.Pool{
|
||||||
emptyMap = make(map[string]any)
|
New: func() any {
|
||||||
formDataPool = sync.Pool{
|
return make(map[string]any, 16)
|
||||||
New: func() any {
|
},
|
||||||
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()
|
||||||
|
34
moonshark.go
34
moonshark.go
@ -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,18 +163,9 @@ 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) {
|
return fmt.Errorf("lua router init failed: %v", err)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Infof("LuaRouter is g2g! %s", color.Yellow(s.Config.Dirs.Routes))
|
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 (
|
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),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
err = r.buildRoutes()
|
|
||||||
|
|
||||||
if len(r.failedRoutes) > 0 {
|
|
||||||
return r, ErrRoutesCompilationErrors
|
|
||||||
}
|
|
||||||
|
|
||||||
return r, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Refresh rebuilds the router by rescanning the routes directory
|
// methodNode returns the root node for a method
|
||||||
func (r *LuaRouter) Refresh() error {
|
func (r *Router) methodNode(method string) *node {
|
||||||
r.mu.Lock()
|
switch method {
|
||||||
defer r.mu.Unlock()
|
case "GET":
|
||||||
|
return r.get
|
||||||
for method := range r.routes {
|
case "POST":
|
||||||
r.routes[method] = &node{
|
return r.post
|
||||||
staticChild: make(map[string]*node),
|
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.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 {
|
if strings.TrimSuffix(info.Name(), ".lua") == "middleware" {
|
||||||
return ErrRoutesCompilationErrors
|
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
|
// parseURLPath strips group segments from filesystem path
|
||||||
func (r *LuaRouter) ReportFailedRoutes() []*RouteError {
|
func (r *Router) parseURLPath(fsPath string) string {
|
||||||
r.mu.RLock()
|
segments := strings.Split(strings.Trim(fsPath, "/"), "/")
|
||||||
defer r.mu.RUnlock()
|
var urlSegments []string
|
||||||
|
|
||||||
result := make([]*RouteError, 0, len(r.failedRoutes))
|
for _, segment := range segments {
|
||||||
for _, re := range r.failedRoutes {
|
if segment == "" {
|
||||||
result = append(result, re)
|
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
|
// getMiddlewareChain returns middleware files that apply to the given filesystem path
|
||||||
func (r *LuaRouter) Close() {
|
func (r *Router) getMiddlewareChain(fsPath string) []string {
|
||||||
r.compileStateMu.Lock()
|
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)
|
|
||||||
|
|
||||||
for _, root := range r.routes {
|
|
||||||
count, bytes := countNodesAndBytecode(root)
|
|
||||||
routeCount += count
|
|
||||||
bytecodeBytes += bytes
|
|
||||||
}
|
}
|
||||||
|
return h
|
||||||
return routeCount, bytecodeBytes
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type NodeWithError struct {
|
func uint64ToBytes(n uint64) []byte {
|
||||||
ScriptPath string
|
b := make([]byte, 8)
|
||||||
Error error
|
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
|
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
|
|
||||||
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",
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Apply options
|
r := &Runner{
|
||||||
for _, opt := range options {
|
poolSize: poolSize,
|
||||||
opt(runner)
|
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
|
lualibs.InitSQLite(dataDir)
|
||||||
if runner.moduleLoader == nil {
|
lualibs.InitFS(fsDir)
|
||||||
config := &ModuleConfig{
|
lualibs.SetSQLitePoolSize(poolSize)
|
||||||
ScriptDir: "",
|
|
||||||
LibDirs: []string{},
|
|
||||||
}
|
|
||||||
runner.moduleLoader = NewModuleLoader(config)
|
|
||||||
}
|
|
||||||
|
|
||||||
lualibs.InitSQLite(runner.dataDir)
|
r.states = make([]*State, poolSize)
|
||||||
lualibs.InitFS(runner.fsDir)
|
r.statePool = make(chan int, poolSize)
|
||||||
|
|
||||||
lualibs.SetSQLitePoolSize(runner.poolSize)
|
if err := r.initStates(); err != nil {
|
||||||
|
|
||||||
// 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)
|
||||||
if err != nil {
|
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 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 {
|
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,74 +297,33 @@ 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)
|
||||||
|
})
|
||||||
|
|
||||||
logger.Infof("Runner is refreshing all states...")
|
// Parse multipart form if present
|
||||||
|
if multipartForm, err := ctx.MultipartForm(); err == nil {
|
||||||
// Drain all states from the pool
|
for key, values := range multipartForm.Value {
|
||||||
for {
|
if len(values) == 1 {
|
||||||
select {
|
form[key] = values[0]
|
||||||
case <-r.statePool:
|
} else {
|
||||||
default:
|
form[key] = values
|
||||||
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
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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
|
if len(form) == 0 {
|
||||||
for i, state := range r.states {
|
return 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.Close()
|
|
||||||
r.states[i] = nil
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
return form
|
||||||
// Reinitialize all states
|
|
||||||
if err := r.initializeStates(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Debugf("All states refreshed successfully")
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// NotifyFileChanged alerts the runner about file changes
|
// NotifyFileChanged alerts the runner about file changes
|
||||||
@ -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)
|
||||||
|
@ -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{
|
||||||
|
Loading…
x
Reference in New Issue
Block a user