diff --git a/http/server.go b/http/server.go index a28d662..a5a6565 100644 --- a/http/server.go +++ b/http/server.go @@ -1,3 +1,4 @@ +// server.go - Simplified HTTP server package http import ( @@ -9,7 +10,6 @@ import ( "Moonshark/router" "Moonshark/runner" - "Moonshark/runner/lualibs" "Moonshark/sessions" "Moonshark/utils" "Moonshark/utils/color" @@ -17,46 +17,35 @@ import ( "Moonshark/utils/logger" "Moonshark/utils/metadata" - luajit "git.sharkk.net/Sky/LuaJIT-to-Go" "github.com/valyala/fasthttp" ) var ( - //methodGET = []byte("GET") - methodPOST = []byte("POST") - methodPUT = []byte("PUT") - methodPATCH = []byte("PATCH") - debugPath = []byte("/debug/stats") + debugPath = []byte("/debug/stats") + staticMethods = map[string]bool{"GET": true, "HEAD": true, "OPTIONS": true} + cached404, cached500 []byte + cacheMu sync.RWMutex + emptyMap = make(map[string]any) ) type Server struct { - luaRouter *router.LuaRouter + luaRouter *router.Router staticHandler fasthttp.RequestHandler - staticFS *fasthttp.FS luaRunner *runner.Runner fasthttpServer *fasthttp.Server - debugMode bool - cfg *config.Config sessionManager *sessions.SessionManager - errorConfig utils.ErrorPageConfig - ctxPool sync.Pool - paramsPool sync.Pool - staticPrefix string + cfg *config.Config + debugMode bool staticPrefixBytes []byte - - // Cached error pages - cached404 []byte - cached500 []byte - errorCacheMu sync.RWMutex } -func New(luaRouter *router.LuaRouter, runner *runner.Runner, cfg *config.Config, debugMode bool) *Server { +func New(luaRouter *router.Router, runner *runner.Runner, cfg *config.Config, debugMode bool) *Server { staticPrefix := cfg.Server.StaticPrefix if !strings.HasPrefix(staticPrefix, "/") { staticPrefix = "/" + staticPrefix } if !strings.HasSuffix(staticPrefix, "/") { - staticPrefix = staticPrefix + "/" + staticPrefix += "/" } s := &Server{ @@ -65,58 +54,44 @@ func New(luaRouter *router.LuaRouter, runner *runner.Runner, cfg *config.Config, debugMode: debugMode, cfg: cfg, sessionManager: sessions.GlobalSessionManager, - staticPrefix: staticPrefix, staticPrefixBytes: []byte(staticPrefix), - errorConfig: utils.ErrorPageConfig{ - OverrideDir: cfg.Dirs.Override, - DebugMode: debugMode, - }, - ctxPool: sync.Pool{ - New: func() any { - return make(map[string]any, 6) - }, - }, - paramsPool: sync.Pool{ - New: func() any { - return make(map[string]any, 4) - }, - }, } - // Pre-cache error pages - s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, "")) - s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error")) + // Cache error pages + errorConfig := utils.ErrorPageConfig{ + OverrideDir: cfg.Dirs.Override, + DebugMode: debugMode, + } + cacheMu.Lock() + cached404 = []byte(utils.NotFoundPage(errorConfig, "")) + cached500 = []byte(utils.InternalErrorPage(errorConfig, "", "Internal Server Error")) + cacheMu.Unlock() // Setup static file serving if cfg.Dirs.Static != "" { - s.staticFS = &fasthttp.FS{ + staticFS := &fasthttp.FS{ Root: cfg.Dirs.Static, IndexNames: []string{"index.html"}, - GenerateIndexPages: false, AcceptByteRange: true, Compress: true, CompressedFileSuffix: ".gz", CompressBrotli: true, - CompressZstd: true, PathRewrite: fasthttp.NewPathPrefixStripper(len(staticPrefix) - 1), } - s.staticHandler = s.staticFS.NewRequestHandler() + s.staticHandler = staticFS.NewRequestHandler() } s.fasthttpServer = &fasthttp.Server{ - Handler: s.handleRequest, - Name: "Moonshark/" + metadata.Version, - ReadTimeout: 30 * time.Second, - WriteTimeout: 30 * time.Second, - IdleTimeout: 120 * time.Second, - MaxRequestBodySize: 16 << 20, - TCPKeepalive: true, - TCPKeepalivePeriod: 60 * time.Second, - ReduceMemoryUsage: true, - DisablePreParseMultipartForm: true, - DisableHeaderNamesNormalizing: true, - NoDefaultServerHeader: true, - StreamRequestBody: true, + Handler: s.handleRequest, + Name: "Moonshark/" + metadata.Version, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + MaxRequestBodySize: 16 << 20, + TCPKeepalive: true, + ReduceMemoryUsage: true, + StreamRequestBody: true, + NoDefaultServerHeader: true, } return s @@ -133,138 +108,66 @@ func (s *Server) Shutdown(ctx context.Context) error { func (s *Server) handleRequest(ctx *fasthttp.RequestCtx) { start := time.Now() - methodBytes := ctx.Method() - pathBytes := ctx.Path() + method := string(ctx.Method()) + path := string(ctx.Path()) - if s.debugMode && bytes.Equal(pathBytes, debugPath) { + // Debug stats endpoint + if s.debugMode && bytes.Equal(ctx.Path(), debugPath) { s.handleDebugStats(ctx) - if s.cfg.Server.HTTPLogging { - logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start)) - } + s.logRequest(ctx, method, path, time.Since(start)) return } - if s.staticHandler != nil && bytes.HasPrefix(pathBytes, s.staticPrefixBytes) { + // Static file serving + if s.staticHandler != nil && bytes.HasPrefix(ctx.Path(), s.staticPrefixBytes) { s.staticHandler(ctx) - if s.cfg.Server.HTTPLogging { - logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start)) - } + s.logRequest(ctx, method, path, time.Since(start)) return } - bytecode, scriptPath, routeErr, params, found := s.luaRouter.GetRouteInfo(methodBytes, pathBytes) - - if found { - if len(bytecode) == 0 || routeErr != nil { - s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, routeErr) - } else { - s.handleLuaRoute(ctx, bytecode, scriptPath, params, methodBytes, pathBytes) - } - } else { - s.send404(ctx, pathBytes) + // Route lookup + bytecode, params, found := s.luaRouter.Lookup(method, path) + if !found { + s.send404(ctx) + s.logRequest(ctx, method, path, time.Since(start)) + return } - if s.cfg.Server.HTTPLogging { - logger.Request(ctx.Response.StatusCode(), string(methodBytes), string(pathBytes), time.Since(start)) - } -} - -func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scriptPath string, - params *router.Params, methodBytes, pathBytes []byte) { - - luaCtx := runner.NewHTTPContext(ctx) - defer luaCtx.Release() - - if lualibs.GetGlobalEnvManager() != nil { - luaCtx.Set("env", lualibs.GetGlobalEnvManager().GetAll()) + if len(bytecode) == 0 { + s.send500(ctx, nil) + s.logRequest(ctx, method, path, time.Since(start)) + return } - sessionMap := s.ctxPool.Get().(map[string]any) - defer func() { - for k := range sessionMap { - delete(sessionMap, k) - } - s.ctxPool.Put(sessionMap) - }() - + // Get session session := s.sessionManager.GetSessionFromRequest(ctx) - // Advance flash data (move current flash to old, clear old) - session.AdvanceFlash() - - sessionMap["id"] = session.ID - - // Get session data and flash data - if !session.IsEmpty() { - sessionMap["data"] = session.GetAll() // This now includes flash data - sessionMap["flash"] = session.GetAllFlash() - } else { - sessionMap["data"] = emptyMap - sessionMap["flash"] = emptyMap - } - - // Set basic context - luaCtx.Set("method", string(methodBytes)) - luaCtx.Set("path", string(pathBytes)) - luaCtx.Set("host", string(ctx.Host())) - luaCtx.Set("session", sessionMap) - - // Add headers to context - headers := make(map[string]any) - ctx.Request.Header.VisitAll(func(key, value []byte) { - headers[string(key)] = string(value) - }) - luaCtx.Set("headers", headers) - - // Handle params - if params != nil && params.Count > 0 { - paramMap := s.paramsPool.Get().(map[string]any) - for i := range params.Count { - paramMap[params.Keys[i]] = params.Values[i] - } - luaCtx.Set("params", paramMap) - defer func() { - for k := range paramMap { - delete(paramMap, k) - } - s.paramsPool.Put(paramMap) - }() - } else { - luaCtx.Set("params", emptyMap) - } - - // Parse form data for POST/PUT/PATCH - if bytes.Equal(methodBytes, methodPOST) || - bytes.Equal(methodBytes, methodPUT) || - bytes.Equal(methodBytes, methodPATCH) { - if formData, err := ParseForm(ctx); err == nil { - luaCtx.Set("form", formData) - } else { - if s.debugMode { - logger.Warnf("Error parsing form: %v", err) - } - luaCtx.Set("form", emptyMap) - } - } else { - luaCtx.Set("form", emptyMap) - } - - response, err := s.luaRunner.Run(bytecode, luaCtx, scriptPath) + // Execute Lua script + response, err := s.luaRunner.ExecuteHTTP(bytecode, ctx, params, session) if err != nil { logger.Errorf("Lua execution error: %v", err) - s.sendError(ctx, fasthttp.StatusInternalServerError, pathBytes, err) + s.send500(ctx, err) + s.logRequest(ctx, method, path, time.Since(start)) return } - // Handle session updates including flash data - if len(response.SessionData) > 0 { - if _, clearAll := response.SessionData["__clear_all"]; clearAll { + // Apply response + s.applyResponse(ctx, response, session) + runner.ReleaseResponse(response) + + s.logRequest(ctx, method, path, time.Since(start)) +} + +func (s *Server) applyResponse(ctx *fasthttp.RequestCtx, resp *runner.Response, session *sessions.Session) { + // Handle session updates + if len(resp.SessionData) > 0 { + if _, clearAll := resp.SessionData["__clear_all"]; clearAll { session.Clear() - session.ClearFlash() // Also clear flash data - delete(response.SessionData, "__clear_all") + session.ClearFlash() + delete(resp.SessionData, "__clear_all") } - for k, v := range response.SessionData { + for k, v := range resp.SessionData { if v == "__DELETE__" { session.Delete(k) } else { @@ -273,91 +176,61 @@ func (s *Server) handleLuaRoute(ctx *fasthttp.RequestCtx, bytecode []byte, scrip } } - // Handle flash data from response - if flashData, ok := response.Metadata["flash"].(map[string]any); ok { + // Handle flash data + if flashData, ok := resp.Metadata["flash"].(map[string]any); ok { for k, v := range flashData { if err := session.FlashSafe(k, v); err != nil && s.debugMode { logger.Warnf("Error setting flash data %s: %v", k, err) } } - delete(response.Metadata, "flash") // Remove from metadata after processing } + // Apply session cookie s.sessionManager.ApplySessionCookie(ctx, session) - runner.ApplyResponse(response, ctx) - runner.ReleaseResponse(response) + + // Apply HTTP response + runner.ApplyResponse(resp, ctx) } -func (s *Server) send404(ctx *fasthttp.RequestCtx, pathBytes []byte) { +func (s *Server) send404(ctx *fasthttp.RequestCtx) { ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusNotFound) - - // Use cached 404 for common case - if len(pathBytes) == 1 && pathBytes[0] == '/' { - s.errorCacheMu.RLock() - ctx.SetBody(s.cached404) - s.errorCacheMu.RUnlock() - } else { - ctx.SetBody([]byte(utils.NotFoundPage(s.errorConfig, string(pathBytes)))) - } + cacheMu.RLock() + ctx.SetBody(cached404) + cacheMu.RUnlock() } -func (s *Server) sendError(ctx *fasthttp.RequestCtx, status int, pathBytes []byte, err error) { +func (s *Server) send500(ctx *fasthttp.RequestCtx, err error) { ctx.SetContentType("text/html; charset=utf-8") - ctx.SetStatusCode(status) + ctx.SetStatusCode(fasthttp.StatusInternalServerError) if err == nil { - s.errorCacheMu.RLock() - ctx.SetBody(s.cached500) - s.errorCacheMu.RUnlock() - return - } - - var errorMessage string - if luaErr, ok := err.(*luajit.LuaError); ok { - // Use just the message if stack trace is empty - if luaErr.StackTrace == "" { - errorMessage = luaErr.Message - } else { - errorMessage = err.Error() // Full error with stack trace - } + cacheMu.RLock() + ctx.SetBody(cached500) + cacheMu.RUnlock() } else { - errorMessage = err.Error() + errorConfig := utils.ErrorPageConfig{ + OverrideDir: s.cfg.Dirs.Override, + DebugMode: s.debugMode, + } + ctx.SetBody([]byte(utils.InternalErrorPage(errorConfig, string(ctx.Path()), err.Error()))) } - - ctx.SetBody([]byte(utils.InternalErrorPage(s.errorConfig, string(pathBytes), errorMessage))) } func (s *Server) handleDebugStats(ctx *fasthttp.RequestCtx) { stats := utils.CollectSystemStats(s.cfg) - routeCount, bytecodeBytes := s.luaRouter.GetRouteStats() stats.Components = utils.ComponentStats{ - RouteCount: routeCount, - BytecodeBytes: bytecodeBytes, - SessionStats: sessions.GlobalSessionManager.GetCacheStats(), + RouteCount: 0, // TODO: Get from router + BytecodeBytes: 0, // TODO: Get from router + SessionStats: s.sessionManager.GetCacheStats(), } ctx.SetContentType("text/html; charset=utf-8") ctx.SetStatusCode(fasthttp.StatusOK) ctx.SetBody([]byte(utils.DebugStatsPage(stats))) } -// SetStaticCaching enables/disables static file caching -func (s *Server) SetStaticCaching(duration time.Duration) { - if s.staticFS != nil { - s.staticFS.CacheDuration = duration - s.staticHandler = s.staticFS.NewRequestHandler() +func (s *Server) logRequest(ctx *fasthttp.RequestCtx, method, path string, duration time.Duration) { + if s.cfg.Server.HTTPLogging { + logger.Request(ctx.Response.StatusCode(), method, path, duration) } } - -// GetStaticPrefix returns the URL prefix for static files -func (s *Server) GetStaticPrefix() string { - return s.staticPrefix -} - -// UpdateErrorCache refreshes cached error pages -func (s *Server) UpdateErrorCache() { - s.errorCacheMu.Lock() - s.cached404 = []byte(utils.NotFoundPage(s.errorConfig, "")) - s.cached500 = []byte(utils.InternalErrorPage(s.errorConfig, "", "Internal Server Error")) - s.errorCacheMu.Unlock() -} diff --git a/http/utils.go b/http/utils.go index 94e6bde..07d3a51 100644 --- a/http/utils.go +++ b/http/utils.go @@ -10,14 +10,11 @@ import ( "github.com/valyala/fasthttp" ) -var ( - emptyMap = make(map[string]any) - formDataPool = sync.Pool{ - New: func() any { - return make(map[string]any, 16) - }, - } -) +var formDataPool = sync.Pool{ + New: func() any { + return make(map[string]any, 16) + }, +} func QueryToLua(ctx *fasthttp.RequestCtx) map[string]any { args := ctx.QueryArgs() diff --git a/moonshark.go b/moonshark.go index a1f8380..c1d0261 100644 --- a/moonshark.go +++ b/moonshark.go @@ -8,6 +8,7 @@ import ( "os" "os/signal" "path/filepath" + "runtime" "strconv" "syscall" "time" @@ -28,7 +29,7 @@ import ( type Moonshark struct { Config *config.Config - LuaRouter *router.LuaRouter + LuaRouter *router.Router LuaRunner *runner.Runner HTTPServer *http.Server cleanupFuncs []func() error @@ -95,6 +96,9 @@ func newMoonshark(cfg *config.Config, debug, scriptMode bool) (*Moonshark, error if scriptMode { poolSize = 1 } + if poolSize == 0 { + poolSize = runtime.GOMAXPROCS(0) + } // Initialize runner first (needed for both modes) if err := s.initRunner(poolSize); err != nil { @@ -112,16 +116,8 @@ func newMoonshark(cfg *config.Config, debug, scriptMode bool) (*Moonshark, error } s.setupWatchers() - s.HTTPServer = http.New(s.LuaRouter, s.LuaRunner, cfg, debug) - // Set caching based on debug mode - if cfg.Server.Debug { - s.HTTPServer.SetStaticCaching(0) - } else { - s.HTTPServer.SetStaticCaching(time.Hour) - } - // Log static directory status if dirExists(cfg.Dirs.Static) { logger.Infof("Static files enabled: %s", color.Yellow(cfg.Dirs.Static)) @@ -152,12 +148,7 @@ func (s *Moonshark) initRunner(poolSize int) error { sessions.GlobalSessionManager.SetCookieOptions("MoonsharkSID", "/", "", false, true, 86400) var err error - s.LuaRunner, err = runner.NewRunner( - runner.WithPoolSize(poolSize), - runner.WithLibDirs(s.Config.Dirs.Libs...), - runner.WithFsDir(s.Config.Dirs.FS), - runner.WithDataDir(s.Config.Dirs.Data), - ) + s.LuaRunner, err = runner.NewRunner(poolSize, s.Config.Dirs.Data, s.Config.Dirs.FS) if err != nil { return fmt.Errorf("lua runner init failed: %v", err) } @@ -172,18 +163,9 @@ func (s *Moonshark) initRouter() error { } var err error - s.LuaRouter, err = router.NewLuaRouter(s.Config.Dirs.Routes) + s.LuaRouter, err = router.New(s.Config.Dirs.Routes) if err != nil { - if errors.Is(err, router.ErrRoutesCompilationErrors) { - logger.Warnf("Some routes failed to compile") - if failedRoutes := s.LuaRouter.ReportFailedRoutes(); len(failedRoutes) > 0 { - for _, re := range failedRoutes { - logger.Errorf("Route %s %s: %v", re.Method, re.Path, re.Err) - } - } - } else { - return fmt.Errorf("lua router init failed: %v", err) - } + return fmt.Errorf("lua router init failed: %v", err) } logger.Infof("LuaRouter is g2g! %s", color.Yellow(s.Config.Dirs.Routes)) diff --git a/router/build.go b/router/build.go deleted file mode 100644 index 8f75dbd..0000000 --- a/router/build.go +++ /dev/null @@ -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 - }) -} diff --git a/router/cache.go b/router/cache.go deleted file mode 100644 index 6bc8cc5..0000000 --- a/router/cache.go +++ /dev/null @@ -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, - } -} diff --git a/router/compile.go b/router/compile.go deleted file mode 100644 index ef83b21..0000000 --- a/router/compile.go +++ /dev/null @@ -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 -} diff --git a/router/errors.go b/router/errors.go deleted file mode 100644 index 914c3b4..0000000 --- a/router/errors.go +++ /dev/null @@ -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() -} diff --git a/router/match.go b/router/match.go deleted file mode 100644 index c55ff6f..0000000 --- a/router/match.go +++ /dev/null @@ -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)) -} diff --git a/router/node.go b/router/node.go deleted file mode 100644 index 01af455..0000000 --- a/router/node.go +++ /dev/null @@ -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 -} diff --git a/router/params.go b/router/params.go deleted file mode 100644 index f32f4b4..0000000 --- a/router/params.go +++ /dev/null @@ -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++ - } -} diff --git a/router/router.go b/router/router.go index df8429f..590c9d1 100644 --- a/router/router.go +++ b/router/router.go @@ -3,44 +3,54 @@ package router import ( "errors" "os" + "path/filepath" + "strings" "sync" - "time" luajit "git.sharkk.net/Sky/LuaJIT-to-Go" "github.com/VictoriaMetrics/fastcache" ) -// Default cache sizes -const ( - defaultBytecodeMaxBytes = 32 * 1024 * 1024 // 32MB for bytecode cache - defaultRouteMaxBytes = 8 * 1024 * 1024 // 8MB for route match cache -) - -// LuaRouter is a filesystem-based HTTP router for Lua files -type LuaRouter struct { - routesDir string // Root directory containing route files - routes map[string]*node // Method -> route tree - failedRoutes map[string]*RouteError // Track failed routes - mu sync.RWMutex // Lock for concurrent access to routes - - routeCache *fastcache.Cache // Cache for route lookups - bytecodeCache *fastcache.Cache // Cache for compiled bytecode - - // Middleware tracking for path hierarchy - middlewareFiles map[string][]string // filesystem path -> middleware file paths - - // Caching fields - middlewareCache map[string][]byte // path -> content - sourceCache map[string][]byte // combined source cache key -> compiled bytecode - sourceMtimes map[string]time.Time // track modification times - - // Shared Lua state for compilation - compileState *luajit.State - compileStateMu sync.Mutex // Protect concurrent access to Lua state +// node represents a node in the radix trie +type node struct { + segment string + bytecode []byte + scriptPath string + children []*node + isDynamic bool + isWildcard bool + maxParams uint8 } -// NewLuaRouter creates a new LuaRouter instance -func NewLuaRouter(routesDir string) (*LuaRouter, error) { +// Router is a filesystem-based HTTP router for Lua files with bytecode caching +type Router struct { + routesDir string + get, post, put, patch, delete *node + bytecodeCache *fastcache.Cache + compileState *luajit.State + compileMu sync.Mutex + paramsBuffer []string + middlewareFiles map[string][]string // filesystem path -> middleware file paths +} + +// Params holds URL parameters +type Params struct { + Keys []string + Values []string +} + +// Get returns a parameter value by name +func (p *Params) Get(name string) string { + for i, key := range p.Keys { + if key == name && i < len(p.Values) { + return p.Values[i] + } + } + return "" +} + +// New creates a new Router instance +func New(routesDir string) (*Router, error) { info, err := os.Stat(routesDir) if err != nil { return nil, err @@ -49,108 +59,443 @@ func NewLuaRouter(routesDir string) (*LuaRouter, error) { return nil, errors.New("routes path is not a directory") } - // Create shared Lua state compileState := luajit.New() if compileState == nil { return nil, errors.New("failed to create Lua compile state") } - r := &LuaRouter{ + r := &Router{ routesDir: routesDir, - routes: make(map[string]*node), - failedRoutes: make(map[string]*RouteError), - middlewareFiles: make(map[string][]string), - routeCache: fastcache.New(defaultRouteMaxBytes), - bytecodeCache: fastcache.New(defaultBytecodeMaxBytes), - middlewareCache: make(map[string][]byte), - sourceCache: make(map[string][]byte), - sourceMtimes: make(map[string]time.Time), + get: &node{}, + post: &node{}, + put: &node{}, + patch: &node{}, + delete: &node{}, + bytecodeCache: fastcache.New(32 * 1024 * 1024), // 32MB compileState: compileState, + paramsBuffer: make([]string, 64), + middlewareFiles: make(map[string][]string), } - methods := []string{"GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS", "HEAD"} - for _, method := range methods { - r.routes[method] = &node{ - staticChild: make(map[string]*node), - } - } - - err = r.buildRoutes() - - if len(r.failedRoutes) > 0 { - return r, ErrRoutesCompilationErrors - } - - return r, err + return r, r.buildRoutes() } -// Refresh rebuilds the router by rescanning the routes directory -func (r *LuaRouter) Refresh() error { - r.mu.Lock() - defer r.mu.Unlock() - - for method := range r.routes { - r.routes[method] = &node{ - staticChild: make(map[string]*node), - } +// methodNode returns the root node for a method +func (r *Router) methodNode(method string) *node { + switch method { + case "GET": + return r.get + case "POST": + return r.post + case "PUT": + return r.put + case "PATCH": + return r.patch + case "DELETE": + return r.delete + default: + return nil } +} - r.failedRoutes = make(map[string]*RouteError) +// buildRoutes scans the routes directory and builds the routing tree +func (r *Router) buildRoutes() error { r.middlewareFiles = make(map[string][]string) - r.middlewareCache = make(map[string][]byte) - r.sourceCache = make(map[string][]byte) - r.sourceMtimes = make(map[string]time.Time) - err := r.buildRoutes() + // First pass: collect all middleware files + err := filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") { + return err + } - if len(r.failedRoutes) > 0 { - return ErrRoutesCompilationErrors + if strings.TrimSuffix(info.Name(), ".lua") == "middleware" { + relDir, err := filepath.Rel(r.routesDir, filepath.Dir(path)) + if err != nil { + return err + } + + fsPath := "/" + if relDir != "." { + fsPath = "/" + strings.ReplaceAll(relDir, "\\", "/") + } + + r.middlewareFiles[fsPath] = append(r.middlewareFiles[fsPath], path) + } + + return nil + }) + + if err != nil { + return err } - return err + // Second pass: build routes + return filepath.Walk(r.routesDir, func(path string, info os.FileInfo, err error) error { + if err != nil || info.IsDir() || !strings.HasSuffix(info.Name(), ".lua") { + return err + } + + fileName := strings.TrimSuffix(info.Name(), ".lua") + + // Skip middleware files + if fileName == "middleware" { + return nil + } + + // Get relative path from routes directory + relPath, err := filepath.Rel(r.routesDir, path) + if err != nil { + return err + } + + // Get filesystem path (includes groups) + fsPath := "/" + strings.ReplaceAll(filepath.Dir(relPath), "\\", "/") + if fsPath == "/." { + fsPath = "/" + } + + // Get URL path (excludes groups) + urlPath := r.parseURLPath(fsPath) + + // Handle method files (get.lua, post.lua, etc.) + method := strings.ToUpper(fileName) + root := r.methodNode(method) + if root != nil { + return r.addRoute(root, urlPath, fsPath, path) + } + + // Handle index files - register for all methods + if fileName == "index" { + for _, method := range []string{"GET", "POST", "PUT", "PATCH", "DELETE"} { + if root := r.methodNode(method); root != nil { + if err := r.addRoute(root, urlPath, fsPath, path); err != nil { + return err + } + } + } + return nil + } + + // Handle named route files - register as GET by default + namedPath := urlPath + if urlPath == "/" { + namedPath = "/" + fileName + } else { + namedPath = urlPath + "/" + fileName + } + return r.addRoute(r.get, namedPath, fsPath, path) + }) } -// ReportFailedRoutes returns a list of routes that failed to compile -func (r *LuaRouter) ReportFailedRoutes() []*RouteError { - r.mu.RLock() - defer r.mu.RUnlock() +// parseURLPath strips group segments from filesystem path +func (r *Router) parseURLPath(fsPath string) string { + segments := strings.Split(strings.Trim(fsPath, "/"), "/") + var urlSegments []string - result := make([]*RouteError, 0, len(r.failedRoutes)) - for _, re := range r.failedRoutes { - result = append(result, re) + for _, segment := range segments { + if segment == "" { + continue + } + // Skip group segments (enclosed in parentheses) + if strings.HasPrefix(segment, "(") && strings.HasSuffix(segment, ")") { + continue + } + urlSegments = append(urlSegments, segment) } - return result + if len(urlSegments) == 0 { + return "/" + } + return "/" + strings.Join(urlSegments, "/") } -// Close cleans up the router and its resources -func (r *LuaRouter) Close() { - r.compileStateMu.Lock() +// getMiddlewareChain returns middleware files that apply to the given filesystem path +func (r *Router) getMiddlewareChain(fsPath string) []string { + var chain []string + + pathParts := strings.Split(strings.Trim(fsPath, "/"), "/") + if pathParts[0] == "" { + pathParts = []string{} + } + + // Add root middleware + if mw, exists := r.middlewareFiles["/"]; exists { + chain = append(chain, mw...) + } + + // Add middleware from each path level (including groups) + currentPath := "" + for _, part := range pathParts { + currentPath += "/" + part + if mw, exists := r.middlewareFiles[currentPath]; exists { + chain = append(chain, mw...) + } + } + + return chain +} + +// buildCombinedSource combines middleware and handler source +func (r *Router) buildCombinedSource(fsPath, scriptPath string) (string, error) { + var combined strings.Builder + + // Add middleware in order + middlewareChain := r.getMiddlewareChain(fsPath) + for _, mwPath := range middlewareChain { + content, err := os.ReadFile(mwPath) + if err != nil { + return "", err + } + combined.WriteString("-- Middleware: ") + combined.WriteString(mwPath) + combined.WriteString("\n") + combined.Write(content) + combined.WriteString("\n") + } + + // Add main handler + content, err := os.ReadFile(scriptPath) + if err != nil { + return "", err + } + combined.WriteString("-- Handler: ") + combined.WriteString(scriptPath) + combined.WriteString("\n") + combined.Write(content) + + return combined.String(), nil +} + +// addRoute adds a new route to the trie with bytecode compilation +func (r *Router) addRoute(root *node, urlPath, fsPath, scriptPath string) error { + // Build combined source with middleware + combinedSource, err := r.buildCombinedSource(fsPath, scriptPath) + if err != nil { + return err + } + + // Compile bytecode + r.compileMu.Lock() + bytecode, err := r.compileState.CompileBytecode(combinedSource, scriptPath) + r.compileMu.Unlock() + + if err != nil { + return err + } + + // Cache bytecode + cacheKey := hashString(scriptPath) + r.bytecodeCache.Set(uint64ToBytes(cacheKey), bytecode) + + if urlPath == "/" { + root.bytecode = bytecode + root.scriptPath = scriptPath + return nil + } + + current := root + pos := 0 + paramCount := uint8(0) + + for { + seg, newPos, more := readSegment(urlPath, pos) + if seg == "" { + break + } + + isDyn := len(seg) > 2 && seg[0] == '[' && seg[len(seg)-1] == ']' + isWC := len(seg) > 0 && seg[0] == '*' + + if isWC && more { + return errors.New("wildcard must be the last segment") + } + + if isDyn || isWC { + paramCount++ + } + + // Find or create child + var child *node + for _, c := range current.children { + if c.segment == seg { + child = c + break + } + } + + if child == nil { + child = &node{ + segment: seg, + isDynamic: isDyn, + isWildcard: isWC, + } + current.children = append(current.children, child) + } + + if child.maxParams < paramCount { + child.maxParams = paramCount + } + + current = child + pos = newPos + } + + current.bytecode = bytecode + current.scriptPath = scriptPath + return nil +} + +// readSegment extracts the next path segment +func readSegment(path string, start int) (segment string, end int, hasMore bool) { + if start >= len(path) { + return "", start, false + } + if path[start] == '/' { + start++ + } + if start >= len(path) { + return "", start, false + } + end = start + for end < len(path) && path[end] != '/' { + end++ + } + return path[start:end], end, end < len(path) +} + +// Lookup finds bytecode and parameters for a method and path +func (r *Router) Lookup(method, path string) ([]byte, *Params, bool) { + root := r.methodNode(method) + if root == nil { + return nil, nil, false + } + + if path == "/" { + if root.bytecode != nil { + return root.bytecode, &Params{}, true + } + return nil, nil, false + } + + // Prepare params buffer + buffer := r.paramsBuffer + if cap(buffer) < int(root.maxParams) { + buffer = make([]string, root.maxParams) + r.paramsBuffer = buffer + } + buffer = buffer[:0] + + var keys []string + bytecode, paramCount, found := r.match(root, path, 0, &buffer, &keys) + if !found { + return nil, nil, false + } + + params := &Params{ + Keys: keys[:paramCount], + Values: buffer[:paramCount], + } + + return bytecode, params, true +} + +// match traverses the trie to find bytecode +func (r *Router) match(current *node, path string, start int, params *[]string, keys *[]string) ([]byte, int, bool) { + paramCount := 0 + + // Check wildcard first + for _, c := range current.children { + if c.isWildcard { + rem := path[start:] + if len(rem) > 0 && rem[0] == '/' { + rem = rem[1:] + } + *params = append(*params, rem) + *keys = append(*keys, strings.TrimPrefix(c.segment, "*")) + return c.bytecode, 1, c.bytecode != nil + } + } + + seg, pos, more := readSegment(path, start) + if seg == "" { + return current.bytecode, 0, current.bytecode != nil + } + + for _, c := range current.children { + if c.segment == seg || c.isDynamic { + if c.isDynamic { + *params = append(*params, seg) + paramName := c.segment[1 : len(c.segment)-1] // Remove [ ] + *keys = append(*keys, paramName) + paramCount++ + } + + if !more { + return c.bytecode, paramCount, c.bytecode != nil + } + + bytecode, nestedCount, ok := r.match(c, path, pos, params, keys) + if ok { + return bytecode, paramCount + nestedCount, true + } + + // Backtrack on failure + if c.isDynamic { + *params = (*params)[:len(*params)-1] + *keys = (*keys)[:len(*keys)-1] + } + } + } + + return nil, 0, false +} + +// GetBytecode gets cached bytecode by script path +func (r *Router) GetBytecode(scriptPath string) []byte { + cacheKey := hashString(scriptPath) + return r.bytecodeCache.Get(nil, uint64ToBytes(cacheKey)) +} + +// Refresh rebuilds the router +func (r *Router) Refresh() error { + r.get = &node{} + r.post = &node{} + r.put = &node{} + r.patch = &node{} + r.delete = &node{} + r.middlewareFiles = make(map[string][]string) + r.bytecodeCache.Reset() + return r.buildRoutes() +} + +// Close cleans up resources +func (r *Router) Close() { + r.compileMu.Lock() if r.compileState != nil { r.compileState.Close() r.compileState = nil } - r.compileStateMu.Unlock() + r.compileMu.Unlock() } -// GetRouteStats returns statistics about the router -func (r *LuaRouter) GetRouteStats() (int, int64) { - r.mu.RLock() - defer r.mu.RUnlock() - - routeCount := 0 - bytecodeBytes := int64(0) - - for _, root := range r.routes { - count, bytes := countNodesAndBytecode(root) - routeCount += count - bytecodeBytes += bytes +// Helper functions from cache.go +func hashString(s string) uint64 { + h := uint64(5381) + for i := 0; i < len(s); i++ { + h = ((h << 5) + h) + uint64(s[i]) } - - return routeCount, bytecodeBytes + return h } -type NodeWithError struct { - ScriptPath string - Error error +func uint64ToBytes(n uint64) []byte { + b := make([]byte, 8) + b[0] = byte(n) + b[1] = byte(n >> 8) + b[2] = byte(n >> 16) + b[3] = byte(n >> 24) + b[4] = byte(n >> 32) + b[5] = byte(n >> 40) + b[6] = byte(n >> 48) + b[7] = byte(n >> 56) + return b } diff --git a/runner/runner.go b/runner/runner.go index e6dca13..867e17d 100644 --- a/runner/runner.go +++ b/runner/runner.go @@ -1,237 +1,106 @@ +// runner.go - Simplified interface package runner import ( - "Moonshark/runner/lualibs" - "Moonshark/utils/color" - "Moonshark/utils/logger" - "context" "errors" "fmt" "os" "path/filepath" "runtime" - "strconv" "strings" "sync" "sync/atomic" "time" + "Moonshark/router" + "Moonshark/runner/lualibs" + "Moonshark/sessions" + "Moonshark/utils/logger" + luajit "git.sharkk.net/Sky/LuaJIT-to-Go" "github.com/goccy/go-json" "github.com/valyala/bytebufferpool" "github.com/valyala/fasthttp" ) -// Common errors +var emptyMap = make(map[string]any) + var ( ErrRunnerClosed = errors.New("lua runner is closed") - ErrInitFailed = errors.New("initialization failed") - ErrStateNotReady = errors.New("lua state not ready") ErrTimeout = errors.New("operation timed out") + ErrStateNotReady = errors.New("lua state not ready") ) -// RunnerOption defines a functional option for configuring the Runner -type RunnerOption func(*Runner) - -// State wraps a Lua state with its sandbox type State struct { - L *luajit.State // The Lua state - sandbox *Sandbox // Associated sandbox - index int // Index for debugging - inUse atomic.Bool // Whether the state is currently in use + L *luajit.State + sandbox *Sandbox + index int + inUse atomic.Bool } -// Runner runs Lua scripts using a pool of Lua states type Runner struct { - states []*State // All states managed by this runner - statePool chan int // Pool of available state indexes - poolSize int // Size of the state pool - moduleLoader *ModuleLoader // Module loader - dataDir string // Data directory for SQLite databases - fsDir string // Virtual filesystem directory - isRunning atomic.Bool // Whether the runner is active - mu sync.RWMutex // Mutex for thread safety - scriptDir string // Current script directory + states []*State + statePool chan int + poolSize int + moduleLoader *ModuleLoader + isRunning atomic.Bool + mu sync.RWMutex + scriptDir string + + // Pre-allocated pools for HTTP processing + ctxPool sync.Pool + paramsPool sync.Pool } -// WithPoolSize sets the state pool size -func WithPoolSize(size int) RunnerOption { - return func(r *Runner) { - if size > 0 { - r.poolSize = size - } - } -} - -// WithLibDirs sets additional library directories -func WithLibDirs(dirs ...string) RunnerOption { - return func(r *Runner) { - if r.moduleLoader == nil { - r.moduleLoader = NewModuleLoader(&ModuleConfig{ - LibDirs: dirs, - }) - } else { - r.moduleLoader.config.LibDirs = dirs - } - } -} - -// WithDataDir sets the data directory for SQLite databases -func WithDataDir(dataDir string) RunnerOption { - return func(r *Runner) { - if dataDir != "" { - r.dataDir = dataDir - } - } -} - -// WithFsDir sets the virtual filesystem directory -func WithFsDir(fsDir string) RunnerOption { - return func(r *Runner) { - if fsDir != "" { - r.fsDir = fsDir - } - } -} - -// NewRunner creates a new Runner with a pool of states -func NewRunner(options ...RunnerOption) (*Runner, error) { - // Default configuration - runner := &Runner{ - poolSize: runtime.GOMAXPROCS(0), - dataDir: "data", - fsDir: "fs", +func NewRunner(poolSize int, dataDir, fsDir string) (*Runner, error) { + if poolSize <= 0 { + poolSize = runtime.GOMAXPROCS(0) } - // Apply options - for _, opt := range options { - opt(runner) + r := &Runner{ + poolSize: poolSize, + moduleLoader: NewModuleLoader(&ModuleConfig{}), + ctxPool: sync.Pool{ + New: func() any { return make(map[string]any, 8) }, + }, + paramsPool: sync.Pool{ + New: func() any { return make(map[string]any, 4) }, + }, } - // Set up module loader if not already initialized - if runner.moduleLoader == nil { - config := &ModuleConfig{ - ScriptDir: "", - LibDirs: []string{}, - } - runner.moduleLoader = NewModuleLoader(config) - } + lualibs.InitSQLite(dataDir) + lualibs.InitFS(fsDir) + lualibs.SetSQLitePoolSize(poolSize) - lualibs.InitSQLite(runner.dataDir) - lualibs.InitFS(runner.fsDir) + r.states = make([]*State, poolSize) + r.statePool = make(chan int, poolSize) - lualibs.SetSQLitePoolSize(runner.poolSize) - - // Initialize states and pool - runner.states = make([]*State, runner.poolSize) - runner.statePool = make(chan int, runner.poolSize) - - // Create and initialize all states - if err := runner.initializeStates(); err != nil { + if err := r.initStates(); err != nil { lualibs.CleanupSQLite() - runner.Close() return nil, err } - runner.isRunning.Store(true) - return runner, nil + r.isRunning.Store(true) + return r, nil } -// initializeStates creates and initializes all states in the pool -func (r *Runner) initializeStates() error { - logger.Infof("[LuaRunner] Creating %s states...", color.Yellow(strconv.Itoa(r.poolSize))) +// Single entry point for HTTP execution +func (r *Runner) ExecuteHTTP(bytecode []byte, httpCtx *fasthttp.RequestCtx, + params *router.Params, session *sessions.Session) (*Response, error) { - for i := range r.poolSize { - state, err := r.createState(i) - if err != nil { - return err - } - - r.states[i] = state - r.statePool <- i // Add index to the pool - } - - return nil -} - -// createState initializes a new Lua state -func (r *Runner) createState(index int) (*State, error) { - verbose := index == 0 - if verbose { - logger.Debugf("Creating Lua state %d", index) - } - - L := luajit.New(true) // Explicitly open standard libraries - if L == nil { - return nil, errors.New("failed to create Lua state") - } - - sb := NewSandbox() - - // Set up sandbox - if err := sb.Setup(L, verbose); err != nil { - L.Cleanup() - L.Close() - return nil, ErrInitFailed - } - - // Set up module loader - if err := r.moduleLoader.SetupRequire(L); err != nil { - L.Cleanup() - L.Close() - return nil, ErrInitFailed - } - - // Preload modules - if err := r.moduleLoader.PreloadModules(L); err != nil { - L.Cleanup() - L.Close() - return nil, errors.New("failed to preload modules") - } - - if verbose { - logger.Debugf("Lua state %d initialized successfully", index) - } - - return &State{ - L: L, - sandbox: sb, - index: index, - }, nil -} - -// Execute runs a script in a sandbox with context -func (r *Runner) Execute(ctx context.Context, bytecode []byte, execCtx *Context, scriptPath string) (*Response, error) { if !r.isRunning.Load() { return nil, ErrRunnerClosed } - // Set script directory if provided - if scriptPath != "" { - r.mu.Lock() - r.scriptDir = filepath.Dir(scriptPath) - r.moduleLoader.SetScriptDir(r.scriptDir) - r.mu.Unlock() - } - - // Get a state from the pool + // Get state with timeout var stateIndex int select { case stateIndex = <-r.statePool: - // Got a state - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(1 * time.Second): + case <-time.After(time.Second): return nil, ErrTimeout } state := r.states[stateIndex] - if state == nil { - r.statePool <- stateIndex - return nil, ErrStateNotReady - } - - // Use atomic operations state.inUse.Store(true) defer func() { @@ -240,26 +109,148 @@ func (r *Runner) Execute(ctx context.Context, bytecode []byte, execCtx *Context, select { case r.statePool <- stateIndex: default: - // Pool is full or closed, state will be cleaned up by Close() } } }() - // Execute in sandbox - response, err := state.sandbox.Execute(state.L, bytecode, execCtx) - if err != nil { + // Build Lua context directly from HTTP request + luaCtx := r.buildHTTPContext(httpCtx, params, session) + defer r.releaseHTTPContext(luaCtx) + + return state.sandbox.Execute(state.L, bytecode, luaCtx) +} + +// Build Lua context from HTTP request +func (r *Runner) buildHTTPContext(ctx *fasthttp.RequestCtx, params *router.Params, session *sessions.Session) *Context { + luaCtx := NewContext() + + // Basic request info + luaCtx.Set("method", string(ctx.Method())) + luaCtx.Set("path", string(ctx.Path())) + luaCtx.Set("host", string(ctx.Host())) + + // Headers + headers := r.ctxPool.Get().(map[string]any) + ctx.Request.Header.VisitAll(func(key, value []byte) { + headers[string(key)] = string(value) + }) + luaCtx.Set("headers", headers) + + // Route parameters + if params != nil && len(params.Keys) > 0 { + paramMap := r.paramsPool.Get().(map[string]any) + for i, key := range params.Keys { + if i < len(params.Values) { + paramMap[key] = params.Values[i] + } + } + luaCtx.Set("params", paramMap) + } else { + luaCtx.Set("params", emptyMap) + } + + // Form data for POST/PUT/PATCH + method := ctx.Method() + if string(method) == "POST" || string(method) == "PUT" || string(method) == "PATCH" { + if formData := parseForm(ctx); formData != nil { + luaCtx.Set("form", formData) + } else { + luaCtx.Set("form", emptyMap) + } + } else { + luaCtx.Set("form", emptyMap) + } + + // Session data + sessionMap := r.ctxPool.Get().(map[string]any) + session.AdvanceFlash() + sessionMap["id"] = session.ID + + if !session.IsEmpty() { + sessionMap["data"] = session.GetAll() + sessionMap["flash"] = session.GetAllFlash() + } else { + sessionMap["data"] = emptyMap + sessionMap["flash"] = emptyMap + } + luaCtx.Set("session", sessionMap) + + // Environment variables + if envMgr := lualibs.GetGlobalEnvManager(); envMgr != nil { + luaCtx.Set("env", envMgr.GetAll()) + } + + return luaCtx +} + +func (r *Runner) releaseHTTPContext(luaCtx *Context) { + // Return pooled maps + if headers, ok := luaCtx.Get("headers").(map[string]any); ok { + for k := range headers { + delete(headers, k) + } + r.ctxPool.Put(headers) + } + + if params, ok := luaCtx.Get("params").(map[string]any); ok && len(params) > 0 { + for k := range params { + delete(params, k) + } + r.paramsPool.Put(params) + } + + if sessionMap, ok := luaCtx.Get("session").(map[string]any); ok { + for k := range sessionMap { + delete(sessionMap, k) + } + r.ctxPool.Put(sessionMap) + } + + luaCtx.Release() +} + +func (r *Runner) initStates() error { + logger.Infof("[LuaRunner] Creating %d states...", r.poolSize) + + for i := range r.poolSize { + state, err := r.createState(i) + if err != nil { + return err + } + r.states[i] = state + r.statePool <- i + } + return nil +} + +func (r *Runner) createState(index int) (*State, error) { + L := luajit.New(true) + if L == nil { + return nil, errors.New("failed to create Lua state") + } + + sb := NewSandbox() + if err := sb.Setup(L, index == 0); err != nil { + L.Cleanup() + L.Close() return nil, err } - return response, nil + if err := r.moduleLoader.SetupRequire(L); err != nil { + L.Cleanup() + L.Close() + return nil, err + } + + if err := r.moduleLoader.PreloadModules(L); err != nil { + L.Cleanup() + L.Close() + return nil, err + } + + return &State{L: L, sandbox: sb, index: index}, nil } -// Run executes a Lua script with immediate context -func (r *Runner) Run(bytecode []byte, execCtx *Context, scriptPath string) (*Response, error) { - return r.Execute(context.Background(), bytecode, execCtx, scriptPath) -} - -// Close gracefully shuts down the Runner func (r *Runner) Close() error { r.mu.Lock() defer r.mu.Unlock() @@ -267,22 +258,21 @@ func (r *Runner) Close() error { if !r.isRunning.Load() { return ErrRunnerClosed } - r.isRunning.Store(false) - // Drain all states from the pool + // Drain pool for { select { case <-r.statePool: default: - goto waitForInUse + goto cleanup } } -waitForInUse: - // Wait for in-use states to finish (with timeout) +cleanup: + // Wait for states to finish timeout := time.Now().Add(10 * time.Second) - for { + for time.Now().Before(timeout) { allIdle := true for _, state := range r.states { if state != nil && state.inUse.Load() { @@ -290,25 +280,15 @@ waitForInUse: break } } - if allIdle { break } - - if time.Now().After(timeout) { - logger.Warnf("Timeout waiting for states to finish during shutdown, forcing close") - break - } - time.Sleep(10 * time.Millisecond) } - // Now safely close all states + // Close states for i, state := range r.states { if state != nil { - if state.inUse.Load() { - logger.Warnf("Force closing state %d that is still in use", i) - } state.L.Cleanup() state.L.Close() r.states[i] = nil @@ -317,74 +297,33 @@ waitForInUse: lualibs.CleanupFS() lualibs.CleanupSQLite() - - logger.Debugf("Runner closed") return nil } -// RefreshStates rebuilds all states in the pool -func (r *Runner) RefreshStates() error { - r.mu.Lock() - defer r.mu.Unlock() +// parseForm extracts form data from HTTP request +func parseForm(ctx *fasthttp.RequestCtx) map[string]any { + form := make(map[string]any) - if !r.isRunning.Load() { - return ErrRunnerClosed - } + // Parse POST form data + ctx.PostArgs().VisitAll(func(key, value []byte) { + form[string(key)] = string(value) + }) - logger.Infof("Runner is refreshing all states...") - - // Drain all states from the pool - for { - select { - case <-r.statePool: - default: - goto waitForInUse - } - } - -waitForInUse: - // Wait for in-use states to finish (with timeout) - timeout := time.Now().Add(10 * time.Second) - for { - allIdle := true - for _, state := range r.states { - if state != nil && state.inUse.Load() { - allIdle = false - break + // Parse multipart form if present + if multipartForm, err := ctx.MultipartForm(); err == nil { + for key, values := range multipartForm.Value { + if len(values) == 1 { + form[key] = values[0] + } else { + form[key] = values } } - - if allIdle { - break - } - - if time.Now().After(timeout) { - logger.Warnf("Timeout waiting for states to finish, forcing refresh") - break - } - - time.Sleep(10 * time.Millisecond) } - // Now safely destroy all states - for i, state := range r.states { - if state != nil { - if state.inUse.Load() { - logger.Warnf("Force closing state %d that is still in use", i) - } - state.L.Cleanup() - state.L.Close() - r.states[i] = nil - } + if len(form) == 0 { + return nil } - - // Reinitialize all states - if err := r.initializeStates(); err != nil { - return err - } - - logger.Debugf("All states refreshed successfully") - return nil + return form } // NotifyFileChanged alerts the runner about file changes @@ -418,7 +357,6 @@ func (r *Runner) RefreshModule(moduleName string) bool { continue } - // Use the enhanced module loader refresh if err := r.moduleLoader.RefreshModule(state.L, moduleName); err != nil { success = false logger.Debugf("Failed to refresh module %s in state %d: %v", moduleName, state.index, err) @@ -432,63 +370,6 @@ func (r *Runner) RefreshModule(moduleName string) bool { return success } -// RefreshModuleByPath refreshes a module by its file path -func (r *Runner) RefreshModuleByPath(filePath string) bool { - r.mu.RLock() - defer r.mu.RUnlock() - - if !r.isRunning.Load() { - return false - } - - logger.Debugf("Refreshing module by path: %s", filePath) - - success := true - for _, state := range r.states { - if state == nil || state.inUse.Load() { - continue - } - - // Use the enhanced module loader refresh by path - if err := r.moduleLoader.RefreshModuleByPath(state.L, filePath); err != nil { - success = false - logger.Debugf("Failed to refresh module at %s in state %d: %v", filePath, state.index, err) - } - } - - return success -} - -// GetStateCount returns the number of initialized states -func (r *Runner) GetStateCount() int { - r.mu.RLock() - defer r.mu.RUnlock() - - count := 0 - for _, state := range r.states { - if state != nil { - count++ - } - } - - return count -} - -// GetActiveStateCount returns the number of states currently in use -func (r *Runner) GetActiveStateCount() int { - r.mu.RLock() - defer r.mu.RUnlock() - - count := 0 - for _, state := range r.states { - if state != nil && state.inUse.Load() { - count++ - } - } - - return count -} - // RunScriptFile loads, compiles and executes a Lua script file func (r *Runner) RunScriptFile(filePath string) (*Response, error) { if !r.isRunning.Load() { @@ -523,10 +404,10 @@ func (r *Runner) RunScriptFile(filePath string) (*Response, error) { r.mu.Unlock() }() + // Get state from pool var stateIndex int select { case stateIndex = <-r.statePool: - // Got a state case <-time.After(5 * time.Second): return nil, ErrTimeout } @@ -544,24 +425,25 @@ func (r *Runner) RunScriptFile(filePath string) (*Response, error) { if r.isRunning.Load() { select { case r.statePool <- stateIndex: - // State returned to pool default: - // Pool is full or closed } } }() + // Compile script bytecode, err := state.L.CompileBytecode(string(content), filepath.Base(absPath)) if err != nil { return nil, fmt.Errorf("compilation error: %w", err) } + // Create simple context for script execution ctx := NewContext() defer ctx.Release() ctx.Set("_script_path", absPath) ctx.Set("_script_dir", scriptDir) + // Execute script response, err := state.sandbox.Execute(state.L, bytecode, ctx) if err != nil { return nil, fmt.Errorf("execution error: %w", err) diff --git a/watchers/api.go b/watchers/api.go index ca59bf9..f35f9f7 100644 --- a/watchers/api.go +++ b/watchers/api.go @@ -34,7 +34,7 @@ func ShutdownWatcherManager() { } // WatchLuaRouter sets up a watcher for a LuaRouter's routes directory -func WatchLuaRouter(router *router.LuaRouter, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) { +func WatchLuaRouter(router *router.Router, runner *runner.Runner, routesDir string) (*DirectoryWatcher, error) { manager := GetWatcherManager() config := DirectoryWatcherConfig{