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 github.com/google/uuid v1.6.0
|
||||||
|
|
||||||
require golang.org/x/text v0.27.0
|
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||||
github.com/klauspost/compress v1.18.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/crypto"
|
||||||
"Moonshark/modules/fs"
|
"Moonshark/modules/fs"
|
||||||
|
"Moonshark/modules/http"
|
||||||
"Moonshark/modules/json"
|
"Moonshark/modules/json"
|
||||||
"Moonshark/modules/math"
|
"Moonshark/modules/math"
|
||||||
lua_string "Moonshark/modules/string"
|
lua_string "Moonshark/modules/string"
|
||||||
@ -17,7 +18,7 @@ import (
|
|||||||
// Global registry instance
|
// Global registry instance
|
||||||
var Global *Registry
|
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
|
var embeddedModules embed.FS
|
||||||
|
|
||||||
// Registry manages all Lua modules and Go functions
|
// 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, math.GetFunctionList())
|
||||||
maps.Copy(r.goFuncs, fs.GetFunctionList())
|
maps.Copy(r.goFuncs, fs.GetFunctionList())
|
||||||
maps.Copy(r.goFuncs, crypto.GetFunctionList())
|
maps.Copy(r.goFuncs, crypto.GetFunctionList())
|
||||||
|
maps.Copy(r.goFuncs, http.GetFunctionList())
|
||||||
|
|
||||||
r.loadEmbeddedModules()
|
r.loadEmbeddedModules()
|
||||||
return r
|
return r
|
||||||
|
22
moonshark.go
22
moonshark.go
@ -3,8 +3,11 @@ package main
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"Moonshark/modules/http"
|
||||||
"Moonshark/state"
|
"Moonshark/state"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -29,4 +32,23 @@ func main() {
|
|||||||
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
fmt.Fprintf(os.Stderr, "Error: %v\n", err)
|
||||||
os.Exit(1)
|
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"
|
"path/filepath"
|
||||||
|
|
||||||
"Moonshark/modules"
|
"Moonshark/modules"
|
||||||
|
"Moonshark/modules/http"
|
||||||
|
|
||||||
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
luajit "git.sharkk.net/Sky/LuaJIT-to-Go"
|
||||||
)
|
)
|
||||||
@ -15,6 +16,7 @@ type State struct {
|
|||||||
*luajit.State
|
*luajit.State
|
||||||
initialized bool
|
initialized bool
|
||||||
isWorker bool
|
isWorker bool
|
||||||
|
scriptDir string
|
||||||
}
|
}
|
||||||
|
|
||||||
// Config holds state initialization options
|
// Config holds state initialization options
|
||||||
@ -48,8 +50,9 @@ func New(config ...Config) (*State, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
state := &State{
|
state := &State{
|
||||||
State: baseState,
|
State: baseState,
|
||||||
isWorker: cfg.IsWorker,
|
isWorker: cfg.IsWorker,
|
||||||
|
scriptDir: cfg.ScriptDir,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set worker global flag if this is a worker state
|
// 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)
|
return New(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Store main state initialization for worker replication
|
||||||
|
var (
|
||||||
|
mainStateScriptDir string
|
||||||
|
mainStateScript string
|
||||||
|
mainStateScriptName string
|
||||||
|
)
|
||||||
|
|
||||||
// initializeModules sets up the module system
|
// initializeModules sets up the module system
|
||||||
func (s *State) initializeModules() error {
|
func (s *State) initializeModules() error {
|
||||||
// Initialize global registry if needed
|
// 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
|
// 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)
|
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)
|
return s.ExecuteString(string(scriptContent), scriptPath)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user