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