| 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@278 | 8 WEBMCP_VERSION = "2.0.0" | 
| jbe@294 | 9 --//-- | 
| jbe@64 | 10 | 
| jbe@317 | 11 --[[-- | 
| jbe@317 | 12 _G | 
| jbe@317 | 13 | 
| jbe@317 | 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. | 
| jbe@317 | 15 | 
| jbe@317 | 16 The only exception is the <framework>/env/__init.lua file and the <application>/env/__init.lua file, in which global variables may be set without using the _G reference. | 
| jbe@317 | 17 | 
| jbe@317 | 18 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@317 | 19 | 
| jbe@317 | 20 --]]-- | 
| jbe@231 | 21 local _G = _G | 
| jbe@237 | 22 local allowed_globals = {} | 
| jbe@231 | 23 local global_metatable = { | 
| jbe@231 | 24   __index = _G, | 
| jbe@231 | 25   __newindex = function(self, key, value) | 
| jbe@231 | 26     _G[key] = value | 
| jbe@231 | 27   end | 
| jbe@231 | 28 } | 
| jbe@317 | 29 _ENV = setmetatable( | 
| jbe@317 | 30   {},  -- proxy environment used within mcp.lua and by all chunks loaded through loadcached(...) | 
| jbe@317 | 31   global_metatable | 
| jbe@317 | 32 ) | 
| jbe@317 | 33 local function protect_globals()  -- called before first configuration file is loaded | 
| jbe@317 | 34   function global_metatable.__newindex(self, key, value) | 
| jbe@317 | 35     if allowed_globals[key] then | 
| jbe@317 | 36       _G[key] = value | 
| jbe@317 | 37     else | 
| jbe@317 | 38       if type(key) == "string" and string.match(key, "^[A-Za-z_][A-Za-z_0-9]*$") then | 
| jbe@317 | 39         error('Attempt to set global variable "' .. key .. '" (hint: use _G.' .. key .. '=<value> to override protection mechnamism)', 2) | 
| jbe@317 | 40       else | 
| jbe@317 | 41         error('Attempt to set global variable', 2) | 
| jbe@317 | 42       end | 
| jbe@317 | 43     end | 
| jbe@317 | 44   end | 
| jbe@317 | 45 end | 
| jbe@231 | 46 | 
| jbe@294 | 47 --[[-- | 
| jbe@309 | 48 lua_func =   -- compiled Lua function | 
| jbe@309 | 49 loadcached( | 
| jbe@309 | 50   filename   -- path to a Lua source or byte-code file | 
| jbe@309 | 51 ) | 
| jbe@309 | 52 | 
| jbe@309 | 53 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. | 
| jbe@309 | 54 | 
| jbe@309 | 55 --]]-- | 
| jbe@309 | 56 do | 
| jbe@309 | 57   local cache = {} | 
| jbe@309 | 58   function loadcached(filename) | 
| jbe@311 | 59     local cached_func = cache[filename] | 
| jbe@311 | 60     if cached_func then | 
| jbe@311 | 61       return cached_func | 
| jbe@311 | 62     end | 
| jbe@309 | 63     local file, read_error = io.open(filename, "r") | 
| jbe@309 | 64     if file then | 
| jbe@309 | 65       local filedata = assert(file:read("*a")) | 
| jbe@309 | 66       assert(file:close()) | 
| jbe@309 | 67       local func, compile_error = load(filedata, "=" .. filename, nil, _ENV) | 
| jbe@309 | 68       if func then | 
| jbe@309 | 69         cache[filename] = func | 
| jbe@309 | 70         return func | 
| jbe@309 | 71       end | 
| jbe@311 | 72       error(compile_error, 0) | 
| jbe@309 | 73     else | 
| jbe@309 | 74       return nil, read_error | 
| jbe@309 | 75     end | 
| jbe@309 | 76   end | 
| jbe@309 | 77 end | 
| jbe@309 | 78 --//-- | 
| jbe@309 | 79 | 
| jbe@309 | 80 --[[-- | 
| jbe@294 | 81 WEBMCP_MODE | 
| jbe@294 | 82 | 
| jbe@294 | 83 A constant set to "listen" in case of a network request, or set to "interactive" in case of interactive mode. | 
| jbe@294 | 84 --]]-- | 
| jbe@203 | 85 if listen then  -- defined by moonbridge | 
| jbe@203 | 86   WEBMCP_MODE = "listen" | 
| jbe@203 | 87 else | 
| jbe@203 | 88   WEBMCP_MODE = "interactive" | 
| jbe@64 | 89 end | 
| jbe@294 | 90 --//-- | 
| jbe@203 | 91 | 
| jbe@294 | 92 --[[-- | 
| jbe@294 | 93 WEBMCP_CONFIG_NAMES | 
| jbe@294 | 94 | 
| jbe@294 | 95 A list of the selected configuration names. | 
| jbe@294 | 96 --]]-- | 
| jbe@294 | 97 -- configuration names are provided as 4th, 5th, etc. command line argument | 
| jbe@206 | 98 WEBMCP_CONFIG_NAMES = {select(4, ...)} | 
| jbe@294 | 99 --//-- | 
| jbe@294 | 100 | 
| jbe@294 | 101 --[[-- | 
| jbe@294 | 102 WEBMCP_FRAMEWORK_PATH | 
| jbe@294 | 103 | 
| jbe@294 | 104 Directory of the WebMCP framework (always includes a trailing slash). | 
| jbe@294 | 105 --]]-- | 
| jbe@294 | 106 -- set in mcp.lua | 
| jbe@294 | 107 --//-- | 
| jbe@294 | 108 | 
| jbe@294 | 109 --[[-- | 
| jbe@294 | 110 WEBMCP_BASE_PATH | 
| jbe@294 | 111 | 
| jbe@294 | 112 Base directory of the application (always includes a trailing slash). | 
| jbe@294 | 113 --]]-- | 
| jbe@294 | 114 -- set in mcp.lua | 
| jbe@294 | 115 --//-- | 
| jbe@294 | 116 | 
| jbe@294 | 117 --[[-- | 
| jbe@294 | 118 WEBMCP_APP_NAME | 
| jbe@294 | 119 | 
| jbe@294 | 120 Application name (usually "main"). May be nil in case of interactive mode. | 
| jbe@294 | 121 --]]-- | 
| jbe@294 | 122 -- set in mcp.lua | 
| jbe@294 | 123 --//-- | 
| jbe@203 | 124 | 
| jbe@203 | 125 -- determine framework and bath path from command line arguments | 
| jbe@203 | 126 -- or print usage synopsis (if applicable) | 
| jbe@68 | 127 do | 
| jbe@206 | 128   local arg1, arg2, arg3 = ... | 
| jbe@203 | 129   local helpout | 
| jbe@203 | 130   if | 
| jbe@203 | 131     arg1 == "-h" or arg1 == "--help" or | 
| jbe@206 | 132     arg2 == "-h" or arg2 == "--help"  -- if first arg is provided by wrapper | 
| jbe@203 | 133   then | 
| jbe@203 | 134     helpout = io.stdout | 
| jbe@316 | 135   elseif #WEBMCP_CONFIG_NAMES < 1 then | 
| jbe@203 | 136     helpout = io.stderr | 
| jbe@203 | 137   end | 
| jbe@203 | 138   if helpout then | 
| jbe@316 | 139     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 | 140     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 | 141     if helpout == io.stderr then | 
| jbe@203 | 142       return 1 | 
| jbe@68 | 143     else | 
| jbe@203 | 144       return 0 | 
| jbe@68 | 145     end | 
| jbe@68 | 146   end | 
| jbe@203 | 147   local function append_trailing_slash(str) | 
| jbe@217 | 148     return string.gsub(str, "([^/])$", function(last) return last .. "/" end) | 
| jbe@203 | 149   end | 
| jbe@203 | 150   WEBMCP_FRAMEWORK_PATH = append_trailing_slash(arg1) | 
| jbe@203 | 151   WEBMCP_BASE_PATH      = append_trailing_slash(arg2) | 
| jbe@316 | 152   WEBMCP_APP_NAME       = arg3 | 
| jbe@68 | 153 end | 
| jbe@1 | 154 | 
| jbe@313 | 155 -- check if framework path is correct | 
| jbe@313 | 156 do | 
| jbe@313 | 157   local file, errmsg = io.open(WEBMCP_FRAMEWORK_PATH .. "webmcp_version", "r") | 
| jbe@313 | 158   if not file then | 
| jbe@313 | 159     error('Could not find "webmcp_version" file: ' .. errmsg, 0) | 
| jbe@313 | 160   end | 
| jbe@313 | 161   local version = assert(file:read()) | 
| jbe@313 | 162   assert(file:close()) | 
| jbe@313 | 163   if version ~= WEBMCP_VERSION then | 
| jbe@313 | 164     error('Version mismatch in file "' .. WEBMCP_FRAMEWORK_PATH .. 'webmcp_version"') | 
| jbe@313 | 165   end | 
| jbe@313 | 166 end | 
| jbe@313 | 167 | 
| jbe@203 | 168 -- setup search paths for libraries | 
| jbe/bsw@0 | 169 do | 
| jbe@217 | 170   if string.match(package.path, "^[^;]") then | 
| jbe@217 | 171     package.path = ";" .. package.path | 
| jbe@217 | 172   end | 
| jbe@217 | 173   package.path = WEBMCP_FRAMEWORK_PATH .. "lib/?.lua" .. package.path | 
| jbe/bsw@0 | 174   -- find out which file name extension shared libraries have | 
| jbe/bsw@0 | 175   local slib_exts = {} | 
| jbe/bsw@0 | 176   for ext in string.gmatch(package.cpath, "%?%.([A-Za-z0-9_-]+)") do | 
| jbe@217 | 177     if not slib_exts[ext] then | 
| jbe@217 | 178       slib_exts[#slib_exts+1] = ext | 
| jbe@217 | 179       slib_exts[ext] = true | 
| jbe@217 | 180     end | 
| jbe/bsw@0 | 181   end | 
| jbe/bsw@0 | 182   local paths = {} | 
| jbe@217 | 183   for i, ext in ipairs(slib_exts) do | 
| jbe@203 | 184     paths[#paths+1] = WEBMCP_FRAMEWORK_PATH .. "accelerator/?." .. ext | 
| jbe/bsw@0 | 185   end | 
| jbe@217 | 186   for i, ext in ipairs(slib_exts) do | 
| jbe@203 | 187     paths[#paths+1] = WEBMCP_FRAMEWORK_PATH .. "lib/?." .. ext | 
| jbe/bsw@0 | 188   end | 
| jbe/bsw@0 | 189   paths[#paths+1] = package.cpath | 
| jbe/bsw@0 | 190   package.cpath = table.concat(paths, ";") | 
| jbe/bsw@0 | 191 end | 
| jbe/bsw@0 | 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@317 | 196 local root_init  -- function which executes the __init.lua file in the environment's root | 
| jbe/bsw@0 | 197 do | 
| jbe/bsw@0 | 198   local weakkey_mt = { __mode = "k" } | 
| jbe/bsw@0 | 199   local autoloader_category = setmetatable({}, weakkey_mt) | 
| jbe/bsw@0 | 200   local autoloader_path     = setmetatable({}, weakkey_mt) | 
| jbe/bsw@0 | 201   local autoloader_mt       = {} | 
| jbe@219 | 202   local function install_autoloader(self, category, path_fragment) | 
| jbe/bsw@0 | 203     autoloader_category[self] = category | 
| jbe@219 | 204     autoloader_path[self]     = path_fragment | 
| jbe/bsw@0 | 205     setmetatable(self, autoloader_mt) | 
| jbe/bsw@0 | 206   end | 
| jbe/bsw@0 | 207   local function try_exec(filename) | 
| jbe@309 | 208     local func = loadcached(filename) | 
| jbe@309 | 209     if func then | 
| jbe@309 | 210       func() | 
| jbe@309 | 211       return true | 
| jbe/bsw@0 | 212     else | 
| jbe/bsw@0 | 213       return false | 
| jbe/bsw@0 | 214     end | 
| jbe/bsw@0 | 215   end | 
| jbe/bsw@0 | 216   function autoloader_mt.__index(self, key) | 
| jbe/bsw@0 | 217     local category, base_path, merge_base_path, file_key | 
| jbe/bsw@0 | 218     local merge = false | 
| jbe/bsw@0 | 219     if | 
| jbe/bsw@0 | 220       string.find(key, "^[a-z_][A-Za-z0-9_]*$") and | 
| jbe/bsw@0 | 221       not string.find(key, "^__") | 
| jbe/bsw@0 | 222     then | 
| jbe/bsw@0 | 223       category        = "env" | 
| jbe@203 | 224       base_path       = WEBMCP_FRAMEWORK_PATH .. "env/" | 
| jbe/bsw@0 | 225       merge           = true | 
| jbe@203 | 226       merge_base_path = WEBMCP_BASE_PATH .. "env/" | 
| jbe/bsw@0 | 227       file_key        = key | 
| jbe/bsw@0 | 228     elseif string.find(key, "^[A-Z][A-Za-z0-9]*$") then | 
| jbe/bsw@0 | 229       category        = "model" | 
| jbe@203 | 230       base_path       = WEBMCP_BASE_PATH .. "model/" | 
| jbe/bsw@0 | 231       local first = true | 
| jbe/bsw@0 | 232       file_key = string.gsub(key, "[A-Z]", | 
| jbe/bsw@0 | 233         function(c) | 
| jbe/bsw@0 | 234           if first then | 
| jbe/bsw@0 | 235             first = false | 
| jbe/bsw@0 | 236             return string.lower(c) | 
| jbe/bsw@0 | 237           else | 
| jbe/bsw@0 | 238             return "_" .. string.lower(c) | 
| jbe/bsw@0 | 239           end | 
| jbe/bsw@0 | 240         end | 
| jbe/bsw@0 | 241       ) | 
| jbe/bsw@0 | 242     else | 
| jbe/bsw@0 | 243       return | 
| jbe/bsw@0 | 244     end | 
| jbe/bsw@0 | 245     local required_category = autoloader_category[self] | 
| jbe/bsw@0 | 246     if required_category and required_category ~= category then return end | 
| jbe@219 | 247     local path_fragment = autoloader_path[self] | 
| jbe@219 | 248     local path = base_path .. path_fragment .. file_key | 
| jbe@219 | 249     local merge_path | 
| jbe/bsw@0 | 250     if merge then | 
| jbe@219 | 251       merge_path = merge_base_path .. path_fragment .. file_key | 
| jbe/bsw@0 | 252     end | 
| jbe/bsw@0 | 253     local function try_dir(dirname) | 
| jbe/bsw@0 | 254       local dir = io.open(dirname) | 
| jbe/bsw@0 | 255       if dir then | 
| jbe/bsw@0 | 256         io.close(dir) | 
| jbe/bsw@0 | 257         local obj = {} | 
| jbe@219 | 258         install_autoloader(obj, category, path_fragment .. file_key .. "/") | 
| jbe/bsw@0 | 259         rawset(self, key, obj) | 
| jbe@219 | 260         try_exec(path .. "/__init.lua") | 
| jbe@219 | 261         if merge then try_exec(merge_path .. "/__init.lua") end | 
| jbe/bsw@0 | 262         return true | 
| jbe/bsw@0 | 263       else | 
| jbe/bsw@0 | 264         return false | 
| jbe/bsw@0 | 265       end | 
| jbe/bsw@0 | 266     end | 
| jbe@238 | 267     if self == _G then | 
| jbe@237 | 268       allowed_globals[key] = true | 
| jbe@233 | 269     end | 
| jbe@219 | 270     if merge and try_exec(merge_path .. ".lua") then | 
| jbe@219 | 271     elseif merge and try_dir(merge_path .. "/") then | 
| jbe@219 | 272     elseif try_exec(path .. ".lua") then | 
| jbe@219 | 273     elseif try_dir(path .. "/") then | 
| jbe/bsw@0 | 274     else end | 
| jbe@238 | 275     if self == _G then | 
| jbe@237 | 276       allowed_globals[key] = nil | 
| jbe@233 | 277     end | 
| jbe@237 | 278     return rawget(self, key) | 
| jbe/bsw@0 | 279   end | 
| jbe@219 | 280   install_autoloader(_G, nil, "") | 
| jbe@317 | 281   function root_init()  -- upvalue | 
| jbe@317 | 282     try_exec(WEBMCP_FRAMEWORK_PATH .. "env/__init.lua") | 
| jbe@317 | 283     try_exec(WEBMCP_BASE_PATH .. "env/__init.lua") | 
| jbe@317 | 284   end | 
| jbe@214 | 285 end | 
| jbe@214 | 286 | 
| jbe@286 | 287 -- define post-fork initialization function (including loading of "multirand" library) | 
| jbe@286 | 288 local function postfork_init() | 
| jbe@286 | 289   _G.multirand = require "multirand" | 
| jbe@286 | 290   execute.postfork_initializers() | 
| jbe@286 | 291 end | 
| jbe@286 | 292 | 
| jbe@317 | 293 -- prepare for interactive or listen mode | 
| jbe@203 | 294 if WEBMCP_MODE == "interactive" then | 
| jbe@317 | 295   function listen()  -- overwrite Moonbridge's listen function | 
| jbe@317 | 296     -- ignore listen function calls for interactive mode | 
| jbe@317 | 297   end | 
| jbe@317 | 298   trace.disable()  -- avoids memory leakage when scripts are running endlessly | 
| jbe@317 | 299 else | 
| jbe@317 | 300   local moonbridge_listen = listen | 
| jbe@207 | 301   local http = require("moonbridge_http") | 
| jbe@317 | 302   function listen(args)  -- overwrite Moonbridge's listen function | 
| jbe@317 | 303     local http_options = args.http_options or {} | 
| jbe@317 | 304     local min_requests_per_fork = http_options.min_requests_per_fork or 50 | 
| jbe@317 | 305     local max_requests_per_fork = http_options.max_requests_per_fork or 100 | 
| jbe@316 | 306     local interval_handlers = {} | 
| jbe@317 | 307     for j, listener in ipairs(args) do | 
| jbe@317 | 308       if listener.proto == "interval" then | 
| jbe@317 | 309         local name = listener.name or "Unnamed interval #" .. #interval_handlers+1 | 
| jbe@317 | 310         interval_handlers[name] = listener.handler | 
| jbe@317 | 311         listener.name = name | 
| jbe@316 | 312       end | 
| jbe@316 | 313     end | 
| jbe@264 | 314     local request_count = 0 | 
| jbe@266 | 315     local function inner_handler(http_request) | 
| jbe@317 | 316       request.initialize() | 
| jbe@288 | 317       request.handler(http_request, request_count >= max_requests_per_fork) | 
| jbe@264 | 318     end | 
| jbe@264 | 319     local outer_handler = http.generate_handler(inner_handler, http_options) | 
| jbe@317 | 320     args.prepare = postfork_init | 
| jbe@317 | 321     args.connect = function(socket) | 
| jbe@316 | 322       request_count = request_count + 1 | 
| jbe@316 | 323       if socket.interval then | 
| jbe@317 | 324         request.initialize() | 
| jbe@316 | 325         interval_handlers[socket.interval]() | 
| jbe@316 | 326       else | 
| jbe@316 | 327         outer_handler(socket) | 
| jbe@316 | 328       end | 
| jbe@288 | 329       return request_count < min_requests_per_fork | 
| jbe@264 | 330     end | 
| jbe@317 | 331     args.finish = execute.finalizers | 
| jbe@317 | 332     moonbridge_listen(args) | 
| jbe@204 | 333   end | 
| jbe@204 | 334 end | 
| jbe@317 | 335 | 
| jbe@317 | 336 -- execute the __init.lua file in the environment's root | 
| jbe@317 | 337 root_init() | 
| jbe@317 | 338 | 
| jbe@317 | 339 -- prohibit (unintended) definition of new global variables | 
| jbe@317 | 340 protect_globals() | 
| jbe@317 | 341 | 
| jbe@317 | 342 -- execute configurations and pre-fork initializers | 
| jbe@317 | 343 for i, config_name in ipairs(WEBMCP_CONFIG_NAMES) do | 
| jbe@317 | 344   execute.config(config_name) | 
| jbe@317 | 345 end | 
| jbe@317 | 346 execute.prefork_initializers() | 
| jbe@317 | 347 | 
| jbe@317 | 348 -- perform post-fork initializations in case of interactive mode | 
| jbe@317 | 349 if WEBMCP_MODE == "interactive" then | 
| jbe@317 | 350   postfork_init() | 
| jbe@317 | 351 end |