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@337
|
314 minimum_output_chunk_size = minimum_output_chunk_size -- chunk size for chunked-transfer-encoding
|
jbe@336
|
315 }
|
jbe@336
|
316 }
|
jbe@336
|
317
|
jbe@336
|
318 The listen{...} function determines on which TCP port an application is answering requests. A typical call looks as follows:
|
jbe@336
|
319
|
jbe@336
|
320 listen{
|
jbe@336
|
321 { proto = "tcp4", port = 8080, localhost = true },
|
jbe@336
|
322 { proto = "tcp6", port = 8080, localhost = true }
|
jbe@336
|
323 }
|
jbe@336
|
324
|
jbe@336
|
325 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
|
326
|
jbe@336
|
327 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
|
328
|
jbe@336
|
329 --]]--
|
jbe@317
|
330 -- prepare for interactive or listen mode
|
jbe@203
|
331 if WEBMCP_MODE == "interactive" then
|
jbe@317
|
332 function listen() -- overwrite Moonbridge's listen function
|
jbe@317
|
333 -- ignore listen function calls for interactive mode
|
jbe@317
|
334 end
|
jbe@317
|
335 trace.disable() -- avoids memory leakage when scripts are running endlessly
|
jbe@317
|
336 else
|
jbe@317
|
337 local moonbridge_listen = listen
|
jbe@207
|
338 local http = require("moonbridge_http")
|
jbe@317
|
339 function listen(args) -- overwrite Moonbridge's listen function
|
jbe@322
|
340 assert(args, "No argument passed to listen function")
|
jbe@322
|
341 local min_requests_per_fork = args.min_requests_per_fork or 50
|
jbe@335
|
342 local max_requests_per_fork = args.max_requests_per_fork or 200
|
jbe@316
|
343 local interval_handlers = {}
|
jbe@317
|
344 for j, listener in ipairs(args) do
|
jbe@317
|
345 if listener.proto == "interval" then
|
jbe@317
|
346 local name = listener.name or "Unnamed interval #" .. #interval_handlers+1
|
jbe@336
|
347 if interval_handlers[name] ~= nil then
|
jbe@336
|
348 error('Interval handler with duplicate name "' .. name .. '"')
|
jbe@336
|
349 end
|
jbe@317
|
350 interval_handlers[name] = listener.handler
|
jbe@317
|
351 listener.name = name
|
jbe@316
|
352 end
|
jbe@316
|
353 end
|
jbe@264
|
354 local request_count = 0
|
jbe@266
|
355 local function inner_handler(http_request)
|
jbe@328
|
356 request_count = request_count + 1
|
jbe@328
|
357 if request_count >= max_requests_per_fork then
|
jbe@328
|
358 http_request:close_after_finish()
|
jbe@328
|
359 end
|
jbe@317
|
360 request.initialize()
|
jbe@328
|
361 return request.handler(http_request)
|
jbe@264
|
362 end
|
jbe@326
|
363 local outer_handler = http.generate_handler(inner_handler, args.http_options)
|
jbe@317
|
364 args.prepare = postfork_init
|
jbe@317
|
365 args.connect = function(socket)
|
jbe@316
|
366 if socket.interval then
|
jbe@328
|
367 request_count = request_count + 1
|
jbe@317
|
368 request.initialize()
|
jbe@316
|
369 interval_handlers[socket.interval]()
|
jbe@316
|
370 else
|
jbe@327
|
371 local success = outer_handler(socket)
|
jbe@327
|
372 if not success then
|
jbe@327
|
373 return false
|
jbe@327
|
374 end
|
jbe@316
|
375 end
|
jbe@288
|
376 return request_count < min_requests_per_fork
|
jbe@264
|
377 end
|
jbe@317
|
378 args.finish = execute.finalizers
|
jbe@317
|
379 moonbridge_listen(args)
|
jbe@204
|
380 end
|
jbe@204
|
381 end
|
jbe@336
|
382 --//--
|
jbe@317
|
383
|
jbe@317
|
384 -- execute configurations and pre-fork initializers
|
jbe@317
|
385 for i, config_name in ipairs(WEBMCP_CONFIG_NAMES) do
|
jbe@317
|
386 execute.config(config_name)
|
jbe@317
|
387 end
|
jbe@317
|
388 execute.prefork_initializers()
|
jbe@317
|
389
|
jbe@324
|
390 -- perform post-fork initializations (once) in case of interactive mode
|
jbe@317
|
391 if WEBMCP_MODE == "interactive" then
|
jbe@317
|
392 postfork_init()
|
jbe@317
|
393 end
|