jbe@203: #!/usr/bin/env moonbridge jbe/bsw@0: jbe@294: --[[-- jbe@294: WEBMCP_VERSION jbe@294: jbe@294: A string containing the WebMCP version, e.g. "2.0.0" jbe@294: --]]-- jbe@388: WEBMCP_VERSION = "2.0.4" jbe@294: --//-- jbe@64: jbe@317: --[[-- jbe@294: WEBMCP_MODE jbe@294: jbe@294: A constant set to "listen" in case of a network request, or set to "interactive" in case of interactive mode. jbe@294: --]]-- jbe@323: if _MOONBRIDGE_VERSION then jbe@203: WEBMCP_MODE = "listen" jbe@203: else jbe@203: WEBMCP_MODE = "interactive" jbe@64: end jbe@294: --//-- jbe@203: jbe@294: --[[-- jbe@294: WEBMCP_CONFIG_NAMES jbe@294: jbe@294: A list of the selected configuration names. jbe@294: --]]-- jbe@294: -- configuration names are provided as 4th, 5th, etc. command line argument jbe@206: WEBMCP_CONFIG_NAMES = {select(4, ...)} jbe@294: --//-- jbe@294: jbe@294: --[[-- jbe@294: WEBMCP_FRAMEWORK_PATH jbe@294: jbe@294: Directory of the WebMCP framework (always includes a trailing slash). jbe@294: --]]-- jbe@294: -- set in mcp.lua jbe@294: --//-- jbe@294: jbe@294: --[[-- jbe@294: WEBMCP_BASE_PATH jbe@294: jbe@294: Base directory of the application (always includes a trailing slash). jbe@294: --]]-- jbe@294: -- set in mcp.lua jbe@294: --//-- jbe@294: jbe@294: --[[-- jbe@294: WEBMCP_APP_NAME jbe@294: jbe@294: Application name (usually "main"). May be nil in case of interactive mode. jbe@294: --]]-- jbe@294: -- set in mcp.lua jbe@294: --//-- jbe@203: jbe@203: -- determine framework and bath path from command line arguments jbe@203: -- or print usage synopsis (if applicable) jbe@68: do jbe@206: local arg1, arg2, arg3 = ... jbe@203: local helpout jbe@203: if jbe@203: arg1 == "-h" or arg1 == "--help" or jbe@206: arg2 == "-h" or arg2 == "--help" -- if first arg is provided by wrapper jbe@203: then jbe@203: helpout = io.stdout jbe@316: elseif #WEBMCP_CONFIG_NAMES < 1 then jbe@203: helpout = io.stderr jbe@203: end jbe@203: if helpout then jbe@316: helpout:write("Usage: moonbridge [moonbr opts] -- /bin/mcp.lua [ ...]\n") jbe@316: helpout:write(" or: lua -i [Lua opts] -- /bin/mcp.lua [ ...]\n") jbe@203: if helpout == io.stderr then jbe@203: return 1 jbe@68: else jbe@203: return 0 jbe@68: end jbe@68: end jbe@203: local function append_trailing_slash(str) jbe@385: return (string.gsub(str, "([^/])$", function(last) return last .. "/" end)) jbe@203: end jbe@203: WEBMCP_FRAMEWORK_PATH = append_trailing_slash(arg1) jbe@203: WEBMCP_BASE_PATH = append_trailing_slash(arg2) jbe@316: WEBMCP_APP_NAME = arg3 jbe@68: end jbe@1: jbe@203: -- setup search paths for libraries jbe/bsw@0: do jbe@217: if string.match(package.path, "^[^;]") then jbe@217: package.path = ";" .. package.path jbe@217: end jbe@217: package.path = WEBMCP_FRAMEWORK_PATH .. "lib/?.lua" .. package.path jbe/bsw@0: -- find out which file name extension shared libraries have jbe/bsw@0: local slib_exts = {} jbe/bsw@0: for ext in string.gmatch(package.cpath, "%?%.([A-Za-z0-9_-]+)") do jbe@217: if not slib_exts[ext] then jbe@217: slib_exts[#slib_exts+1] = ext jbe@217: slib_exts[ext] = true jbe@217: end jbe/bsw@0: end jbe/bsw@0: local paths = {} jbe@217: for i, ext in ipairs(slib_exts) do jbe@203: paths[#paths+1] = WEBMCP_FRAMEWORK_PATH .. "accelerator/?." .. ext jbe/bsw@0: end jbe@217: for i, ext in ipairs(slib_exts) do jbe@203: paths[#paths+1] = WEBMCP_FRAMEWORK_PATH .. "lib/?." .. ext jbe/bsw@0: end jbe/bsw@0: paths[#paths+1] = package.cpath jbe/bsw@0: package.cpath = table.concat(paths, ";") jbe/bsw@0: end jbe/bsw@0: jbe@352: -- load "extos" library (needed by function "loadcached") jbe@352: _G.extos = require "extos" jbe@352: jbe@352: --[[-- jbe@352: _G jbe@352: jbe@352: A reference to the global namespace. To avoid accidental programming errors, global variables cannot be set directly, but they must be set through the _G reference, e.g. use _G.foo = true to set the variable "foo" to a value of true. jbe@352: jbe@352: Note that the global namespace may or may not be shared between requests (Moonbridge creates multiple forks of the Lua machine). To set variables that are to be cleared after the request has been finished, an application may use the "app" table, e.g. app.foo = true to set the variable app.foo to a value of true, which will be cleared automatically when the request has ended. jbe@352: jbe@352: --]]-- jbe@352: local _G = _G jbe@352: local allowed_globals = {} jbe@352: local protected_environment = setmetatable( jbe@352: {}, -- proxy environment used all chunks loaded through loadcached(...) jbe@352: { jbe@352: __index = _G, jbe@352: __newindex = function(self, key, value) jbe@352: if allowed_globals[key] then jbe@352: _G[key] = value jbe@352: else jbe@352: if type(key) == "string" and string.match(key, "^[A-Za-z_][A-Za-z_0-9]*$") then jbe@352: error('Attempt to set global variable "' .. key .. '" (Hint: missing local statement? Use _G.' .. key .. '= to really set global variable.)', 2) jbe@352: else jbe@352: error('Attempt to set global variable', 2) jbe@352: end jbe@352: end jbe@352: end jbe@352: } jbe@352: ) jbe@352: --//-- jbe@352: jbe@352: --[[-- jbe@354: lua_func, -- compiled Lua function, nil if the file does not exist jbe@354: errmsg = -- error message (only for non-existing file, other errors are thrown) jbe@352: loadcached( jbe@352: filename -- path to a Lua source or byte-code file jbe@352: ) jbe@352: jbe@352: Loads, compiles and caches a Lua chunk. The cached value (i.e. the compiled function) is returned. If the file does not exist, nil and an error string are returned. Any other errors are thrown using error(...). Unsuccessful attempts are not cached (to prohibit cache pollution). jbe@352: jbe@352: --]]-- jbe@352: do jbe@352: local cache = {} jbe@352: function loadcached(filename) jbe@352: local cached_func = cache[filename] jbe@352: if cached_func then jbe@352: return cached_func jbe@352: end jbe@352: local stat, errmsg = extos.stat(filename) jbe@352: if stat == nil then jbe@352: error(errmsg) jbe@352: elseif stat == false then jbe@352: return nil, 'File "' .. filename .. '" does not exist' jbe@352: elseif stat.isdir then jbe@352: error('File "' .. filename .. '" is a directory') jbe@352: elseif not stat.isreg then jbe@352: error('File "' .. filename .. '" is not a regular file') jbe@352: end jbe@352: local func, compile_error = loadfile(filename, nil, protected_environment) jbe@352: if func then jbe@352: cache[filename] = func jbe@352: return func jbe@352: end jbe@352: error(compile_error, 0) jbe@352: end jbe@352: end jbe@352: --//-- jbe@352: jbe@352: -- check if framework path is correct jbe@352: do jbe@352: local file, errmsg = io.open(WEBMCP_FRAMEWORK_PATH .. "webmcp_version", "r") jbe@352: if not file then jbe@352: error('Could not find "webmcp_version" file: ' .. errmsg, 0) jbe@352: end jbe@352: local version = assert(file:read()) jbe@352: assert(file:close()) jbe@352: if version ~= WEBMCP_VERSION then jbe@352: error('Version mismatch in file "' .. WEBMCP_FRAMEWORK_PATH .. 'webmcp_version"') jbe@352: end jbe@352: end jbe@352: jbe@203: -- autoloader system for WebMCP environment "$WEBMCP_FRAMEWORK_PATH/env/", jbe@203: -- application environment extensions "$WEBMCP_BASE_PATH/env/" jbe@203: -- and models "$WEBMCP_BASE_PATH/model/" jbe/bsw@0: do jbe/bsw@0: local weakkey_mt = { __mode = "k" } jbe/bsw@0: local autoloader_category = setmetatable({}, weakkey_mt) jbe/bsw@0: local autoloader_path = setmetatable({}, weakkey_mt) jbe/bsw@0: local autoloader_mt = {} jbe@219: local function install_autoloader(self, category, path_fragment) jbe/bsw@0: autoloader_category[self] = category jbe@219: autoloader_path[self] = path_fragment jbe/bsw@0: setmetatable(self, autoloader_mt) jbe/bsw@0: end jbe/bsw@0: local function try_exec(filename) jbe@309: local func = loadcached(filename) jbe@309: if func then jbe@309: func() jbe@309: return true jbe/bsw@0: else jbe/bsw@0: return false jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: function autoloader_mt.__index(self, key) jbe/bsw@0: local category, base_path, merge_base_path, file_key jbe/bsw@0: local merge = false jbe/bsw@0: if jbe/bsw@0: string.find(key, "^[a-z_][A-Za-z0-9_]*$") and jbe/bsw@0: not string.find(key, "^__") jbe/bsw@0: then jbe/bsw@0: category = "env" jbe@203: base_path = WEBMCP_FRAMEWORK_PATH .. "env/" jbe/bsw@0: merge = true jbe@203: merge_base_path = WEBMCP_BASE_PATH .. "env/" jbe/bsw@0: file_key = key jbe/bsw@0: elseif string.find(key, "^[A-Z][A-Za-z0-9]*$") then jbe/bsw@0: category = "model" jbe@203: base_path = WEBMCP_BASE_PATH .. "model/" jbe/bsw@0: local first = true jbe/bsw@0: file_key = string.gsub(key, "[A-Z]", jbe/bsw@0: function(c) jbe/bsw@0: if first then jbe/bsw@0: first = false jbe/bsw@0: return string.lower(c) jbe/bsw@0: else jbe/bsw@0: return "_" .. string.lower(c) jbe/bsw@0: end jbe/bsw@0: end jbe/bsw@0: ) jbe/bsw@0: else jbe/bsw@0: return jbe/bsw@0: end jbe/bsw@0: local required_category = autoloader_category[self] jbe/bsw@0: if required_category and required_category ~= category then return end jbe@219: local path_fragment = autoloader_path[self] jbe@219: local path = base_path .. path_fragment .. file_key jbe@219: local merge_path jbe/bsw@0: if merge then jbe@219: merge_path = merge_base_path .. path_fragment .. file_key jbe/bsw@0: end jbe/bsw@0: local function try_dir(dirname) jbe/bsw@0: local dir = io.open(dirname) jbe/bsw@0: if dir then jbe/bsw@0: io.close(dir) jbe/bsw@0: local obj = {} jbe@219: install_autoloader(obj, category, path_fragment .. file_key .. "/") jbe/bsw@0: rawset(self, key, obj) jbe@219: try_exec(path .. "/__init.lua") jbe@219: if merge then try_exec(merge_path .. "/__init.lua") end jbe/bsw@0: return true jbe/bsw@0: else jbe/bsw@0: return false jbe/bsw@0: end jbe/bsw@0: end jbe@238: if self == _G then jbe@237: allowed_globals[key] = true jbe@233: end jbe@219: if merge and try_exec(merge_path .. ".lua") then jbe@219: elseif merge and try_dir(merge_path .. "/") then jbe@219: elseif try_exec(path .. ".lua") then jbe@219: elseif try_dir(path .. "/") then jbe/bsw@0: else end jbe@238: if self == _G then jbe@237: allowed_globals[key] = nil jbe@233: end jbe@237: return rawget(self, key) jbe/bsw@0: end jbe@219: install_autoloader(_G, nil, "") jbe@324: try_exec(WEBMCP_FRAMEWORK_PATH .. "env/__init.lua") jbe@324: try_exec(WEBMCP_BASE_PATH .. "env/__init.lua") jbe@214: end jbe@214: jbe@286: -- define post-fork initialization function (including loading of "multirand" library) jbe@286: local function postfork_init() jbe@324: multirand = require "multirand" jbe@286: execute.postfork_initializers() jbe@286: end jbe@286: jbe@336: --[[-- jbe@336: listen{ jbe@336: { jbe@336: proto = proto, -- "local", "tcp4", "tcp6", or "interval" jbe@336: path = path, -- path to unix domain socket if proto == "local" jbe@336: port = port, -- TCP port number jbe@336: localhost = localhost_only, -- set to true to only listen on localhost (127.0.0.1 or ::1) interface jbe@336: name = interval_name, -- optional interval name (may be useful for log output) jbe@336: handler = interval_handler -- interval handler if proto == "interval" jbe@336: }, jbe@336: { jbe@336: ... -- second listener jbe@336: }, jbe@336: ... -- more listeners jbe@336: -- the following options are all optional and have default values: jbe@336: pre_fork = pre_fork, -- desired number of spare (idle) processes jbe@336: min_fork = min_fork, -- minimum number of processes jbe@336: max_fork = max_fork, -- maximum number of processes (hard limit) jbe@336: fork_delay = fork_delay, -- delay (seconds) between creation of spare processes jbe@336: fork_error_delay = fork_error_delay, -- delay (seconds) before retry of failed process creation jbe@336: exit_delay = exit_delay, -- delay (seconds) between destruction of excessive spare processes jbe@336: idle_timeout = idle_timeout, -- idle time (seconds) after a fork gets terminated (0 for no timeout) jbe@336: memory_limit = memory_limit, -- maximum memory consumption (bytes) before process gets terminated jbe@336: min_requests_per_fork = min_requests_per_fork, -- minimum count of requests handled before fork is terminated jbe@336: max_requests_per_fork = max_requests_per_fork, -- maximum count of requests handled before fork is terminated jbe@336: http_options = { jbe@337: static_headers = static_headers, -- string or table of static headers to be returned with every request jbe@337: request_header_size_limit = request_header_size_limit, -- maximum size of request headers sent by client jbe@337: request_body_size_limit = request_body_size_limit, -- maximum size of request body sent by client jbe@367: idle_timeout = idle_timeout, -- maximum time until receiving the first byte of the request header jbe@367: stall_timeout = stall_timeout, -- maximum time a client connection may be stalled jbe@366: request_header_timeout = request_header_timeout, -- maximum time until receiving the remaining bytes of the request header jbe@366: response_timeout = response_timeout, -- time in which request body and response must be sent jbe@338: maximum_input_chunk_size = maximum_input_chunk_size, -- tweaks behavior of request-body parser jbe@337: minimum_output_chunk_size = minimum_output_chunk_size -- chunk size for chunked-transfer-encoding jbe@336: } jbe@336: } jbe@336: jbe@336: The listen{...} function determines on which TCP port an application is answering requests. A typical call looks as follows: jbe@336: jbe@336: listen{ jbe@336: { proto = "tcp4", port = 8080, localhost = true }, jbe@336: { proto = "tcp6", port = 8080, localhost = true } jbe@336: } jbe@336: jbe@336: This function must be called in a configuration file (in the config/ directory) or in pre-fork initializers (in the app/_prefork/ or app//_prefork/ directories), unless WebMCP is invoked in interactive mode (in which case any calls of listen{...} are ignored). jbe@336: jbe@336: This function is a variant of Moonbridge's listen{...} function which has been wrapped for WebMCP. No "prepare", "conenct", or "finish" handler can be set. Instead WebMCP automatically dispatches incoming connections. For interval timers, an interval handler may be specified in each listener. jbe@336: jbe@336: --]]-- jbe@317: -- prepare for interactive or listen mode jbe@203: if WEBMCP_MODE == "interactive" then jbe@317: function listen() -- overwrite Moonbridge's listen function jbe@317: -- ignore listen function calls for interactive mode jbe@317: end jbe@317: trace.disable() -- avoids memory leakage when scripts are running endlessly jbe@317: else jbe@317: local moonbridge_listen = listen jbe@207: local http = require("moonbridge_http") jbe@317: function listen(args) -- overwrite Moonbridge's listen function jbe@322: assert(args, "No argument passed to listen function") jbe@322: local min_requests_per_fork = args.min_requests_per_fork or 50 jbe@335: local max_requests_per_fork = args.max_requests_per_fork or 200 jbe@316: local interval_handlers = {} jbe@317: for j, listener in ipairs(args) do jbe@317: if listener.proto == "interval" then jbe@317: local name = listener.name or "Unnamed interval #" .. #interval_handlers+1 jbe@336: if interval_handlers[name] ~= nil then jbe@336: error('Interval handler with duplicate name "' .. name .. '"') jbe@336: end jbe@317: interval_handlers[name] = listener.handler jbe@317: listener.name = name jbe@316: end jbe@316: end jbe@264: local request_count = 0 jbe@266: local function inner_handler(http_request) jbe@328: request_count = request_count + 1 jbe@328: if request_count >= max_requests_per_fork then jbe@328: http_request:close_after_finish() jbe@328: end jbe@317: request.initialize() jbe@328: return request.handler(http_request) jbe@264: end jbe@326: local outer_handler = http.generate_handler(inner_handler, args.http_options) jbe@317: args.prepare = postfork_init jbe@317: args.connect = function(socket) jbe@316: if socket.interval then jbe@328: request_count = request_count + 1 jbe@317: request.initialize() jbe@316: interval_handlers[socket.interval]() jbe@316: else jbe@327: local success = outer_handler(socket) jbe@327: if not success then jbe@327: return false jbe@327: end jbe@316: end jbe@288: return request_count < min_requests_per_fork jbe@264: end jbe@317: args.finish = execute.finalizers jbe@317: moonbridge_listen(args) jbe@204: end jbe@204: end jbe@336: --//-- jbe@317: jbe@317: -- execute configurations and pre-fork initializers jbe@317: for i, config_name in ipairs(WEBMCP_CONFIG_NAMES) do jbe@317: execute.config(config_name) jbe@317: end jbe@317: execute.prefork_initializers() jbe@317: jbe@324: -- perform post-fork initializations (once) in case of interactive mode jbe@317: if WEBMCP_MODE == "interactive" then jbe@317: postfork_init() jbe@317: end