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