webmcp

annotate framework/bin/mcp.lua @ 567:5e3ebe9fd0ce

Added tag v2.2.1 for changeset 3b71fdb3a00d
author jbe
date Wed Apr 28 13:07:52 2021 +0200 (3 months ago)
parents 7ad7023c91df
children
rev   line source
jbe@203 1 #!/usr/bin/env moonbridge
jbe/bsw@0 2
jbe@294 3 --[[--
jbe@294 4 WEBMCP_VERSION
jbe@294 5
jbe@294 6 A string containing the WebMCP version, e.g. "2.0.0"
jbe@294 7 --]]--
jbe@536 8 WEBMCP_VERSION = "2.2.0"
jbe@294 9 --//--
jbe@64 10
jbe@317 11 --[[--
jbe@294 12 WEBMCP_MODE
jbe@294 13
jbe@294 14 A constant set to "listen" in case of a network request, or set to "interactive" in case of interactive mode.
jbe@294 15 --]]--
jbe@323 16 if _MOONBRIDGE_VERSION then
jbe@203 17 WEBMCP_MODE = "listen"
jbe@203 18 else
jbe@203 19 WEBMCP_MODE = "interactive"
jbe@64 20 end
jbe@294 21 --//--
jbe@203 22
jbe@294 23 --[[--
jbe@294 24 WEBMCP_CONFIG_NAMES
jbe@294 25
jbe@294 26 A list of the selected configuration names.
jbe@294 27 --]]--
jbe@294 28 -- configuration names are provided as 4th, 5th, etc. command line argument
jbe@206 29 WEBMCP_CONFIG_NAMES = {select(4, ...)}
jbe@294 30 --//--
jbe@294 31
jbe@294 32 --[[--
jbe@294 33 WEBMCP_FRAMEWORK_PATH
jbe@294 34
jbe@294 35 Directory of the WebMCP framework (always includes a trailing slash).
jbe@294 36 --]]--
jbe@294 37 -- set in mcp.lua
jbe@294 38 --//--
jbe@294 39
jbe@294 40 --[[--
jbe@294 41 WEBMCP_BASE_PATH
jbe@294 42
jbe@294 43 Base directory of the application (always includes a trailing slash).
jbe@294 44 --]]--
jbe@294 45 -- set in mcp.lua
jbe@294 46 --//--
jbe@294 47
jbe@294 48 --[[--
jbe@294 49 WEBMCP_APP_NAME
jbe@294 50
jbe@294 51 Application name (usually "main"). May be nil in case of interactive mode.
jbe@294 52 --]]--
jbe@294 53 -- set in mcp.lua
jbe@294 54 --//--
jbe@203 55
jbe@203 56 -- determine framework and bath path from command line arguments
jbe@203 57 -- or print usage synopsis (if applicable)
jbe@68 58 do
jbe@206 59 local arg1, arg2, arg3 = ...
jbe@203 60 local helpout
jbe@203 61 if
jbe@203 62 arg1 == "-h" or arg1 == "--help" or
jbe@206 63 arg2 == "-h" or arg2 == "--help" -- if first arg is provided by wrapper
jbe@203 64 then
jbe@203 65 helpout = io.stdout
jbe@316 66 elseif #WEBMCP_CONFIG_NAMES < 1 then
jbe@203 67 helpout = io.stderr
jbe@203 68 end
jbe@203 69 if helpout then
jbe@316 70 helpout:write("Usage: moonbridge [moonbr opts] -- <framework path>/bin/mcp.lua <framework path> <app base path> <app name> <config name> [<config name> ...]\n")
jbe@316 71 helpout:write(" or: lua -i [Lua opts] -- <framework path>/bin/mcp.lua <framework path> <app base path> <app name> <config name> [<config name> ...]\n")
jbe@203 72 if helpout == io.stderr then
jbe@203 73 return 1
jbe@68 74 else
jbe@203 75 return 0
jbe@68 76 end
jbe@68 77 end
jbe@203 78 local function append_trailing_slash(str)
jbe@385 79 return (string.gsub(str, "([^/])$", function(last) return last .. "/" end))
jbe@203 80 end
jbe@203 81 WEBMCP_FRAMEWORK_PATH = append_trailing_slash(arg1)
jbe@203 82 WEBMCP_BASE_PATH = append_trailing_slash(arg2)
jbe@316 83 WEBMCP_APP_NAME = arg3
jbe@68 84 end
jbe@1 85
jbe@203 86 -- setup search paths for libraries
jbe/bsw@0 87 do
jbe@217 88 if string.match(package.path, "^[^;]") then
jbe@217 89 package.path = ";" .. package.path
jbe@217 90 end
jbe@217 91 package.path = WEBMCP_FRAMEWORK_PATH .. "lib/?.lua" .. package.path
jbe/bsw@0 92 -- find out which file name extension shared libraries have
jbe/bsw@0 93 local slib_exts = {}
jbe/bsw@0 94 for ext in string.gmatch(package.cpath, "%?%.([A-Za-z0-9_-]+)") do
jbe@217 95 if not slib_exts[ext] then
jbe@217 96 slib_exts[#slib_exts+1] = ext
jbe@217 97 slib_exts[ext] = true
jbe@217 98 end
jbe/bsw@0 99 end
jbe/bsw@0 100 local paths = {}
jbe@217 101 for i, ext in ipairs(slib_exts) do
jbe@203 102 paths[#paths+1] = WEBMCP_FRAMEWORK_PATH .. "accelerator/?." .. ext
jbe/bsw@0 103 end
jbe@217 104 for i, ext in ipairs(slib_exts) do
jbe@203 105 paths[#paths+1] = WEBMCP_FRAMEWORK_PATH .. "lib/?." .. ext
jbe/bsw@0 106 end
jbe/bsw@0 107 paths[#paths+1] = package.cpath
jbe/bsw@0 108 package.cpath = table.concat(paths, ";")
jbe/bsw@0 109 end
jbe/bsw@0 110
jbe@352 111 -- load "extos" library (needed by function "loadcached")
jbe@352 112 _G.extos = require "extos"
jbe@352 113
jbe@352 114 --[[--
jbe@352 115 _G
jbe@352 116
jbe@352 117 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 118
jbe@352 119 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 120
jbe@352 121 --]]--
jbe@352 122 local _G = _G
jbe@352 123 local allowed_globals = {}
jbe@352 124 local protected_environment = setmetatable(
jbe@352 125 {}, -- proxy environment used all chunks loaded through loadcached(...)
jbe@352 126 {
jbe@352 127 __index = _G,
jbe@352 128 __newindex = function(self, key, value)
jbe@352 129 if allowed_globals[key] then
jbe@352 130 _G[key] = value
jbe@352 131 else
jbe@352 132 if type(key) == "string" and string.match(key, "^[A-Za-z_][A-Za-z_0-9]*$") then
jbe@352 133 error('Attempt to set global variable "' .. key .. '" (Hint: missing local statement? Use _G.' .. key .. '=<value> to really set global variable.)', 2)
jbe@352 134 else
jbe@352 135 error('Attempt to set global variable', 2)
jbe@352 136 end
jbe@352 137 end
jbe@352 138 end
jbe@352 139 }
jbe@352 140 )
jbe@352 141 --//--
jbe@352 142
jbe@352 143 --[[--
jbe@354 144 lua_func, -- compiled Lua function, nil if the file does not exist
jbe@354 145 errmsg = -- error message (only for non-existing file, other errors are thrown)
jbe@352 146 loadcached(
jbe@352 147 filename -- path to a Lua source or byte-code file
jbe@352 148 )
jbe@352 149
jbe@352 150 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 151
jbe@352 152 --]]--
jbe@352 153 do
jbe@352 154 local cache = {}
jbe@352 155 function loadcached(filename)
jbe@352 156 local cached_func = cache[filename]
jbe@352 157 if cached_func then
jbe@352 158 return cached_func
jbe@352 159 end
jbe@352 160 local stat, errmsg = extos.stat(filename)
jbe@352 161 if stat == nil then
jbe@352 162 error(errmsg)
jbe@352 163 elseif stat == false then
jbe@352 164 return nil, 'File "' .. filename .. '" does not exist'
jbe@352 165 elseif stat.isdir then
jbe@352 166 error('File "' .. filename .. '" is a directory')
jbe@352 167 elseif not stat.isreg then
jbe@352 168 error('File "' .. filename .. '" is not a regular file')
jbe@352 169 end
jbe@352 170 local func, compile_error = loadfile(filename, nil, protected_environment)
jbe@352 171 if func then
jbe@352 172 cache[filename] = func
jbe@352 173 return func
jbe@352 174 end
jbe@352 175 error(compile_error, 0)
jbe@352 176 end
jbe@352 177 end
jbe@352 178 --//--
jbe@352 179
jbe@352 180 -- check if framework path is correct
jbe@352 181 do
jbe@352 182 local file, errmsg = io.open(WEBMCP_FRAMEWORK_PATH .. "webmcp_version", "r")
jbe@352 183 if not file then
jbe@352 184 error('Could not find "webmcp_version" file: ' .. errmsg, 0)
jbe@352 185 end
jbe@352 186 local version = assert(file:read())
jbe@352 187 assert(file:close())
jbe@352 188 if version ~= WEBMCP_VERSION then
jbe@352 189 error('Version mismatch in file "' .. WEBMCP_FRAMEWORK_PATH .. 'webmcp_version"')
jbe@352 190 end
jbe@352 191 end
jbe@352 192
jbe@203 193 -- autoloader system for WebMCP environment "$WEBMCP_FRAMEWORK_PATH/env/",
jbe@203 194 -- application environment extensions "$WEBMCP_BASE_PATH/env/"
jbe@203 195 -- and models "$WEBMCP_BASE_PATH/model/"
jbe/bsw@0 196 do
jbe/bsw@0 197 local weakkey_mt = { __mode = "k" }
jbe/bsw@0 198 local autoloader_category = setmetatable({}, weakkey_mt)
jbe/bsw@0 199 local autoloader_path = setmetatable({}, weakkey_mt)
jbe/bsw@0 200 local autoloader_mt = {}
jbe@219 201 local function install_autoloader(self, category, path_fragment)
jbe/bsw@0 202 autoloader_category[self] = category
jbe@219 203 autoloader_path[self] = path_fragment
jbe/bsw@0 204 setmetatable(self, autoloader_mt)
jbe/bsw@0 205 end
jbe/bsw@0 206 local function try_exec(filename)
jbe@309 207 local func = loadcached(filename)
jbe@309 208 if func then
jbe@309 209 func()
jbe@309 210 return true
jbe/bsw@0 211 else
jbe/bsw@0 212 return false
jbe/bsw@0 213 end
jbe/bsw@0 214 end
jbe/bsw@0 215 function autoloader_mt.__index(self, key)
jbe/bsw@0 216 local category, base_path, merge_base_path, file_key
jbe/bsw@0 217 local merge = false
jbe/bsw@0 218 if
jbe/bsw@0 219 string.find(key, "^[a-z_][A-Za-z0-9_]*$") and
jbe/bsw@0 220 not string.find(key, "^__")
jbe/bsw@0 221 then
jbe/bsw@0 222 category = "env"
jbe@203 223 base_path = WEBMCP_FRAMEWORK_PATH .. "env/"
jbe/bsw@0 224 merge = true
jbe@203 225 merge_base_path = WEBMCP_BASE_PATH .. "env/"
jbe/bsw@0 226 file_key = key
jbe/bsw@0 227 elseif string.find(key, "^[A-Z][A-Za-z0-9]*$") then
jbe/bsw@0 228 category = "model"
jbe@203 229 base_path = WEBMCP_BASE_PATH .. "model/"
jbe/bsw@0 230 local first = true
jbe/bsw@0 231 file_key = string.gsub(key, "[A-Z]",
jbe/bsw@0 232 function(c)
jbe/bsw@0 233 if first then
jbe/bsw@0 234 first = false
jbe/bsw@0 235 return string.lower(c)
jbe/bsw@0 236 else
jbe/bsw@0 237 return "_" .. string.lower(c)
jbe/bsw@0 238 end
jbe/bsw@0 239 end
jbe/bsw@0 240 )
jbe/bsw@0 241 else
jbe/bsw@0 242 return
jbe/bsw@0 243 end
jbe/bsw@0 244 local required_category = autoloader_category[self]
jbe/bsw@0 245 if required_category and required_category ~= category then return end
jbe@219 246 local path_fragment = autoloader_path[self]
jbe@219 247 local path = base_path .. path_fragment .. file_key
jbe@219 248 local merge_path
jbe/bsw@0 249 if merge then
jbe@219 250 merge_path = merge_base_path .. path_fragment .. file_key
jbe/bsw@0 251 end
jbe/bsw@0 252 local function try_dir(dirname)
jbe/bsw@0 253 local dir = io.open(dirname)
jbe/bsw@0 254 if dir then
jbe/bsw@0 255 io.close(dir)
jbe/bsw@0 256 local obj = {}
jbe@219 257 install_autoloader(obj, category, path_fragment .. file_key .. "/")
jbe/bsw@0 258 rawset(self, key, obj)
jbe@219 259 try_exec(path .. "/__init.lua")
jbe@219 260 if merge then try_exec(merge_path .. "/__init.lua") end
jbe/bsw@0 261 return true
jbe/bsw@0 262 else
jbe/bsw@0 263 return false
jbe/bsw@0 264 end
jbe/bsw@0 265 end
jbe@238 266 if self == _G then
jbe@237 267 allowed_globals[key] = true
jbe@233 268 end
jbe@219 269 if merge and try_exec(merge_path .. ".lua") then
jbe@219 270 elseif merge and try_dir(merge_path .. "/") then
jbe@219 271 elseif try_exec(path .. ".lua") then
jbe@219 272 elseif try_dir(path .. "/") then
jbe/bsw@0 273 else end
jbe@238 274 if self == _G then
jbe@237 275 allowed_globals[key] = nil
jbe@233 276 end
jbe@237 277 return rawget(self, key)
jbe/bsw@0 278 end
jbe@219 279 install_autoloader(_G, nil, "")
jbe@324 280 try_exec(WEBMCP_FRAMEWORK_PATH .. "env/__init.lua")
jbe@324 281 try_exec(WEBMCP_BASE_PATH .. "env/__init.lua")
jbe@214 282 end
jbe@214 283
jbe@286 284 -- define post-fork initialization function (including loading of "multirand" library)
jbe@286 285 local function postfork_init()
jbe@324 286 multirand = require "multirand"
jbe@286 287 execute.postfork_initializers()
jbe@286 288 end
jbe@286 289
jbe@336 290 --[[--
jbe@336 291 listen{
jbe@336 292 {
jbe@474 293 proto = proto, -- "local", "tcp", "interval", or "main"
jbe@474 294 path = path, -- path to unix domain socket if proto == "local"
jbe@474 295 port = port, -- TCP port number
jbe@474 296 host = host, -- "::" for all IPv6 interfaces, "0.0.0.0" for all IPv4 interfaces
jbe@474 297 name = name, -- optional name for main handlers or interval handlers (may be useful for log output)
jbe@474 298 handler = handler, -- handler if proto == "interval" or proto == "main"
jbe@474 299 delay = delay, -- delay between invocations of interval handler
jbe@474 300 strict = strict -- set to true to substract runtime of interval handler from delay
jbe@336 301 },
jbe@336 302 {
jbe@336 303 ... -- second listener
jbe@336 304 },
jbe@336 305 ... -- more listeners
jbe@336 306 -- the following options are all optional and have default values:
jbe@336 307 pre_fork = pre_fork, -- desired number of spare (idle) processes
jbe@336 308 min_fork = min_fork, -- minimum number of processes
jbe@336 309 max_fork = max_fork, -- maximum number of processes (hard limit)
jbe@336 310 fork_delay = fork_delay, -- delay (seconds) between creation of spare processes
jbe@336 311 fork_error_delay = fork_error_delay, -- delay (seconds) before retry of failed process creation
jbe@336 312 exit_delay = exit_delay, -- delay (seconds) between destruction of excessive spare processes
jbe@336 313 idle_timeout = idle_timeout, -- idle time (seconds) after a fork gets terminated (0 for no timeout)
jbe@336 314 memory_limit = memory_limit, -- maximum memory consumption (bytes) before process gets terminated
jbe@336 315 min_requests_per_fork = min_requests_per_fork, -- minimum count of requests handled before fork is terminated
jbe@336 316 max_requests_per_fork = max_requests_per_fork, -- maximum count of requests handled before fork is terminated
jbe@336 317 http_options = {
jbe@337 318 static_headers = static_headers, -- string or table of static headers to be returned with every request
jbe@337 319 request_header_size_limit = request_header_size_limit, -- maximum size of request headers sent by client
jbe@337 320 request_body_size_limit = request_body_size_limit, -- maximum size of request body sent by client
jbe@367 321 idle_timeout = idle_timeout, -- maximum time until receiving the first byte of the request header
jbe@367 322 stall_timeout = stall_timeout, -- maximum time a client connection may be stalled
jbe@366 323 request_header_timeout = request_header_timeout, -- maximum time until receiving the remaining bytes of the request header
jbe@366 324 response_timeout = response_timeout, -- time in which request body and response must be sent
jbe@338 325 maximum_input_chunk_size = maximum_input_chunk_size, -- tweaks behavior of request-body parser
jbe@337 326 minimum_output_chunk_size = minimum_output_chunk_size -- chunk size for chunked-transfer-encoding
jbe@336 327 }
jbe@336 328 }
jbe@336 329
jbe@336 330 The listen{...} function determines on which TCP port an application is answering requests. A typical call looks as follows:
jbe@336 331
jbe@336 332 listen{
jbe@336 333 { proto = "tcp4", port = 8080, localhost = true },
jbe@336 334 { proto = "tcp6", port = 8080, localhost = true }
jbe@336 335 }
jbe@336 336
jbe@336 337 This function must be called in a configuration file (in the config/ directory) or in pre-fork initializers (in the app/_prefork/ or app/<application name>/_prefork/ directories), unless WebMCP is invoked in interactive mode (in which case any calls of listen{...} are ignored).
jbe@336 338
jbe@485 339 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 and main routines, a handler may be specified in each listener. If a main handler returns, the WebMCP system will shut down. Main handlers get the function moonbridge_io.poll passed as first argument.
jbe@480 340
jbe@336 341 --]]--
jbe@317 342 -- prepare for interactive or listen mode
jbe@203 343 if WEBMCP_MODE == "interactive" then
jbe@317 344 function listen() -- overwrite Moonbridge's listen function
jbe@317 345 -- ignore listen function calls for interactive mode
jbe@317 346 end
jbe@317 347 trace.disable() -- avoids memory leakage when scripts are running endlessly
jbe@317 348 else
jbe@317 349 local moonbridge_listen = listen
jbe@207 350 local http = require("moonbridge_http")
jbe@317 351 function listen(args) -- overwrite Moonbridge's listen function
jbe@322 352 assert(args, "No argument passed to listen function")
jbe@322 353 local min_requests_per_fork = args.min_requests_per_fork or 50
jbe@335 354 local max_requests_per_fork = args.max_requests_per_fork or 200
jbe@473 355 local main_handlers = {}
jbe@316 356 local interval_handlers = {}
jbe@317 357 for j, listener in ipairs(args) do
jbe@473 358 if listener.proto == "main" then
jbe@473 359 local name = listener.name or "Unnamed main thread #" .. #main_handlers+1
jbe@473 360 if main_handlers[name] ~= nil then
jbe@473 361 error('Main thread handler with duplicate name "' .. name .. '"')
jbe@473 362 end
jbe@473 363 main_handlers[name] = listener.handler
jbe@473 364 listener.name = name
jbe@473 365 elseif listener.proto == "interval" then
jbe@317 366 local name = listener.name or "Unnamed interval #" .. #interval_handlers+1
jbe@336 367 if interval_handlers[name] ~= nil then
jbe@336 368 error('Interval handler with duplicate name "' .. name .. '"')
jbe@336 369 end
jbe@317 370 interval_handlers[name] = listener.handler
jbe@317 371 listener.name = name
jbe@316 372 end
jbe@316 373 end
jbe@264 374 local request_count = 0
jbe@266 375 local function inner_handler(http_request)
jbe@328 376 request_count = request_count + 1
jbe@328 377 if request_count >= max_requests_per_fork then
jbe@328 378 http_request:close_after_finish()
jbe@328 379 end
jbe@317 380 request.initialize()
jbe@328 381 return request.handler(http_request)
jbe@264 382 end
jbe@326 383 local outer_handler = http.generate_handler(inner_handler, args.http_options)
jbe@317 384 args.prepare = postfork_init
jbe@317 385 args.connect = function(socket)
jbe@473 386 if socket.main then
jbe@473 387 request.initialize()
jbe@484 388 main_handlers[socket.main](moonbridge_io.poll)
jbe@484 389 io.stderr:write('Main handler "' .. socket.main .. '" terminated.\n')
jbe@473 390 return false
jbe@473 391 elseif socket.interval then
jbe@328 392 request_count = request_count + 1
jbe@317 393 request.initialize()
jbe@316 394 interval_handlers[socket.interval]()
jbe@316 395 else
jbe@327 396 local success = outer_handler(socket)
jbe@327 397 if not success then
jbe@327 398 return false
jbe@327 399 end
jbe@316 400 end
jbe@288 401 return request_count < min_requests_per_fork
jbe@264 402 end
jbe@317 403 args.finish = execute.finalizers
jbe@317 404 moonbridge_listen(args)
jbe@204 405 end
jbe@204 406 end
jbe@336 407 --//--
jbe@317 408
jbe@317 409 -- execute configurations and pre-fork initializers
jbe@317 410 for i, config_name in ipairs(WEBMCP_CONFIG_NAMES) do
jbe@317 411 execute.config(config_name)
jbe@317 412 end
jbe@317 413 execute.prefork_initializers()
jbe@317 414
jbe@324 415 -- perform post-fork initializations (once) in case of interactive mode
jbe@317 416 if WEBMCP_MODE == "interactive" then
jbe@317 417 postfork_init()
jbe@317 418 end

Impressum / About Us