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@336
|
283 --[[--
|
jbe@336
|
284 listen{
|
jbe@336
|
285 {
|
jbe@336
|
286 proto = proto, -- "local", "tcp4", "tcp6", or "interval"
|
jbe@336
|
287 path = path, -- path to unix domain socket if proto == "local"
|
jbe@336
|
288 port = port, -- TCP port number
|
jbe@336
|
289 localhost = localhost_only, -- set to true to only listen on localhost (127.0.0.1 or ::1) interface
|
jbe@336
|
290 name = interval_name, -- optional interval name (may be useful for log output)
|
jbe@336
|
291 handler = interval_handler -- interval handler if proto == "interval"
|
jbe@336
|
292 },
|
jbe@336
|
293 {
|
jbe@336
|
294 ... -- second listener
|
jbe@336
|
295 },
|
jbe@336
|
296 ... -- more listeners
|
jbe@336
|
297 -- the following options are all optional and have default values:
|
jbe@336
|
298 pre_fork = pre_fork, -- desired number of spare (idle) processes
|
jbe@336
|
299 min_fork = min_fork, -- minimum number of processes
|
jbe@336
|
300 max_fork = max_fork, -- maximum number of processes (hard limit)
|
jbe@336
|
301 fork_delay = fork_delay, -- delay (seconds) between creation of spare processes
|
jbe@336
|
302 fork_error_delay = fork_error_delay, -- delay (seconds) before retry of failed process creation
|
jbe@336
|
303 exit_delay = exit_delay, -- delay (seconds) between destruction of excessive spare processes
|
jbe@336
|
304 idle_timeout = idle_timeout, -- idle time (seconds) after a fork gets terminated (0 for no timeout)
|
jbe@336
|
305 memory_limit = memory_limit, -- maximum memory consumption (bytes) before process gets terminated
|
jbe@336
|
306 min_requests_per_fork = min_requests_per_fork, -- minimum count of requests handled before fork is terminated
|
jbe@336
|
307 max_requests_per_fork = max_requests_per_fork, -- maximum count of requests handled before fork is terminated
|
jbe@336
|
308 http_options = {
|
jbe@337
|
309 static_headers = static_headers, -- string or table of static headers to be returned with every request
|
jbe@337
|
310 request_header_size_limit = request_header_size_limit, -- maximum size of request headers sent by client
|
jbe@337
|
311 request_body_size_limit = request_body_size_limit, -- maximum size of request body sent by client
|
jbe@337
|
312 request_header_timeout = request_header_timeout, -- time after which request headers must have been received and processed
|
jbe@337
|
313 timeout = timeout, -- time in which request body and response must be sent
|
jbe@338
|
314 maximum_input_chunk_size = maximum_input_chunk_size, -- tweaks behavior of request-body parser
|
jbe@337
|
315 minimum_output_chunk_size = minimum_output_chunk_size -- chunk size for chunked-transfer-encoding
|
jbe@336
|
316 }
|
jbe@336
|
317 }
|
jbe@336
|
318
|
jbe@336
|
319 The listen{...} function determines on which TCP port an application is answering requests. A typical call looks as follows:
|
jbe@336
|
320
|
jbe@336
|
321 listen{
|
jbe@336
|
322 { proto = "tcp4", port = 8080, localhost = true },
|
jbe@336
|
323 { proto = "tcp6", port = 8080, localhost = true }
|
jbe@336
|
324 }
|
jbe@336
|
325
|
jbe@336
|
326 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
|
327
|
jbe@336
|
328 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.
|
jbe@336
|
329
|
jbe@336
|
330 --]]--
|
jbe@317
|
331 -- prepare for interactive or listen mode
|
jbe@203
|
332 if WEBMCP_MODE == "interactive" then
|
jbe@317
|
333 function listen() -- overwrite Moonbridge's listen function
|
jbe@317
|
334 -- ignore listen function calls for interactive mode
|
jbe@317
|
335 end
|
jbe@317
|
336 trace.disable() -- avoids memory leakage when scripts are running endlessly
|
jbe@317
|
337 else
|
jbe@317
|
338 local moonbridge_listen = listen
|
jbe@207
|
339 local http = require("moonbridge_http")
|
jbe@317
|
340 function listen(args) -- overwrite Moonbridge's listen function
|
jbe@322
|
341 assert(args, "No argument passed to listen function")
|
jbe@322
|
342 local min_requests_per_fork = args.min_requests_per_fork or 50
|
jbe@335
|
343 local max_requests_per_fork = args.max_requests_per_fork or 200
|
jbe@316
|
344 local interval_handlers = {}
|
jbe@317
|
345 for j, listener in ipairs(args) do
|
jbe@317
|
346 if listener.proto == "interval" then
|
jbe@317
|
347 local name = listener.name or "Unnamed interval #" .. #interval_handlers+1
|
jbe@336
|
348 if interval_handlers[name] ~= nil then
|
jbe@336
|
349 error('Interval handler with duplicate name "' .. name .. '"')
|
jbe@336
|
350 end
|
jbe@317
|
351 interval_handlers[name] = listener.handler
|
jbe@317
|
352 listener.name = name
|
jbe@316
|
353 end
|
jbe@316
|
354 end
|
jbe@264
|
355 local request_count = 0
|
jbe@266
|
356 local function inner_handler(http_request)
|
jbe@328
|
357 request_count = request_count + 1
|
jbe@328
|
358 if request_count >= max_requests_per_fork then
|
jbe@328
|
359 http_request:close_after_finish()
|
jbe@328
|
360 end
|
jbe@317
|
361 request.initialize()
|
jbe@328
|
362 return request.handler(http_request)
|
jbe@264
|
363 end
|
jbe@326
|
364 local outer_handler = http.generate_handler(inner_handler, args.http_options)
|
jbe@317
|
365 args.prepare = postfork_init
|
jbe@317
|
366 args.connect = function(socket)
|
jbe@316
|
367 if socket.interval then
|
jbe@328
|
368 request_count = request_count + 1
|
jbe@317
|
369 request.initialize()
|
jbe@316
|
370 interval_handlers[socket.interval]()
|
jbe@316
|
371 else
|
jbe@327
|
372 local success = outer_handler(socket)
|
jbe@327
|
373 if not success then
|
jbe@327
|
374 return false
|
jbe@327
|
375 end
|
jbe@316
|
376 end
|
jbe@288
|
377 return request_count < min_requests_per_fork
|
jbe@264
|
378 end
|
jbe@317
|
379 args.finish = execute.finalizers
|
jbe@317
|
380 moonbridge_listen(args)
|
jbe@204
|
381 end
|
jbe@204
|
382 end
|
jbe@336
|
383 --//--
|
jbe@317
|
384
|
jbe@317
|
385 -- execute configurations and pre-fork initializers
|
jbe@317
|
386 for i, config_name in ipairs(WEBMCP_CONFIG_NAMES) do
|
jbe@317
|
387 execute.config(config_name)
|
jbe@317
|
388 end
|
jbe@317
|
389 execute.prefork_initializers()
|
jbe@317
|
390
|
jbe@324
|
391 -- perform post-fork initializations (once) in case of interactive mode
|
jbe@317
|
392 if WEBMCP_MODE == "interactive" then
|
jbe@317
|
393 postfork_init()
|
jbe@317
|
394 end
|