http module success 1

This commit is contained in:
Sky Johnson 2025-07-17 18:56:48 -05:00
parent a110b93f5c
commit 1753007090
7 changed files with 1256 additions and 6 deletions

2
go.mod
View File

@ -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
View 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
View 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
View 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
}

View File

@ -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

View File

@ -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()
}
} }

View File

@ -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)
} }