Comparison

util/startup.lua @ 8635:47e3b8b6f17a

prosody, prosodyctl, util.startup: Finally factor out startup-related and common code into a separate module
author Matthew Wild <mwild1@gmail.com>
date Tue, 20 Mar 2018 16:10:37 +0000
child 8636:8691083420e4
comparison
equal deleted inserted replaced
8634:f6f62c92b642 8635:47e3b8b6f17a
1 local startup = {};
2
3 local prosody = { events = require "util.events".new() };
4
5 local config = require "core.configmanager";
6
7 local dependencies = require "util.dependencies";
8
9 function startup.read_config()
10 local filenames = {};
11
12 local filename;
13 if arg[1] == "--config" and arg[2] then
14 table.insert(filenames, arg[2]);
15 if CFG_CONFIGDIR then
16 table.insert(filenames, CFG_CONFIGDIR.."/"..arg[2]);
17 end
18 table.remove(arg, 1); table.remove(arg, 1);
19 elseif os.getenv("PROSODY_CONFIG") then -- Passed by prosodyctl
20 table.insert(filenames, os.getenv("PROSODY_CONFIG"));
21 else
22 table.insert(filenames, (CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
23 end
24 for _,_filename in ipairs(filenames) do
25 filename = _filename;
26 local file = io.open(filename);
27 if file then
28 file:close();
29 prosody.config_file = filename;
30 CFG_CONFIGDIR = filename:match("^(.*)[\\/][^\\/]*$");
31 break;
32 end
33 end
34 prosody.config_file = filename
35 local ok, level, err = config.load(filename);
36 if not ok then
37 print("\n");
38 print("**************************");
39 if level == "parser" then
40 print("A problem occured while reading the config file "..filename);
41 print("");
42 local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)");
43 if err:match("chunk has too many syntax levels$") then
44 print("An Include statement in a config file is including an already-included");
45 print("file and causing an infinite loop. An Include statement in a config file is...");
46 else
47 print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err)));
48 end
49 print("");
50 elseif level == "file" then
51 print("Prosody was unable to find the configuration file.");
52 print("We looked for: "..filename);
53 print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist");
54 print("Copy or rename it to prosody.cfg.lua and edit as necessary.");
55 end
56 print("More help on configuring Prosody can be found at https://prosody.im/doc/configure");
57 print("Good luck!");
58 print("**************************");
59 print("");
60 os.exit(1);
61 end
62 end
63
64 function startup.check_dependencies()
65 if not dependencies.check_dependencies() then
66 os.exit(1);
67 end
68 end
69
70 -- luacheck: globals socket server
71
72 function startup.load_libraries()
73 -- Load socket framework
74 -- luacheck: ignore 111/server 111/socket
75 socket = require "socket";
76 server = require "net.server"
77 end
78
79 -- The global log() gets defined by loggingmanager
80 -- luacheck: ignore 113/log
81
82 function startup.init_logging()
83 -- Initialize logging
84 require "core.loggingmanager"
85 end
86
87 function startup.log_dependency_warnings()
88 dependencies.log_warnings();
89 end
90
91 function startup.sanity_check()
92 for host, host_config in pairs(config.getconfig()) do
93 if host ~= "*"
94 and host_config.enabled ~= false
95 and not host_config.component_module then
96 return;
97 end
98 end
99 log("error", "No enabled VirtualHost entries found in the config file.");
100 log("error", "At least one active host is required for Prosody to function. Exiting...");
101 os.exit(1);
102 end
103
104 function startup.sandbox_require()
105 -- Replace require() with one that doesn't pollute _G, required
106 -- for neat sandboxing of modules
107 -- luacheck: ignore 113/getfenv 111/require
108 local _realG = _G;
109 local _real_require = require;
110 local getfenv = getfenv or function (f)
111 -- FIXME: This is a hack to replace getfenv() in Lua 5.2
112 local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1);
113 if name == "_ENV" then
114 return env;
115 end
116 end
117 function require(...)
118 local curr_env = getfenv(2);
119 local curr_env_mt = getmetatable(curr_env);
120 local _realG_mt = getmetatable(_realG);
121 if curr_env_mt and curr_env_mt.__index and not curr_env_mt.__newindex and _realG_mt then
122 local old_newindex, old_index;
123 old_newindex, _realG_mt.__newindex = _realG_mt.__newindex, curr_env;
124 old_index, _realG_mt.__index = _realG_mt.__index, function (_G, k) -- luacheck: ignore 212/_G
125 return rawget(curr_env, k);
126 end;
127 local ret = _real_require(...);
128 _realG_mt.__newindex = old_newindex;
129 _realG_mt.__index = old_index;
130 return ret;
131 end
132 return _real_require(...);
133 end
134 end
135
136 function startup.set_function_metatable()
137 local mt = {};
138 function mt.__index(f, upvalue)
139 local i, name, value = 0;
140 repeat
141 i = i + 1;
142 name, value = debug.getupvalue(f, i);
143 until name == upvalue or name == nil;
144 return value;
145 end
146 function mt.__newindex(f, upvalue, value)
147 local i, name = 0;
148 repeat
149 i = i + 1;
150 name = debug.getupvalue(f, i);
151 until name == upvalue or name == nil;
152 if name then
153 debug.setupvalue(f, i, value);
154 end
155 end
156 function mt.__tostring(f)
157 local info = debug.getinfo(f);
158 return ("function(%s:%d)"):format(info.short_src:match("[^\\/]*$"), info.linedefined);
159 end
160 debug.setmetatable(function() end, mt);
161 end
162
163 function startup.detect_platform()
164 prosody.platform = "unknown";
165 if os.getenv("WINDIR") then
166 prosody.platform = "windows";
167 elseif package.config:sub(1,1) == "/" then
168 prosody.platform = "posix";
169 end
170 end
171
172 function startup.detect_installed()
173 prosody.installed = nil;
174 if CFG_SOURCEDIR and (prosody.platform == "windows" or CFG_SOURCEDIR:match("^/")) then
175 prosody.installed = true;
176 end
177 end
178
179 function startup.chdir()
180 if prosody.installed then
181 -- Change working directory to data path.
182 require "lfs".chdir(data_path);
183 end
184 end
185
186 function startup.init_global_state()
187 prosody.bare_sessions = {};
188 prosody.full_sessions = {};
189 prosody.hosts = {};
190
191 -- COMPAT: These globals are deprecated
192 -- luacheck: ignore 111/bare_sessions 111/full_sessions 111/hosts
193 bare_sessions = prosody.bare_sessions;
194 full_sessions = prosody.full_sessions;
195 hosts = prosody.hosts;
196
197 local data_path = config.get("*", "data_path") or CFG_DATADIR or "data";
198 local custom_plugin_paths = config.get("*", "plugin_paths");
199 if custom_plugin_paths then
200 local path_sep = package.config:sub(3,3);
201 -- path1;path2;path3;defaultpath...
202 CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
203 end
204 prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR or ".",
205 plugins = CFG_PLUGINDIR or "plugins", data = data_path };
206
207 prosody.arg = _G.arg;
208
209 startup.detect_platform();
210 startup.detect_installed();
211 _G.prosody = prosody;
212 end
213
214 function startup.add_global_prosody_functions()
215 -- Function to reload the config file
216 function prosody.reload_config()
217 log("info", "Reloading configuration file");
218 prosody.events.fire_event("reloading-config");
219 local ok, level, err = config.load(prosody.config_file);
220 if not ok then
221 if level == "parser" then
222 log("error", "There was an error parsing the configuration file: %s", tostring(err));
223 elseif level == "file" then
224 log("error", "Couldn't read the config file when trying to reload: %s", tostring(err));
225 end
226 end
227 return ok, (err and tostring(level)..": "..tostring(err)) or nil;
228 end
229
230 -- Function to reopen logfiles
231 function prosody.reopen_logfiles()
232 log("info", "Re-opening log files");
233 prosody.events.fire_event("reopen-log-files");
234 end
235
236 -- Function to initiate prosody shutdown
237 function prosody.shutdown(reason, code)
238 log("info", "Shutting down: %s", reason or "unknown reason");
239 prosody.shutdown_reason = reason;
240 prosody.shutdown_code = code;
241 prosody.events.fire_event("server-stopping", {
242 reason = reason;
243 code = code;
244 });
245 server.setquitting(true);
246 end
247 end
248
249 function startup.load_secondary_libraries()
250 --- Load and initialise core modules
251 require "util.import"
252 require "util.xmppstream"
253 require "core.stanza_router"
254 require "core.statsmanager"
255 require "core.hostmanager"
256 require "core.portmanager"
257 require "core.modulemanager"
258 require "core.usermanager"
259 require "core.rostermanager"
260 require "core.sessionmanager"
261 package.loaded['core.componentmanager'] = setmetatable({},{__index=function()
262 log("warn", "componentmanager is deprecated: %s", debug.traceback():match("\n[^\n]*\n[ \t]*([^\n]*)"));
263 return function() end
264 end});
265
266 require "util.array"
267 require "util.datetime"
268 require "util.iterators"
269 require "util.timer"
270 require "util.helpers"
271
272 pcall(require, "util.signal") -- Not on Windows
273
274 -- Commented to protect us from
275 -- the second kind of people
276 --[[
277 pcall(require, "remdebug.engine");
278 if remdebug then remdebug.engine.start() end
279 ]]
280
281 require "util.stanza"
282 require "util.jid"
283 end
284
285 function startup.init_http_client()
286 local http = require "net.http"
287 local config_ssl = config.get("*", "ssl") or {}
288 local https_client = config.get("*", "client_https_ssl")
289 http.default.options.sslctx = require "core.certmanager".create_context("client_https port 0", "client",
290 { capath = config_ssl.capath, cafile = config_ssl.cafile, verify = "peer", }, https_client);
291 end
292
293 function startup.init_data_store()
294 require "core.storagemanager";
295 end
296
297 function startup.prepare_to_start()
298 log("info", "Prosody is using the %s backend for connection handling", server.get_backend());
299 -- Signal to modules that we are ready to start
300 prosody.events.fire_event("server-starting");
301 prosody.start_time = os.time();
302 end
303
304 function startup.init_global_protection()
305 -- Catch global accesses
306 -- luacheck: ignore 212/t
307 local locked_globals_mt = {
308 __index = function (t, k) log("warn", "%s", debug.traceback("Attempt to read a non-existent global '"..tostring(k).."'", 2)); end;
309 __newindex = function (t, k, v) error("Attempt to set a global: "..tostring(k).." = "..tostring(v), 2); end;
310 };
311
312 function prosody.unlock_globals()
313 setmetatable(_G, nil);
314 end
315
316 function prosody.lock_globals()
317 setmetatable(_G, locked_globals_mt);
318 end
319
320 -- And lock now...
321 prosody.lock_globals();
322 end
323
324 function startup.read_version()
325 -- Try to determine version
326 local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version");
327 prosody.version = "unknown";
328 if version_file then
329 prosody.version = version_file:read("*a"):gsub("%s*$", "");
330 version_file:close();
331 if #prosody.version == 12 and prosody.version:match("^[a-f0-9]+$") then
332 prosody.version = "hg:"..prosody.version;
333 end
334 else
335 local hg = require"util.mercurial";
336 local hgid = hg.check_id(CFG_SOURCEDIR or ".");
337 if hgid then prosody.version = "hg:" .. hgid; end
338 end
339 end
340
341 function startup.log_greeting()
342 log("info", "Hello and welcome to Prosody version %s", prosody.version);
343 end
344
345 function startup.notify_started()
346 prosody.events.fire_event("server-started");
347 end
348
349 -- Override logging config (used by prosodyctl)
350 function startup.force_console_logging()
351 local original_logging_config = config.get("*", "log");
352 config.set("*", "log", { { levels = { min="info" }, to = "console" } });
353 end
354
355 function startup.switch_user()
356 -- Switch away from root and into the prosody user --
357 -- NOTE: This function is only used by prosodyctl.
358 -- The prosody process is built with the assumption that
359 -- it is already started as the appropriate user.
360 local switched_user, current_uid;
361
362 local want_pposix_version = "0.4.0";
363 local have_pposix, pposix = pcall(require, "util.pposix");
364
365 if have_pposix and pposix then
366 if pposix._VERSION ~= want_pposix_version then
367 print(string.format("Unknown version (%s) of binary pposix module, expected %s",
368 tostring(pposix._VERSION), want_pposix_version));
369 os.exit(1);
370 end
371 current_uid = pposix.getuid();
372 local arg_root = arg[1] == "--root";
373 if arg_root then table.remove(arg, 1); end
374 if current_uid == 0 and config.get("*", "run_as_root") ~= true and not arg_root then
375 -- We haz root!
376 local desired_user = config.get("*", "prosody_user") or "prosody";
377 local desired_group = config.get("*", "prosody_group") or desired_user;
378 local ok, err = pposix.setgid(desired_group);
379 if ok then
380 ok, err = pposix.initgroups(desired_user);
381 end
382 if ok then
383 ok, err = pposix.setuid(desired_user);
384 if ok then
385 -- Yay!
386 switched_user = true;
387 end
388 end
389 if not switched_user then
390 -- Boo!
391 print("Warning: Couldn't switch to Prosody user/group '"..tostring(desired_user).."'/'"..tostring(desired_group).."': "..tostring(err));
392 else
393 -- Make sure the Prosody user can read the config
394 local conf, err, errno = io.open(ENV_CONFIG);
395 if conf then
396 conf:close();
397 else
398 print("The config file is not readable by the '"..desired_user.."' user.");
399 print("Prosody will not be able to read it.");
400 print("Error was "..err);
401 os.exit(1);
402 end
403 end
404 end
405
406 -- Set our umask to protect data files
407 pposix.umask(config.get("*", "umask") or "027");
408 pposix.setenv("HOME", data_path);
409 pposix.setenv("PROSODY_CONFIG", ENV_CONFIG);
410 else
411 print("Error: Unable to load pposix module. Check that Prosody is installed correctly.")
412 print("For more help send the below error to us through https://prosody.im/discuss");
413 print(tostring(pposix))
414 os.exit(1);
415 end
416 end
417
418 function startup.check_unwriteable()
419 local function test_writeable(filename)
420 local f, err = io.open(filename, "a");
421 if not f then
422 return false, err;
423 end
424 f:close();
425 return true;
426 end
427
428 local unwriteable_files = {};
429 if type(original_logging_config) == "string" and original_logging_config:sub(1,1) ~= "*" then
430 local ok, err = test_writeable(original_logging_config);
431 if not ok then
432 table.insert(unwriteable_files, err);
433 end
434 elseif type(original_logging_config) == "table" then
435 for _, rule in ipairs(original_logging_config) do
436 if rule.filename then
437 local ok, err = test_writeable(rule.filename);
438 if not ok then
439 table.insert(unwriteable_files, err);
440 end
441 end
442 end
443 end
444
445 if #unwriteable_files > 0 then
446 print("One of more of the Prosody log files are not");
447 print("writeable, please correct the errors and try");
448 print("starting prosodyctl again.");
449 print("");
450 for _, err in ipairs(unwriteable_files) do
451 print(err);
452 end
453 print("");
454 os.exit(1);
455 end
456 end
457
458 function startup.make_dummy_hosts()
459 -- When running under prosodyctl, we don't want to
460 -- fully initialize the server, so we populate prosody.hosts
461 -- with just enough things for most code to work correctly
462 prosody.core_post_stanza = function () end; -- TODO: mod_router!
463 local function make_host(hostname)
464 return {
465 type = "local",
466 events = prosody.events,
467 modules = {},
468 sessions = {},
469 users = require "core.usermanager".new_null_provider(hostname)
470 };
471 end
472
473 for hostname, config in pairs(config.getconfig()) do
474 hosts[hostname] = make_host(hostname);
475 end
476 end
477
478 -- prosodyctl only
479 function startup.prosodyctl()
480 startup.read_config();
481 startup.chdir();
482 startup.check_dependencies();
483 startup.force_console_logging();
484 startup.init_global_state();
485 startup.init_logging();
486 startup.log_dependency_warnings();
487 startup.check_unwriteable();
488 startup.load_libraries();
489 startup.init_global_protection();
490 startup.init_http_client();
491 startup.make_dummy_hosts();
492 end
493
494 function startup.prosody()
495 -- These actions are in a strict order, as many depend on
496 -- previous steps to have already been performed
497 startup.read_config();
498 startup.sanity_check();
499 startup.sandbox_require();
500 startup.set_function_metatable();
501 startup.check_dependencies();
502 startup.load_libraries();
503 startup.init_global_state();
504 startup.init_logging();
505 startup.chdir();
506 startup.add_global_prosody_functions();
507 startup.read_version();
508 startup.log_greeting();
509 startup.log_dependency_warnings();
510 startup.load_secondary_libraries();
511 startup.init_http_client();
512 startup.init_data_store();
513 startup.init_global_protection();
514 startup.prepare_to_start();
515 -- startup.notify_started();
516 end
517
518 return startup;