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