webmcp

view framework/bin/mcp.lua @ 480:e09654c4a042

Allow main handlers to use a poll function that indicates when process termination is requested
author jbe
date Tue Jun 06 16:14:33 2017 +0200 (2017-06-06)
parents d3e77b8edb2a
children 416550ff0bb5
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.1.0"
9 --//--
11 --[[--
12 WEBMCP_MODE
14 A constant set to "listen" in case of a network request, or set to "interactive" in case of interactive mode.
15 --]]--
16 if _MOONBRIDGE_VERSION then
17 WEBMCP_MODE = "listen"
18 else
19 WEBMCP_MODE = "interactive"
20 end
21 --//--
23 --[[--
24 WEBMCP_CONFIG_NAMES
26 A list of the selected configuration names.
27 --]]--
28 -- configuration names are provided as 4th, 5th, etc. command line argument
29 WEBMCP_CONFIG_NAMES = {select(4, ...)}
30 --//--
32 --[[--
33 WEBMCP_FRAMEWORK_PATH
35 Directory of the WebMCP framework (always includes a trailing slash).
36 --]]--
37 -- set in mcp.lua
38 --//--
40 --[[--
41 WEBMCP_BASE_PATH
43 Base directory of the application (always includes a trailing slash).
44 --]]--
45 -- set in mcp.lua
46 --//--
48 --[[--
49 WEBMCP_APP_NAME
51 Application name (usually "main"). May be nil in case of interactive mode.
52 --]]--
53 -- set in mcp.lua
54 --//--
56 -- determine framework and bath path from command line arguments
57 -- or print usage synopsis (if applicable)
58 do
59 local arg1, arg2, arg3 = ...
60 local helpout
61 if
62 arg1 == "-h" or arg1 == "--help" or
63 arg2 == "-h" or arg2 == "--help" -- if first arg is provided by wrapper
64 then
65 helpout = io.stdout
66 elseif #WEBMCP_CONFIG_NAMES < 1 then
67 helpout = io.stderr
68 end
69 if helpout then
70 helpout:write("Usage: moonbridge [moonbr opts] -- <framework path>/bin/mcp.lua <framework path> <app base path> <app name> <config name> [<config name> ...]\n")
71 helpout:write(" or: lua -i [Lua opts] -- <framework path>/bin/mcp.lua <framework path> <app base path> <app name> <config name> [<config name> ...]\n")
72 if helpout == io.stderr then
73 return 1
74 else
75 return 0
76 end
77 end
78 local function append_trailing_slash(str)
79 return (string.gsub(str, "([^/])$", function(last) return last .. "/" end))
80 end
81 WEBMCP_FRAMEWORK_PATH = append_trailing_slash(arg1)
82 WEBMCP_BASE_PATH = append_trailing_slash(arg2)
83 WEBMCP_APP_NAME = arg3
84 end
86 -- setup search paths for libraries
87 do
88 if string.match(package.path, "^[^;]") then
89 package.path = ";" .. package.path
90 end
91 package.path = WEBMCP_FRAMEWORK_PATH .. "lib/?.lua" .. package.path
92 -- find out which file name extension shared libraries have
93 local slib_exts = {}
94 for ext in string.gmatch(package.cpath, "%?%.([A-Za-z0-9_-]+)") do
95 if not slib_exts[ext] then
96 slib_exts[#slib_exts+1] = ext
97 slib_exts[ext] = true
98 end
99 end
100 local paths = {}
101 for i, ext in ipairs(slib_exts) do
102 paths[#paths+1] = WEBMCP_FRAMEWORK_PATH .. "accelerator/?." .. ext
103 end
104 for i, ext in ipairs(slib_exts) do
105 paths[#paths+1] = WEBMCP_FRAMEWORK_PATH .. "lib/?." .. ext
106 end
107 paths[#paths+1] = package.cpath
108 package.cpath = table.concat(paths, ";")
109 end
111 -- load "extos" library (needed by function "loadcached")
112 _G.extos = require "extos"
114 --[[--
115 _G
117 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.
119 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.
121 --]]--
122 local _G = _G
123 local allowed_globals = {}
124 local protected_environment = setmetatable(
125 {}, -- proxy environment used all chunks loaded through loadcached(...)
126 {
127 __index = _G,
128 __newindex = function(self, key, value)
129 if allowed_globals[key] then
130 _G[key] = value
131 else
132 if type(key) == "string" and string.match(key, "^[A-Za-z_][A-Za-z_0-9]*$") then
133 error('Attempt to set global variable "' .. key .. '" (Hint: missing local statement? Use _G.' .. key .. '=<value> to really set global variable.)', 2)
134 else
135 error('Attempt to set global variable', 2)
136 end
137 end
138 end
139 }
140 )
141 --//--
143 --[[--
144 lua_func, -- compiled Lua function, nil if the file does not exist
145 errmsg = -- error message (only for non-existing file, other errors are thrown)
146 loadcached(
147 filename -- path to a Lua source or byte-code file
148 )
150 Loads, compiles and caches a Lua chunk. The cached value (i.e. the compiled function) is returned. If the file does not exist, nil and an error string are returned. Any other errors are thrown using error(...). Unsuccessful attempts are not cached (to prohibit cache pollution).
152 --]]--
153 do
154 local cache = {}
155 function loadcached(filename)
156 local cached_func = cache[filename]
157 if cached_func then
158 return cached_func
159 end
160 local stat, errmsg = extos.stat(filename)
161 if stat == nil then
162 error(errmsg)
163 elseif stat == false then
164 return nil, 'File "' .. filename .. '" does not exist'
165 elseif stat.isdir then
166 error('File "' .. filename .. '" is a directory')
167 elseif not stat.isreg then
168 error('File "' .. filename .. '" is not a regular file')
169 end
170 local func, compile_error = loadfile(filename, nil, protected_environment)
171 if func then
172 cache[filename] = func
173 return func
174 end
175 error(compile_error, 0)
176 end
177 end
178 --//--
180 -- check if framework path is correct
181 do
182 local file, errmsg = io.open(WEBMCP_FRAMEWORK_PATH .. "webmcp_version", "r")
183 if not file then
184 error('Could not find "webmcp_version" file: ' .. errmsg, 0)
185 end
186 local version = assert(file:read())
187 assert(file:close())
188 if version ~= WEBMCP_VERSION then
189 error('Version mismatch in file "' .. WEBMCP_FRAMEWORK_PATH .. 'webmcp_version"')
190 end
191 end
193 -- autoloader system for WebMCP environment "$WEBMCP_FRAMEWORK_PATH/env/",
194 -- application environment extensions "$WEBMCP_BASE_PATH/env/"
195 -- and models "$WEBMCP_BASE_PATH/model/"
196 do
197 local weakkey_mt = { __mode = "k" }
198 local autoloader_category = setmetatable({}, weakkey_mt)
199 local autoloader_path = setmetatable({}, weakkey_mt)
200 local autoloader_mt = {}
201 local function install_autoloader(self, category, path_fragment)
202 autoloader_category[self] = category
203 autoloader_path[self] = path_fragment
204 setmetatable(self, autoloader_mt)
205 end
206 local function try_exec(filename)
207 local func = loadcached(filename)
208 if func then
209 func()
210 return true
211 else
212 return false
213 end
214 end
215 function autoloader_mt.__index(self, key)
216 local category, base_path, merge_base_path, file_key
217 local merge = false
218 if
219 string.find(key, "^[a-z_][A-Za-z0-9_]*$") and
220 not string.find(key, "^__")
221 then
222 category = "env"
223 base_path = WEBMCP_FRAMEWORK_PATH .. "env/"
224 merge = true
225 merge_base_path = WEBMCP_BASE_PATH .. "env/"
226 file_key = key
227 elseif string.find(key, "^[A-Z][A-Za-z0-9]*$") then
228 category = "model"
229 base_path = WEBMCP_BASE_PATH .. "model/"
230 local first = true
231 file_key = string.gsub(key, "[A-Z]",
232 function(c)
233 if first then
234 first = false
235 return string.lower(c)
236 else
237 return "_" .. string.lower(c)
238 end
239 end
240 )
241 else
242 return
243 end
244 local required_category = autoloader_category[self]
245 if required_category and required_category ~= category then return end
246 local path_fragment = autoloader_path[self]
247 local path = base_path .. path_fragment .. file_key
248 local merge_path
249 if merge then
250 merge_path = merge_base_path .. path_fragment .. file_key
251 end
252 local function try_dir(dirname)
253 local dir = io.open(dirname)
254 if dir then
255 io.close(dir)
256 local obj = {}
257 install_autoloader(obj, category, path_fragment .. file_key .. "/")
258 rawset(self, key, obj)
259 try_exec(path .. "/__init.lua")
260 if merge then try_exec(merge_path .. "/__init.lua") end
261 return true
262 else
263 return false
264 end
265 end
266 if self == _G then
267 allowed_globals[key] = true
268 end
269 if merge and try_exec(merge_path .. ".lua") then
270 elseif merge and try_dir(merge_path .. "/") then
271 elseif try_exec(path .. ".lua") then
272 elseif try_dir(path .. "/") then
273 else end
274 if self == _G then
275 allowed_globals[key] = nil
276 end
277 return rawget(self, key)
278 end
279 install_autoloader(_G, nil, "")
280 try_exec(WEBMCP_FRAMEWORK_PATH .. "env/__init.lua")
281 try_exec(WEBMCP_BASE_PATH .. "env/__init.lua")
282 end
284 -- signal socket (to catch SIGTERM in main handlers)
285 local sigterm_socket
287 -- define post-fork initialization function (including loading of "multirand" library)
288 local function postfork_init()
289 sigterm_socket = moonbridge_io.signalsocket("TERM")
290 multirand = require "multirand"
291 execute.postfork_initializers()
292 end
294 --[[--
295 listen{
296 {
297 proto = proto, -- "local", "tcp", "interval", or "main"
298 path = path, -- path to unix domain socket if proto == "local"
299 port = port, -- TCP port number
300 host = host, -- "::" for all IPv6 interfaces, "0.0.0.0" for all IPv4 interfaces
301 name = name, -- optional name for main handlers or interval handlers (may be useful for log output)
302 handler = handler, -- handler if proto == "interval" or proto == "main"
303 delay = delay, -- delay between invocations of interval handler
304 strict = strict -- set to true to substract runtime of interval handler from delay
305 },
306 {
307 ... -- second listener
308 },
309 ... -- more listeners
310 -- the following options are all optional and have default values:
311 pre_fork = pre_fork, -- desired number of spare (idle) processes
312 min_fork = min_fork, -- minimum number of processes
313 max_fork = max_fork, -- maximum number of processes (hard limit)
314 fork_delay = fork_delay, -- delay (seconds) between creation of spare processes
315 fork_error_delay = fork_error_delay, -- delay (seconds) before retry of failed process creation
316 exit_delay = exit_delay, -- delay (seconds) between destruction of excessive spare processes
317 idle_timeout = idle_timeout, -- idle time (seconds) after a fork gets terminated (0 for no timeout)
318 memory_limit = memory_limit, -- maximum memory consumption (bytes) before process gets terminated
319 min_requests_per_fork = min_requests_per_fork, -- minimum count of requests handled before fork is terminated
320 max_requests_per_fork = max_requests_per_fork, -- maximum count of requests handled before fork is terminated
321 http_options = {
322 static_headers = static_headers, -- string or table of static headers to be returned with every request
323 request_header_size_limit = request_header_size_limit, -- maximum size of request headers sent by client
324 request_body_size_limit = request_body_size_limit, -- maximum size of request body sent by client
325 idle_timeout = idle_timeout, -- maximum time until receiving the first byte of the request header
326 stall_timeout = stall_timeout, -- maximum time a client connection may be stalled
327 request_header_timeout = request_header_timeout, -- maximum time until receiving the remaining bytes of the request header
328 response_timeout = response_timeout, -- time in which request body and response must be sent
329 maximum_input_chunk_size = maximum_input_chunk_size, -- tweaks behavior of request-body parser
330 minimum_output_chunk_size = minimum_output_chunk_size -- chunk size for chunked-transfer-encoding
331 }
332 }
334 The listen{...} function determines on which TCP port an application is answering requests. A typical call looks as follows:
336 listen{
337 { proto = "tcp4", port = 8080, localhost = true },
338 { proto = "tcp6", port = 8080, localhost = true }
339 }
341 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).
343 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 and main routines, a handler may be specified in each listener.
345 Handlers for main routines (proto = "main") get a poll function passed as first and only parameter. This poll function behaves like moonbridge_io.poll(...) with the exeception that the return value is "terminate", "timeout", or "io" depending whether a termination is requested, a timeout happened, or data is ready for I/O.
347 No handler or pre-/postfork initializer should use moonbridge_io.signalsocket(...) directly because this can interfere with signal detection for shutdown.
349 --]]--
350 -- prepare for interactive or listen mode
351 if WEBMCP_MODE == "interactive" then
352 function listen() -- overwrite Moonbridge's listen function
353 -- ignore listen function calls for interactive mode
354 end
355 trace.disable() -- avoids memory leakage when scripts are running endlessly
356 else
357 local moonbridge_listen = listen
358 local http = require("moonbridge_http")
359 function listen(args) -- overwrite Moonbridge's listen function
360 assert(args, "No argument passed to listen function")
361 local min_requests_per_fork = args.min_requests_per_fork or 50
362 local max_requests_per_fork = args.max_requests_per_fork or 200
363 local main_handlers = {}
364 local interval_handlers = {}
365 for j, listener in ipairs(args) do
366 if listener.proto == "main" then
367 local name = listener.name or "Unnamed main thread #" .. #main_handlers+1
368 if main_handlers[name] ~= nil then
369 error('Main thread handler with duplicate name "' .. name .. '"')
370 end
371 main_handlers[name] = listener.handler
372 listener.name = name
373 elseif listener.proto == "interval" then
374 local name = listener.name or "Unnamed interval #" .. #interval_handlers+1
375 if interval_handlers[name] ~= nil then
376 error('Interval handler with duplicate name "' .. name .. '"')
377 end
378 interval_handlers[name] = listener.handler
379 listener.name = name
380 end
381 end
382 local request_count = 0
383 local function inner_handler(http_request)
384 request_count = request_count + 1
385 if request_count >= max_requests_per_fork then
386 http_request:close_after_finish()
387 end
388 request.initialize()
389 return request.handler(http_request)
390 end
391 local outer_handler = http.generate_handler(inner_handler, args.http_options)
392 args.prepare = postfork_init
393 args.connect = function(socket)
394 if socket.main then
395 request.initialize()
396 local function poll(input_set, output_set, timeout)
397 local input_set_copy = table.new(input_set)
398 input_set_copy[sigterm_socket] = true
399 local timeout = not moonbridge_io.poll(input_set_copy, output_set, timeout)
400 local terminate = #(assert(sigterm_socket:read_nb())) > 0
401 if terminate then
402 return "terminate"
403 elseif timeout then
404 return "timeout"
405 else
406 return "io"
407 end
408 end
409 main_handlers[socket.main](poll)
410 io.stderr:write('Main handler "' .. socket.main .. '" terminated. Requesting shutdown.\n')
411 return false
412 elseif socket.interval then
413 request_count = request_count + 1
414 request.initialize()
415 interval_handlers[socket.interval]()
416 else
417 local success = outer_handler(socket)
418 if not success then
419 return false
420 end
421 end
422 return request_count < min_requests_per_fork
423 end
424 args.finish = execute.finalizers
425 moonbridge_listen(args)
426 end
427 end
428 --//--
430 -- execute configurations and pre-fork initializers
431 for i, config_name in ipairs(WEBMCP_CONFIG_NAMES) do
432 execute.config(config_name)
433 end
434 execute.prefork_initializers()
436 -- perform post-fork initializations (once) in case of interactive mode
437 if WEBMCP_MODE == "interactive" then
438 postfork_init()
439 end

Impressum / About Us