http module success 1
This commit is contained in:
parent
a110b93f5c
commit
1753007090
2
go.mod
2
go.mod
@ -8,8 +8,6 @@ require github.com/goccy/go-json v0.10.5
|
||||
|
||||
require github.com/google/uuid v1.6.0
|
||||
|
||||
require golang.org/x/text v0.27.0
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
|
232
modules/http/http.go
Normal file
232
modules/http/http.go
Normal file
@ -0,0 +1,232 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||
"github.com/valyala/fasthttp"
|
||||
)
|
||||
|
||||
var (
|
||||
globalServer *fasthttp.Server
|
||||
globalWorkerPool *WorkerPool
|
||||
globalStateCreator StateCreator
|
||||
globalMu sync.RWMutex
|
||||
serverRunning bool
|
||||
)
|
||||
|
||||
func SetStateCreator(creator StateCreator) {
|
||||
globalStateCreator = creator
|
||||
}
|
||||
|
||||
func GetFunctionList() map[string]luajit.GoFunction {
|
||||
return map[string]luajit.GoFunction{
|
||||
"http_create_server": http_create_server,
|
||||
"http_spawn_workers": http_spawn_workers,
|
||||
"http_listen": http_listen,
|
||||
"http_close_server": http_close_server,
|
||||
"http_has_servers": http_has_servers,
|
||||
}
|
||||
}
|
||||
|
||||
func http_create_server(s *luajit.State) int {
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
if globalServer != nil {
|
||||
s.PushBoolean(true) // Already created
|
||||
return 1
|
||||
}
|
||||
|
||||
globalServer = &fasthttp.Server{
|
||||
Handler: handleRequest,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
s.PushBoolean(true)
|
||||
return 1
|
||||
}
|
||||
|
||||
func http_spawn_workers(s *luajit.State) int {
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
if globalWorkerPool != nil {
|
||||
s.PushBoolean(true) // Already spawned
|
||||
return 1
|
||||
}
|
||||
|
||||
if globalStateCreator == nil {
|
||||
s.PushBoolean(false)
|
||||
s.PushString("state creator not set")
|
||||
return 2
|
||||
}
|
||||
|
||||
workerCount := max(runtime.NumCPU(), 2)
|
||||
|
||||
pool, err := NewWorkerPool(workerCount, s, globalStateCreator)
|
||||
if err != nil {
|
||||
s.PushBoolean(false)
|
||||
s.PushString(fmt.Sprintf("failed to create worker pool: %v", err))
|
||||
return 2
|
||||
}
|
||||
globalWorkerPool = pool
|
||||
|
||||
s.PushBoolean(true)
|
||||
return 1
|
||||
}
|
||||
|
||||
func http_listen(s *luajit.State) int {
|
||||
if err := s.CheckMinArgs(1); err != nil {
|
||||
return s.PushError("http_listen: %v", err)
|
||||
}
|
||||
|
||||
addr := s.ToString(1)
|
||||
|
||||
globalMu.RLock()
|
||||
server := globalServer
|
||||
globalMu.RUnlock()
|
||||
|
||||
if server == nil {
|
||||
s.PushBoolean(false)
|
||||
s.PushString("no server created")
|
||||
return 2
|
||||
}
|
||||
|
||||
globalMu.Lock()
|
||||
if serverRunning {
|
||||
globalMu.Unlock()
|
||||
s.PushBoolean(true) // Already running
|
||||
return 1
|
||||
}
|
||||
serverRunning = true
|
||||
globalMu.Unlock()
|
||||
|
||||
go func() {
|
||||
if err := server.ListenAndServe(addr); err != nil {
|
||||
fmt.Printf("HTTP server error: %v\n", err)
|
||||
}
|
||||
}()
|
||||
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
|
||||
conn, err := net.Dial("tcp", addr)
|
||||
if err != nil {
|
||||
globalMu.Lock()
|
||||
serverRunning = false
|
||||
globalMu.Unlock()
|
||||
s.PushBoolean(false)
|
||||
s.PushString(fmt.Sprintf("failed to start server: %v", err))
|
||||
return 2
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
s.PushBoolean(true)
|
||||
return 1
|
||||
}
|
||||
|
||||
func http_close_server(s *luajit.State) int {
|
||||
globalMu.Lock()
|
||||
defer globalMu.Unlock()
|
||||
|
||||
if globalWorkerPool != nil {
|
||||
globalWorkerPool.Close()
|
||||
globalWorkerPool = nil
|
||||
}
|
||||
|
||||
if globalServer != nil {
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
globalServer.ShutdownWithContext(ctx)
|
||||
globalServer = nil
|
||||
}
|
||||
|
||||
serverRunning = false
|
||||
s.PushBoolean(true)
|
||||
return 1
|
||||
}
|
||||
|
||||
func http_has_servers(s *luajit.State) int {
|
||||
globalMu.RLock()
|
||||
running := serverRunning
|
||||
globalMu.RUnlock()
|
||||
|
||||
s.PushBoolean(running)
|
||||
return 1
|
||||
}
|
||||
|
||||
func HasActiveServers() bool {
|
||||
globalMu.RLock()
|
||||
defer globalMu.RUnlock()
|
||||
return serverRunning
|
||||
}
|
||||
|
||||
func WaitForServers() {
|
||||
for HasActiveServers() {
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
func handleRequest(ctx *fasthttp.RequestCtx) {
|
||||
globalMu.RLock()
|
||||
pool := globalWorkerPool
|
||||
globalMu.RUnlock()
|
||||
|
||||
if pool == nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
ctx.SetBodyString("Worker pool not initialized")
|
||||
return
|
||||
}
|
||||
|
||||
worker := pool.Get()
|
||||
defer pool.Put(worker)
|
||||
|
||||
if worker == nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
ctx.SetBodyString("No worker available")
|
||||
return
|
||||
}
|
||||
|
||||
req := GetRequest()
|
||||
defer PutRequest(req)
|
||||
|
||||
resp := GetResponse()
|
||||
defer PutResponse(resp)
|
||||
|
||||
// Populate request
|
||||
req.Method = string(ctx.Method())
|
||||
req.Path = string(ctx.Path())
|
||||
req.Body = string(ctx.Request.Body())
|
||||
|
||||
ctx.QueryArgs().VisitAll(func(key, value []byte) {
|
||||
req.Query[string(key)] = string(value)
|
||||
})
|
||||
|
||||
ctx.Request.Header.VisitAll(func(key, value []byte) {
|
||||
req.Headers[string(key)] = string(value)
|
||||
})
|
||||
|
||||
// Let Lua handle everything
|
||||
err := worker.HandleRequest(req, resp)
|
||||
if err != nil {
|
||||
ctx.SetStatusCode(fasthttp.StatusInternalServerError)
|
||||
ctx.SetBodyString(fmt.Sprintf("Internal Server Error: %v", err))
|
||||
return
|
||||
}
|
||||
|
||||
// Apply response
|
||||
ctx.SetStatusCode(resp.StatusCode)
|
||||
for key, value := range resp.Headers {
|
||||
ctx.Response.Header.Set(key, value)
|
||||
}
|
||||
if resp.Body != "" {
|
||||
ctx.SetBodyString(resp.Body)
|
||||
}
|
||||
}
|
611
modules/http/http.lua
Normal file
611
modules/http/http.lua
Normal file
@ -0,0 +1,611 @@
|
||||
-- modules/http.lua - Express-like HTTP server with pure Lua routing
|
||||
|
||||
local http = {}
|
||||
|
||||
-- Global routing tables (shared across all states)
|
||||
_G._http_routes = _G._http_routes or {}
|
||||
_G._http_middleware = _G._http_middleware or {}
|
||||
|
||||
-- Request/Response classes
|
||||
local Request = {}
|
||||
Request.__index = Request
|
||||
|
||||
local Response = {}
|
||||
Response.__index = Response
|
||||
|
||||
-- ======================================================================
|
||||
-- ROUTER IMPLEMENTATION
|
||||
-- ======================================================================
|
||||
|
||||
local function split_path(path)
|
||||
local segments = {}
|
||||
for segment in path:gmatch("[^/]+") do
|
||||
table.insert(segments, segment)
|
||||
end
|
||||
return segments
|
||||
end
|
||||
|
||||
local function match_route(method, path)
|
||||
local path_segments = split_path(path)
|
||||
|
||||
for _, route in ipairs(_G._http_routes) do
|
||||
if route.method == method then
|
||||
local params = {}
|
||||
local route_segments = route.segments
|
||||
local match = true
|
||||
local i = 1
|
||||
|
||||
while i <= #route_segments and match do
|
||||
local route_seg = route_segments[i]
|
||||
|
||||
if route_seg == "*" then
|
||||
-- Wildcard captures everything remaining
|
||||
local remaining = {}
|
||||
for j = i, #path_segments do
|
||||
table.insert(remaining, path_segments[j])
|
||||
end
|
||||
params["*"] = table.concat(remaining, "/")
|
||||
break
|
||||
elseif route_seg:sub(1,1) == ":" then
|
||||
-- Parameter segment
|
||||
if i <= #path_segments then
|
||||
local param_name = route_seg:sub(2)
|
||||
params[param_name] = path_segments[i]
|
||||
else
|
||||
match = false
|
||||
end
|
||||
else
|
||||
-- Static segment
|
||||
if i > #path_segments or route_seg ~= path_segments[i] then
|
||||
match = false
|
||||
end
|
||||
end
|
||||
i = i + 1
|
||||
end
|
||||
|
||||
-- Must consume all segments unless wildcard
|
||||
if match and (i > #path_segments or route_segments[i-1] == "*") then
|
||||
return route, params
|
||||
end
|
||||
end
|
||||
end
|
||||
return nil, {}
|
||||
end
|
||||
|
||||
-- Global handler function called by Go workers
|
||||
function _http_handle_request(req_table, res_table)
|
||||
local req = Request.new(req_table)
|
||||
local res = Response.new(res_table)
|
||||
|
||||
-- Find matching route
|
||||
local route, params = match_route(req.method, req.path)
|
||||
req.params = params
|
||||
|
||||
if not route then
|
||||
res:status(404):send("Not Found")
|
||||
return
|
||||
end
|
||||
|
||||
-- Execute middleware chain
|
||||
local function run_middleware(index)
|
||||
if index > #_G._http_middleware then
|
||||
-- All middleware executed, run route handler
|
||||
route.handler(req, res)
|
||||
return
|
||||
end
|
||||
|
||||
local mw = _G._http_middleware[index]
|
||||
if mw.path == nil or req.path:match("^" .. mw.path:gsub("([%(%)%.%+%-%*%?%[%]%^%$%%])", "%%%1")) then
|
||||
mw.handler(req, res, function()
|
||||
run_middleware(index + 1)
|
||||
end)
|
||||
else
|
||||
run_middleware(index + 1)
|
||||
end
|
||||
end
|
||||
|
||||
run_middleware(1)
|
||||
end
|
||||
|
||||
-- Functions for route synchronization
|
||||
function _http_get_routes()
|
||||
return {
|
||||
routes = _G._http_routes,
|
||||
middleware = _G._http_middleware
|
||||
}
|
||||
end
|
||||
|
||||
function _http_sync_worker_routes()
|
||||
local data = _G._http_routes_data
|
||||
if data and data.routes and data.middleware then
|
||||
_G._http_routes = data.routes
|
||||
_G._http_middleware = data.middleware
|
||||
end
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- SERVER CLASS
|
||||
-- ======================================================================
|
||||
|
||||
local Server = {}
|
||||
Server.__index = Server
|
||||
|
||||
function http.server()
|
||||
-- Workers should not create servers
|
||||
if _G.__IS_WORKER then
|
||||
return setmetatable({}, Server)
|
||||
end
|
||||
|
||||
local server = setmetatable({
|
||||
_server_created = false
|
||||
}, Server)
|
||||
|
||||
-- Create the fasthttp server immediately
|
||||
local success, err = moonshark.http_create_server()
|
||||
if not success then
|
||||
error("Failed to create HTTP server: " .. (err or "unknown error"))
|
||||
end
|
||||
server._server_created = true
|
||||
|
||||
return server
|
||||
end
|
||||
|
||||
function Server:use(...)
|
||||
local args = {...}
|
||||
if #args == 1 and type(args[1]) == "function" then
|
||||
table.insert(_G._http_middleware, {path = nil, handler = args[1]})
|
||||
elseif #args == 2 and type(args[1]) == "string" and type(args[2]) == "function" then
|
||||
table.insert(_G._http_middleware, {path = args[1], handler = args[2]})
|
||||
else
|
||||
error("Invalid arguments to use()")
|
||||
end
|
||||
|
||||
-- Only sync in main state
|
||||
if not _G.__IS_WORKER then
|
||||
self:_sync_routes()
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Server:_add_route(method, path, handler)
|
||||
-- Ensure path starts with /
|
||||
if path:sub(1,1) ~= "/" then
|
||||
path = "/" .. path
|
||||
end
|
||||
|
||||
local segments = split_path(path)
|
||||
|
||||
table.insert(_G._http_routes, {
|
||||
method = method,
|
||||
path = path,
|
||||
segments = segments,
|
||||
handler = handler
|
||||
})
|
||||
|
||||
-- Only sync in main state
|
||||
if not _G.__IS_WORKER then
|
||||
self:_sync_routes()
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Server:_sync_routes()
|
||||
-- No-op - routes are in global Lua tables, workers inherit them automatically
|
||||
end
|
||||
|
||||
-- HTTP method helpers
|
||||
function Server:get(path, handler)
|
||||
return self:_add_route("GET", path, handler)
|
||||
end
|
||||
|
||||
function Server:post(path, handler)
|
||||
return self:_add_route("POST", path, handler)
|
||||
end
|
||||
|
||||
function Server:put(path, handler)
|
||||
return self:_add_route("PUT", path, handler)
|
||||
end
|
||||
|
||||
function Server:delete(path, handler)
|
||||
return self:_add_route("DELETE", path, handler)
|
||||
end
|
||||
|
||||
function Server:patch(path, handler)
|
||||
return self:_add_route("PATCH", path, handler)
|
||||
end
|
||||
|
||||
function Server:head(path, handler)
|
||||
return self:_add_route("HEAD", path, handler)
|
||||
end
|
||||
|
||||
function Server:options(path, handler)
|
||||
return self:_add_route("OPTIONS", path, handler)
|
||||
end
|
||||
|
||||
function Server:all(path, handler)
|
||||
local methods = {"GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"}
|
||||
for _, method in ipairs(methods) do
|
||||
self:_add_route(method, path, handler)
|
||||
end
|
||||
return self
|
||||
end
|
||||
|
||||
function Server:listen(port, host, callback)
|
||||
-- Workers should not listen
|
||||
if _G.__IS_WORKER then
|
||||
if callback then callback() end
|
||||
return self
|
||||
end
|
||||
|
||||
if type(host) == "function" then
|
||||
callback = host
|
||||
host = "localhost"
|
||||
end
|
||||
|
||||
host = host or "localhost"
|
||||
local addr = host .. ":" .. tostring(port)
|
||||
|
||||
-- Spawn workers first
|
||||
local success, err = moonshark.http_spawn_workers()
|
||||
if not success then
|
||||
error("Failed to spawn workers: " .. (err or "unknown error"))
|
||||
end
|
||||
|
||||
-- Then start listening
|
||||
success, err = moonshark.http_listen(addr)
|
||||
if not success then
|
||||
error("Failed to start server: " .. (err or "unknown error"))
|
||||
end
|
||||
|
||||
if callback then
|
||||
callback()
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Server:close()
|
||||
if _G.__IS_WORKER then
|
||||
return true
|
||||
end
|
||||
return moonshark.http_close_server()
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- REQUEST OBJECT
|
||||
-- ======================================================================
|
||||
|
||||
function Request.new(req_table)
|
||||
local req = setmetatable({
|
||||
method = req_table.method,
|
||||
path = req_table.path,
|
||||
query = req_table.query or {},
|
||||
headers = req_table.headers or {},
|
||||
params = {},
|
||||
body = req_table.body or ""
|
||||
}, Request)
|
||||
|
||||
return req
|
||||
end
|
||||
|
||||
function Request:get(header_name)
|
||||
return self.headers[header_name] or self.headers[header_name:lower()]
|
||||
end
|
||||
|
||||
function Request:header(header_name)
|
||||
return self:get(header_name)
|
||||
end
|
||||
|
||||
function Request:param(name, default_value)
|
||||
return self.params[name] or default_value
|
||||
end
|
||||
|
||||
function Request:query_param(name, default_value)
|
||||
return self.query[name] or default_value
|
||||
end
|
||||
|
||||
function Request:json()
|
||||
if self.body == "" then
|
||||
return nil
|
||||
end
|
||||
|
||||
local success, result = pcall(function()
|
||||
return moonshark.json_decode(self.body)
|
||||
end)
|
||||
|
||||
if success then
|
||||
return result
|
||||
else
|
||||
error("Invalid JSON in request body")
|
||||
end
|
||||
end
|
||||
|
||||
function Request:is_json()
|
||||
local content_type = self:get("content-type") or ""
|
||||
return content_type:find("application/json") ~= nil
|
||||
end
|
||||
|
||||
function Request:is_form()
|
||||
local content_type = self:get("content-type") or ""
|
||||
return content_type:find("application/x-www-form-urlencoded") ~= nil
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- RESPONSE OBJECT
|
||||
-- ======================================================================
|
||||
|
||||
function Response.new(res_table)
|
||||
local res = setmetatable({
|
||||
_table = res_table,
|
||||
_sent = false
|
||||
}, Response)
|
||||
|
||||
return res
|
||||
end
|
||||
|
||||
function Response:status(code)
|
||||
if self._sent then
|
||||
error("Cannot set status after response has been sent")
|
||||
end
|
||||
self._table.status = code
|
||||
return self
|
||||
end
|
||||
|
||||
function Response:header(name, value)
|
||||
if self._sent then
|
||||
error("Cannot set headers after response has been sent")
|
||||
end
|
||||
self._table.headers[name] = value
|
||||
return self
|
||||
end
|
||||
|
||||
function Response:set(name, value)
|
||||
return self:header(name, value)
|
||||
end
|
||||
|
||||
function Response:type(content_type)
|
||||
return self:header("Content-Type", content_type)
|
||||
end
|
||||
|
||||
function Response:send(data)
|
||||
if self._sent then
|
||||
error("Response already sent")
|
||||
end
|
||||
|
||||
if type(data) == "table" then
|
||||
self:json(data)
|
||||
elseif type(data) == "number" then
|
||||
self._table.status = data
|
||||
self._table.body = ""
|
||||
else
|
||||
self._table.body = tostring(data or "")
|
||||
end
|
||||
|
||||
self._sent = true
|
||||
return self
|
||||
end
|
||||
|
||||
function Response:json(data)
|
||||
if self._sent then
|
||||
error("Response already sent")
|
||||
end
|
||||
|
||||
self:type("application/json")
|
||||
|
||||
local success, json_str = pcall(function()
|
||||
return moonshark.json_encode(data)
|
||||
end)
|
||||
|
||||
if success then
|
||||
self._table.body = json_str
|
||||
else
|
||||
error("Failed to encode JSON response")
|
||||
end
|
||||
|
||||
self._sent = true
|
||||
return self
|
||||
end
|
||||
|
||||
function Response:text(text)
|
||||
if self._sent then
|
||||
error("Response already sent")
|
||||
end
|
||||
|
||||
self:type("text/plain")
|
||||
self._table.body = tostring(text or "")
|
||||
self._sent = true
|
||||
return self
|
||||
end
|
||||
|
||||
function Response:html(html)
|
||||
if self._sent then
|
||||
error("Response already sent")
|
||||
end
|
||||
|
||||
self:type("text/html")
|
||||
self._table.body = tostring(html or "")
|
||||
self._sent = true
|
||||
return self
|
||||
end
|
||||
|
||||
function Response:redirect(url, status)
|
||||
if self._sent then
|
||||
error("Response already sent")
|
||||
end
|
||||
|
||||
status = status or 302
|
||||
self:status(status)
|
||||
self:header("Location", url)
|
||||
self._table.body = ""
|
||||
self._sent = true
|
||||
return self
|
||||
end
|
||||
|
||||
function Response:cookie(name, value, options)
|
||||
if self._sent then
|
||||
error("Cannot set cookies after response has been sent")
|
||||
end
|
||||
|
||||
options = options or {}
|
||||
local cookie = name .. "=" .. tostring(value)
|
||||
|
||||
if options.expires then
|
||||
cookie = cookie .. "; Expires=" .. options.expires
|
||||
end
|
||||
|
||||
if options.max_age then
|
||||
cookie = cookie .. "; Max-Age=" .. tostring(options.max_age)
|
||||
end
|
||||
|
||||
if options.domain then
|
||||
cookie = cookie .. "; Domain=" .. options.domain
|
||||
end
|
||||
|
||||
if options.path then
|
||||
cookie = cookie .. "; Path=" .. options.path
|
||||
end
|
||||
|
||||
if options.secure then
|
||||
cookie = cookie .. "; Secure"
|
||||
end
|
||||
|
||||
if options.http_only then
|
||||
cookie = cookie .. "; HttpOnly"
|
||||
end
|
||||
|
||||
if options.same_site then
|
||||
cookie = cookie .. "; SameSite=" .. options.same_site
|
||||
end
|
||||
|
||||
local existing = self._table.headers["Set-Cookie"]
|
||||
if existing then
|
||||
if type(existing) == "table" then
|
||||
table.insert(existing, cookie)
|
||||
else
|
||||
self._table.headers["Set-Cookie"] = {existing, cookie}
|
||||
end
|
||||
else
|
||||
self._table.headers["Set-Cookie"] = cookie
|
||||
end
|
||||
|
||||
return self
|
||||
end
|
||||
|
||||
function Response:clear_cookie(name, options)
|
||||
options = options or {}
|
||||
options.expires = "Thu, 01 Jan 1970 00:00:00 GMT"
|
||||
options.max_age = 0
|
||||
return self:cookie(name, "", options)
|
||||
end
|
||||
|
||||
-- ======================================================================
|
||||
-- MIDDLEWARE HELPERS
|
||||
-- ======================================================================
|
||||
|
||||
function http.cors(options)
|
||||
options = options or {}
|
||||
local origin = options.origin or "*"
|
||||
local methods = options.methods or "GET,HEAD,PUT,PATCH,POST,DELETE"
|
||||
local headers = options.headers or "Content-Type,Authorization"
|
||||
|
||||
return function(req, res, next)
|
||||
res:header("Access-Control-Allow-Origin", origin)
|
||||
res:header("Access-Control-Allow-Methods", methods)
|
||||
res:header("Access-Control-Allow-Headers", headers)
|
||||
|
||||
if options.credentials then
|
||||
res:header("Access-Control-Allow-Credentials", "true")
|
||||
end
|
||||
|
||||
if req.method == "OPTIONS" then
|
||||
res:status(204):send("")
|
||||
else
|
||||
next()
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function http.static(root_path)
|
||||
return function(req, res, next)
|
||||
if req.method ~= "GET" and req.method ~= "HEAD" then
|
||||
next()
|
||||
return
|
||||
end
|
||||
|
||||
local file_path = moonshark.path_join(root_path, req.path)
|
||||
file_path = moonshark.path_clean(file_path)
|
||||
|
||||
local abs_root = moonshark.path_abs(root_path)
|
||||
local abs_file = moonshark.path_abs(file_path)
|
||||
|
||||
if not abs_file or not abs_file:find("^" .. abs_root:gsub("([%(%)%.%+%-%*%?%[%]%^%$%%])", "%%%1")) then
|
||||
next()
|
||||
return
|
||||
end
|
||||
|
||||
if moonshark.file_exists(file_path) and not moonshark.file_is_dir(file_path) then
|
||||
local content = moonshark.file_read(file_path)
|
||||
if content then
|
||||
local ext = moonshark.path_ext(file_path):lower()
|
||||
local content_types = {
|
||||
[".html"] = "text/html",
|
||||
[".css"] = "text/css",
|
||||
[".js"] = "application/javascript",
|
||||
[".json"] = "application/json",
|
||||
[".png"] = "image/png",
|
||||
[".jpg"] = "image/jpeg",
|
||||
[".jpeg"] = "image/jpeg",
|
||||
[".gif"] = "image/gif",
|
||||
[".svg"] = "image/svg+xml",
|
||||
[".txt"] = "text/plain",
|
||||
}
|
||||
|
||||
local content_type = content_types[ext] or "application/octet-stream"
|
||||
res:type(content_type):send(content)
|
||||
return
|
||||
end
|
||||
end
|
||||
|
||||
next()
|
||||
end
|
||||
end
|
||||
|
||||
function http.json_parser()
|
||||
return function(req, res, next)
|
||||
if req:is_json() and req.body ~= "" then
|
||||
local success, data = pcall(function()
|
||||
return req:json()
|
||||
end)
|
||||
|
||||
if success then
|
||||
req.json_body = data
|
||||
else
|
||||
res:status(400):json({error = "Invalid JSON"})
|
||||
return
|
||||
end
|
||||
end
|
||||
next()
|
||||
end
|
||||
end
|
||||
|
||||
function http.logger()
|
||||
return function(req, res, next)
|
||||
local start_time = os.clock()
|
||||
|
||||
next()
|
||||
|
||||
local duration = (os.clock() - start_time) * 1000
|
||||
local status = res._table.status or 200
|
||||
print(string.format("%s %s %d %.2fms", req.method, req.path, status, duration))
|
||||
end
|
||||
end
|
||||
|
||||
function http.create_server(callback)
|
||||
local app = http.server()
|
||||
if callback then
|
||||
callback(app)
|
||||
end
|
||||
return app
|
||||
end
|
||||
|
||||
return http
|
333
modules/http/pool.go
Normal file
333
modules/http/pool.go
Normal file
@ -0,0 +1,333 @@
|
||||
package http
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||
)
|
||||
|
||||
type StateCreator func() (*luajit.State, error)
|
||||
|
||||
type Request struct {
|
||||
Method string
|
||||
Path string
|
||||
Query map[string]string
|
||||
Headers map[string]string
|
||||
Body string
|
||||
}
|
||||
|
||||
type Response struct {
|
||||
StatusCode int
|
||||
Headers map[string]string
|
||||
Body string
|
||||
}
|
||||
|
||||
type Worker struct {
|
||||
state *luajit.State
|
||||
id int
|
||||
}
|
||||
|
||||
type WorkerPool struct {
|
||||
workers chan *Worker
|
||||
masterState *luajit.State
|
||||
stateCreator StateCreator
|
||||
workerCount int
|
||||
closed bool
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
requestPool = sync.Pool{
|
||||
New: func() any {
|
||||
return &Request{
|
||||
Query: make(map[string]string),
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
responsePool = sync.Pool{
|
||||
New: func() any {
|
||||
return &Response{
|
||||
Headers: make(map[string]string),
|
||||
}
|
||||
},
|
||||
}
|
||||
)
|
||||
|
||||
func GetRequest() *Request {
|
||||
req := requestPool.Get().(*Request)
|
||||
for k := range req.Query {
|
||||
delete(req.Query, k)
|
||||
}
|
||||
for k := range req.Headers {
|
||||
delete(req.Headers, k)
|
||||
}
|
||||
req.Method = ""
|
||||
req.Path = ""
|
||||
req.Body = ""
|
||||
return req
|
||||
}
|
||||
|
||||
func PutRequest(req *Request) {
|
||||
requestPool.Put(req)
|
||||
}
|
||||
|
||||
func GetResponse() *Response {
|
||||
resp := responsePool.Get().(*Response)
|
||||
for k := range resp.Headers {
|
||||
delete(resp.Headers, k)
|
||||
}
|
||||
resp.StatusCode = 200
|
||||
resp.Body = ""
|
||||
return resp
|
||||
}
|
||||
|
||||
func PutResponse(resp *Response) {
|
||||
responsePool.Put(resp)
|
||||
}
|
||||
|
||||
func NewWorkerPool(size int, masterState *luajit.State, stateCreator StateCreator) (*WorkerPool, error) {
|
||||
pool := &WorkerPool{
|
||||
workers: make(chan *Worker, size),
|
||||
masterState: masterState,
|
||||
stateCreator: stateCreator,
|
||||
workerCount: size,
|
||||
}
|
||||
|
||||
for i := 0; i < size; i++ {
|
||||
worker, err := pool.createWorker(i)
|
||||
if err != nil {
|
||||
pool.Close()
|
||||
return nil, fmt.Errorf("failed to create worker %d: %w", i, err)
|
||||
}
|
||||
pool.workers <- worker
|
||||
}
|
||||
|
||||
return pool, nil
|
||||
}
|
||||
|
||||
func (p *WorkerPool) createWorker(id int) (*Worker, error) {
|
||||
workerState, err := p.stateCreator()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create worker state: %w", err)
|
||||
}
|
||||
|
||||
return &Worker{
|
||||
state: workerState,
|
||||
id: id,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (p *WorkerPool) Get() *Worker {
|
||||
p.mu.RLock()
|
||||
if p.closed {
|
||||
p.mu.RUnlock()
|
||||
return nil
|
||||
}
|
||||
p.mu.RUnlock()
|
||||
|
||||
select {
|
||||
case worker := <-p.workers:
|
||||
return worker
|
||||
default:
|
||||
worker, err := p.createWorker(-1)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return worker
|
||||
}
|
||||
}
|
||||
|
||||
func (p *WorkerPool) Put(worker *Worker) {
|
||||
if worker == nil {
|
||||
return
|
||||
}
|
||||
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
if p.closed {
|
||||
worker.Close()
|
||||
return
|
||||
}
|
||||
|
||||
if worker.id == -1 {
|
||||
worker.Close()
|
||||
return
|
||||
}
|
||||
|
||||
select {
|
||||
case p.workers <- worker:
|
||||
default:
|
||||
worker.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (p *WorkerPool) SyncRoutes(routesData any) {
|
||||
p.mu.RLock()
|
||||
defer p.mu.RUnlock()
|
||||
|
||||
if p.closed {
|
||||
return
|
||||
}
|
||||
|
||||
// Sync routes to all workers
|
||||
workers := make([]*Worker, 0, p.workerCount)
|
||||
|
||||
// Collect all workers
|
||||
for {
|
||||
select {
|
||||
case worker := <-p.workers:
|
||||
workers = append(workers, worker)
|
||||
default:
|
||||
goto syncWorkers
|
||||
}
|
||||
}
|
||||
|
||||
syncWorkers:
|
||||
// Sync and return workers
|
||||
for _, worker := range workers {
|
||||
if worker.state != nil {
|
||||
worker.state.PushValue(routesData)
|
||||
worker.state.SetGlobal("_http_routes_data")
|
||||
|
||||
worker.state.GetGlobal("_http_sync_worker_routes")
|
||||
if worker.state.IsFunction(-1) {
|
||||
worker.state.Call(0, 0)
|
||||
} else {
|
||||
worker.state.Pop(1)
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case p.workers <- worker:
|
||||
default:
|
||||
worker.Close()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *WorkerPool) Close() {
|
||||
p.mu.Lock()
|
||||
defer p.mu.Unlock()
|
||||
|
||||
if p.closed {
|
||||
return
|
||||
}
|
||||
p.closed = true
|
||||
|
||||
close(p.workers)
|
||||
for worker := range p.workers {
|
||||
worker.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) Close() {
|
||||
if w.state != nil {
|
||||
w.state.Close()
|
||||
w.state = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Worker) HandleRequest(req *Request, resp *Response) error {
|
||||
if w.state == nil {
|
||||
return fmt.Errorf("worker state is nil")
|
||||
}
|
||||
|
||||
// Create request table
|
||||
w.state.NewTable()
|
||||
w.state.PushString("method")
|
||||
w.state.PushString(req.Method)
|
||||
w.state.SetTable(-3)
|
||||
|
||||
w.state.PushString("path")
|
||||
w.state.PushString(req.Path)
|
||||
w.state.SetTable(-3)
|
||||
|
||||
w.state.PushString("body")
|
||||
w.state.PushString(req.Body)
|
||||
w.state.SetTable(-3)
|
||||
|
||||
// Query params
|
||||
w.state.PushString("query")
|
||||
w.state.NewTable()
|
||||
for k, v := range req.Query {
|
||||
w.state.PushString(k)
|
||||
w.state.PushString(v)
|
||||
w.state.SetTable(-3)
|
||||
}
|
||||
w.state.SetTable(-3)
|
||||
|
||||
// Headers
|
||||
w.state.PushString("headers")
|
||||
w.state.NewTable()
|
||||
for k, v := range req.Headers {
|
||||
w.state.PushString(k)
|
||||
w.state.PushString(v)
|
||||
w.state.SetTable(-3)
|
||||
}
|
||||
w.state.SetTable(-3)
|
||||
|
||||
// Create response table
|
||||
w.state.NewTable()
|
||||
w.state.PushString("status")
|
||||
w.state.PushNumber(200)
|
||||
w.state.SetTable(-3)
|
||||
|
||||
w.state.PushString("body")
|
||||
w.state.PushString("")
|
||||
w.state.SetTable(-3)
|
||||
|
||||
w.state.PushString("headers")
|
||||
w.state.NewTable()
|
||||
w.state.SetTable(-3)
|
||||
|
||||
// Call _http_handle_request(req, res) - pure Lua routing
|
||||
w.state.GetGlobal("_http_handle_request")
|
||||
if !w.state.IsFunction(-1) {
|
||||
w.state.Pop(3)
|
||||
resp.StatusCode = 500
|
||||
resp.Body = "HTTP handler not initialized"
|
||||
return nil
|
||||
}
|
||||
|
||||
w.state.PushCopy(-3) // request
|
||||
w.state.PushCopy(-3) // response
|
||||
|
||||
if err := w.state.Call(2, 0); err != nil {
|
||||
w.state.Pop(2)
|
||||
resp.StatusCode = 500
|
||||
resp.Body = fmt.Sprintf("Handler error: %v", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Extract response
|
||||
w.state.GetField(-1, "status")
|
||||
if w.state.IsNumber(-1) {
|
||||
resp.StatusCode = int(w.state.ToNumber(-1))
|
||||
}
|
||||
w.state.Pop(1)
|
||||
|
||||
w.state.GetField(-1, "body")
|
||||
if w.state.IsString(-1) {
|
||||
resp.Body = w.state.ToString(-1)
|
||||
}
|
||||
w.state.Pop(1)
|
||||
|
||||
w.state.GetField(-1, "headers")
|
||||
if w.state.IsTable(-1) {
|
||||
w.state.PushNil()
|
||||
for w.state.Next(-2) {
|
||||
if w.state.IsString(-2) && w.state.IsString(-1) {
|
||||
resp.Headers[w.state.ToString(-2)] = w.state.ToString(-1)
|
||||
}
|
||||
w.state.Pop(1)
|
||||
}
|
||||
}
|
||||
w.state.Pop(1)
|
||||
|
||||
w.state.Pop(2) // Clean up request and response tables
|
||||
return nil
|
||||
}
|
@ -7,6 +7,7 @@ import (
|
||||
|
||||
"Moonshark/modules/crypto"
|
||||
"Moonshark/modules/fs"
|
||||
"Moonshark/modules/http"
|
||||
"Moonshark/modules/json"
|
||||
"Moonshark/modules/math"
|
||||
lua_string "Moonshark/modules/string"
|
||||
@ -17,7 +18,7 @@ import (
|
||||
// Global registry instance
|
||||
var Global *Registry
|
||||
|
||||
//go:embed crypto/*.lua fs/*.lua json/*.lua math/*.lua string/*.lua
|
||||
//go:embed crypto/*.lua fs/*.lua json/*.lua math/*.lua string/*.lua http/*.lua
|
||||
var embeddedModules embed.FS
|
||||
|
||||
// Registry manages all Lua modules and Go functions
|
||||
@ -39,6 +40,7 @@ func New() *Registry {
|
||||
maps.Copy(r.goFuncs, math.GetFunctionList())
|
||||
maps.Copy(r.goFuncs, fs.GetFunctionList())
|
||||
maps.Copy(r.goFuncs, crypto.GetFunctionList())
|
||||
maps.Copy(r.goFuncs, http.GetFunctionList())
|
||||
|
||||
r.loadEmbeddedModules()
|
||||
return r
|
||||
|
22
moonshark.go
22
moonshark.go
@ -3,8 +3,11 @@ package main
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"syscall"
|
||||
|
||||
"Moonshark/modules/http"
|
||||
"Moonshark/state"
|
||||
)
|
||||
|
||||
@ -29,4 +32,23 @@ func main() {
|
||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
// Check if HTTP servers are running
|
||||
if http.HasActiveServers() {
|
||||
// Set up signal handling for graceful shutdown
|
||||
sigChan := make(chan os.Signal, 1)
|
||||
signal.Notify(sigChan, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
fmt.Println("HTTP servers running. Press Ctrl+C to exit.")
|
||||
|
||||
// Wait for either signal or servers to close
|
||||
go func() {
|
||||
<-sigChan
|
||||
fmt.Println("\nShutting down...")
|
||||
os.Exit(0)
|
||||
}()
|
||||
|
||||
// Wait for all servers to finish
|
||||
http.WaitForServers()
|
||||
}
|
||||
}
|
||||
|
@ -6,6 +6,7 @@ import (
|
||||
"path/filepath"
|
||||
|
||||
"Moonshark/modules"
|
||||
"Moonshark/modules/http"
|
||||
|
||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||
)
|
||||
@ -15,6 +16,7 @@ type State struct {
|
||||
*luajit.State
|
||||
initialized bool
|
||||
isWorker bool
|
||||
scriptDir string
|
||||
}
|
||||
|
||||
// Config holds state initialization options
|
||||
@ -50,6 +52,7 @@ func New(config ...Config) (*State, error) {
|
||||
state := &State{
|
||||
State: baseState,
|
||||
isWorker: cfg.IsWorker,
|
||||
scriptDir: cfg.ScriptDir,
|
||||
}
|
||||
|
||||
// Set worker global flag if this is a worker state
|
||||
@ -125,6 +128,13 @@ func NewWorkerFromScript(scriptPath string, config ...Config) (*State, error) {
|
||||
return New(cfg)
|
||||
}
|
||||
|
||||
// Store main state initialization for worker replication
|
||||
var (
|
||||
mainStateScriptDir string
|
||||
mainStateScript string
|
||||
mainStateScriptName string
|
||||
)
|
||||
|
||||
// initializeModules sets up the module system
|
||||
func (s *State) initializeModules() error {
|
||||
// Initialize global registry if needed
|
||||
@ -134,7 +144,40 @@ func (s *State) initializeModules() error {
|
||||
}
|
||||
}
|
||||
|
||||
return modules.Global.InstallInState(s.State)
|
||||
// Install modules first
|
||||
if err := modules.Global.InstallInState(s.State); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SetupStateCreator sets up the state creator after script is loaded
|
||||
func (s *State) SetupStateCreator() {
|
||||
if s.isWorker {
|
||||
return
|
||||
}
|
||||
|
||||
http.SetStateCreator(func() (*luajit.State, error) {
|
||||
cfg := DefaultConfig()
|
||||
cfg.IsWorker = true
|
||||
cfg.ScriptDir = mainStateScriptDir
|
||||
|
||||
workerState, err := New(cfg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Execute the same script as main state to get identical environment
|
||||
if mainStateScript != "" {
|
||||
if err := workerState.ExecuteString(mainStateScript, mainStateScriptName); err != nil {
|
||||
workerState.Close()
|
||||
return nil, fmt.Errorf("failed to execute script in worker: %w", err)
|
||||
}
|
||||
}
|
||||
|
||||
return workerState.State, nil
|
||||
})
|
||||
}
|
||||
|
||||
// SetScriptDirectory adds a directory to Lua's package.path
|
||||
@ -156,6 +199,15 @@ func (s *State) ExecuteFile(scriptPath string) error {
|
||||
return fmt.Errorf("failed to read script file '%s': %w", scriptPath, err)
|
||||
}
|
||||
|
||||
// Store for worker replication if this is main state
|
||||
if !s.isWorker {
|
||||
mainStateScript = string(scriptContent)
|
||||
mainStateScriptName = scriptPath
|
||||
mainStateScriptDir = s.scriptDir
|
||||
// Set up state creator now that we have the script
|
||||
s.SetupStateCreator()
|
||||
}
|
||||
|
||||
return s.ExecuteString(string(scriptContent), scriptPath)
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user