diff --git a/go.mod b/go.mod index b11a8df..e3aeedd 100644 --- a/go.mod +++ b/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 diff --git a/modules/http/http.go b/modules/http/http.go new file mode 100644 index 0000000..7647a30 --- /dev/null +++ b/modules/http/http.go @@ -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) + } +} diff --git a/modules/http/http.lua b/modules/http/http.lua new file mode 100644 index 0000000..101f963 --- /dev/null +++ b/modules/http/http.lua @@ -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 \ No newline at end of file diff --git a/modules/http/pool.go b/modules/http/pool.go new file mode 100644 index 0000000..cb50342 --- /dev/null +++ b/modules/http/pool.go @@ -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 +} diff --git a/modules/registry.go b/modules/registry.go index 262d7c4..ee7ccaa 100644 --- a/modules/registry.go +++ b/modules/registry.go @@ -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 diff --git a/moonshark.go b/moonshark.go index 959f905..5bf5a89 100644 --- a/moonshark.go +++ b/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() + } } diff --git a/state/state.go b/state/state.go index fd0b4cd..d3f7ab3 100644 --- a/state/state.go +++ b/state/state.go @@ -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 @@ -48,8 +50,9 @@ func New(config ...Config) (*State, error) { } state := &State{ - State: baseState, - isWorker: cfg.IsWorker, + 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) }