Diff

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
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/util/startup.lua	Tue Mar 20 16:10:37 2018 +0000
@@ -0,0 +1,518 @@
+local startup = {};
+
+local prosody = { events = require "util.events".new() };
+
+local config = require "core.configmanager";
+
+local dependencies = require "util.dependencies";
+
+function startup.read_config()
+	local filenames = {};
+
+	local filename;
+	if arg[1] == "--config" and arg[2] then
+		table.insert(filenames, arg[2]);
+		if CFG_CONFIGDIR then
+			table.insert(filenames, CFG_CONFIGDIR.."/"..arg[2]);
+		end
+		table.remove(arg, 1); table.remove(arg, 1);
+	elseif os.getenv("PROSODY_CONFIG") then -- Passed by prosodyctl
+			table.insert(filenames, os.getenv("PROSODY_CONFIG"));
+	else
+		table.insert(filenames, (CFG_CONFIGDIR or ".").."/prosody.cfg.lua");
+	end
+	for _,_filename in ipairs(filenames) do
+		filename = _filename;
+		local file = io.open(filename);
+		if file then
+			file:close();
+			prosody.config_file = filename;
+			CFG_CONFIGDIR = filename:match("^(.*)[\\/][^\\/]*$");
+			break;
+		end
+	end
+	prosody.config_file = filename
+	local ok, level, err = config.load(filename);
+	if not ok then
+		print("\n");
+		print("**************************");
+		if level == "parser" then
+			print("A problem occured while reading the config file "..filename);
+			print("");
+			local err_line, err_message = tostring(err):match("%[string .-%]:(%d*): (.*)");
+			if err:match("chunk has too many syntax levels$") then
+				print("An Include statement in a config file is including an already-included");
+				print("file and causing an infinite loop. An Include statement in a config file is...");
+			else
+				print("Error"..(err_line and (" on line "..err_line) or "")..": "..(err_message or tostring(err)));
+			end
+			print("");
+		elseif level == "file" then
+			print("Prosody was unable to find the configuration file.");
+			print("We looked for: "..filename);
+			print("A sample config file is included in the Prosody download called prosody.cfg.lua.dist");
+			print("Copy or rename it to prosody.cfg.lua and edit as necessary.");
+		end
+		print("More help on configuring Prosody can be found at https://prosody.im/doc/configure");
+		print("Good luck!");
+		print("**************************");
+		print("");
+		os.exit(1);
+	end
+end
+
+function startup.check_dependencies()
+	if not dependencies.check_dependencies() then
+		os.exit(1);
+	end
+end
+
+-- luacheck: globals socket server
+
+function startup.load_libraries()
+	-- Load socket framework
+	-- luacheck: ignore 111/server 111/socket
+	socket = require "socket";
+	server = require "net.server"
+end
+
+-- The global log() gets defined by loggingmanager
+-- luacheck: ignore 113/log
+
+function startup.init_logging()
+	-- Initialize logging
+	require "core.loggingmanager"
+end
+
+function startup.log_dependency_warnings()
+	dependencies.log_warnings();
+end
+
+function startup.sanity_check()
+	for host, host_config in pairs(config.getconfig()) do
+		if host ~= "*"
+		and host_config.enabled ~= false
+		and not host_config.component_module then
+			return;
+		end
+	end
+	log("error", "No enabled VirtualHost entries found in the config file.");
+	log("error", "At least one active host is required for Prosody to function. Exiting...");
+	os.exit(1);
+end
+
+function startup.sandbox_require()
+	-- Replace require() with one that doesn't pollute _G, required
+	-- for neat sandboxing of modules
+	-- luacheck: ignore 113/getfenv 111/require
+	local _realG = _G;
+	local _real_require = require;
+	local getfenv = getfenv or function (f)
+		-- FIXME: This is a hack to replace getfenv() in Lua 5.2
+		local name, env = debug.getupvalue(debug.getinfo(f or 1).func, 1);
+		if name == "_ENV" then
+			return env;
+		end
+	end
+	function require(...)
+		local curr_env = getfenv(2);
+		local curr_env_mt = getmetatable(curr_env);
+		local _realG_mt = getmetatable(_realG);
+		if curr_env_mt and curr_env_mt.__index and not curr_env_mt.__newindex and _realG_mt then
+			local old_newindex, old_index;
+			old_newindex, _realG_mt.__newindex = _realG_mt.__newindex, curr_env;
+			old_index, _realG_mt.__index = _realG_mt.__index, function (_G, k) -- luacheck: ignore 212/_G
+				return rawget(curr_env, k);
+			end;
+			local ret = _real_require(...);
+			_realG_mt.__newindex = old_newindex;
+			_realG_mt.__index = old_index;
+			return ret;
+		end
+		return _real_require(...);
+	end
+end
+
+function startup.set_function_metatable()
+	local mt = {};
+	function mt.__index(f, upvalue)
+		local i, name, value = 0;
+		repeat
+			i = i + 1;
+			name, value = debug.getupvalue(f, i);
+		until name == upvalue or name == nil;
+		return value;
+	end
+	function mt.__newindex(f, upvalue, value)
+		local i, name = 0;
+		repeat
+			i = i + 1;
+			name = debug.getupvalue(f, i);
+		until name == upvalue or name == nil;
+		if name then
+			debug.setupvalue(f, i, value);
+		end
+	end
+	function mt.__tostring(f)
+		local info = debug.getinfo(f);
+		return ("function(%s:%d)"):format(info.short_src:match("[^\\/]*$"), info.linedefined);
+	end
+	debug.setmetatable(function() end, mt);
+end
+
+function startup.detect_platform()
+	prosody.platform = "unknown";
+	if os.getenv("WINDIR") then
+		prosody.platform = "windows";
+	elseif package.config:sub(1,1) == "/" then
+		prosody.platform = "posix";
+	end
+end
+
+function startup.detect_installed()
+	prosody.installed = nil;
+	if CFG_SOURCEDIR and (prosody.platform == "windows" or CFG_SOURCEDIR:match("^/")) then
+		prosody.installed = true;
+	end
+end
+
+function startup.chdir()
+	if prosody.installed then
+		-- Change working directory to data path.
+		require "lfs".chdir(data_path);
+	end
+end
+
+function startup.init_global_state()
+	prosody.bare_sessions = {};
+	prosody.full_sessions = {};
+	prosody.hosts = {};
+
+	-- COMPAT: These globals are deprecated
+	-- luacheck: ignore 111/bare_sessions 111/full_sessions 111/hosts
+	bare_sessions = prosody.bare_sessions;
+	full_sessions = prosody.full_sessions;
+	hosts = prosody.hosts;
+
+	local data_path = config.get("*", "data_path") or CFG_DATADIR or "data";
+	local custom_plugin_paths = config.get("*", "plugin_paths");
+	if custom_plugin_paths then
+		local path_sep = package.config:sub(3,3);
+		-- path1;path2;path3;defaultpath...
+		CFG_PLUGINDIR = table.concat(custom_plugin_paths, path_sep)..path_sep..(CFG_PLUGINDIR or "plugins");
+	end
+	prosody.paths = { source = CFG_SOURCEDIR, config = CFG_CONFIGDIR or ".",
+	                  plugins = CFG_PLUGINDIR or "plugins", data = data_path };
+
+	prosody.arg = _G.arg;
+
+	startup.detect_platform();
+	startup.detect_installed();
+	_G.prosody = prosody;
+end
+
+function startup.add_global_prosody_functions()
+	-- Function to reload the config file
+	function prosody.reload_config()
+		log("info", "Reloading configuration file");
+		prosody.events.fire_event("reloading-config");
+		local ok, level, err = config.load(prosody.config_file);
+		if not ok then
+			if level == "parser" then
+				log("error", "There was an error parsing the configuration file: %s", tostring(err));
+			elseif level == "file" then
+				log("error", "Couldn't read the config file when trying to reload: %s", tostring(err));
+			end
+		end
+		return ok, (err and tostring(level)..": "..tostring(err)) or nil;
+	end
+
+	-- Function to reopen logfiles
+	function prosody.reopen_logfiles()
+		log("info", "Re-opening log files");
+		prosody.events.fire_event("reopen-log-files");
+	end
+
+	-- Function to initiate prosody shutdown
+	function prosody.shutdown(reason, code)
+		log("info", "Shutting down: %s", reason or "unknown reason");
+		prosody.shutdown_reason = reason;
+		prosody.shutdown_code = code;
+		prosody.events.fire_event("server-stopping", {
+			reason = reason;
+			code = code;
+		});
+		server.setquitting(true);
+	end
+end
+
+function startup.load_secondary_libraries()
+	--- Load and initialise core modules
+	require "util.import"
+	require "util.xmppstream"
+	require "core.stanza_router"
+	require "core.statsmanager"
+	require "core.hostmanager"
+	require "core.portmanager"
+	require "core.modulemanager"
+	require "core.usermanager"
+	require "core.rostermanager"
+	require "core.sessionmanager"
+	package.loaded['core.componentmanager'] = setmetatable({},{__index=function()
+		log("warn", "componentmanager is deprecated: %s", debug.traceback():match("\n[^\n]*\n[ \t]*([^\n]*)"));
+		return function() end
+	end});
+
+	require "util.array"
+	require "util.datetime"
+	require "util.iterators"
+	require "util.timer"
+	require "util.helpers"
+
+	pcall(require, "util.signal") -- Not on Windows
+
+	-- Commented to protect us from
+	-- the second kind of people
+	--[[
+	pcall(require, "remdebug.engine");
+	if remdebug then remdebug.engine.start() end
+	]]
+
+	require "util.stanza"
+	require "util.jid"
+end
+
+function startup.init_http_client()
+	local http = require "net.http"
+	local config_ssl = config.get("*", "ssl") or {}
+	local https_client = config.get("*", "client_https_ssl")
+	http.default.options.sslctx = require "core.certmanager".create_context("client_https port 0", "client",
+		{ capath = config_ssl.capath, cafile = config_ssl.cafile, verify = "peer", }, https_client);
+end
+
+function startup.init_data_store()
+	require "core.storagemanager";
+end
+
+function startup.prepare_to_start()
+	log("info", "Prosody is using the %s backend for connection handling", server.get_backend());
+	-- Signal to modules that we are ready to start
+	prosody.events.fire_event("server-starting");
+	prosody.start_time = os.time();
+end
+
+function startup.init_global_protection()
+	-- Catch global accesses
+	-- luacheck: ignore 212/t
+	local locked_globals_mt = {
+		__index = function (t, k) log("warn", "%s", debug.traceback("Attempt to read a non-existent global '"..tostring(k).."'", 2)); end;
+		__newindex = function (t, k, v) error("Attempt to set a global: "..tostring(k).." = "..tostring(v), 2); end;
+	};
+
+	function prosody.unlock_globals()
+		setmetatable(_G, nil);
+	end
+
+	function prosody.lock_globals()
+		setmetatable(_G, locked_globals_mt);
+	end
+
+	-- And lock now...
+	prosody.lock_globals();
+end
+
+function startup.read_version()
+	-- Try to determine version
+	local version_file = io.open((CFG_SOURCEDIR or ".").."/prosody.version");
+	prosody.version = "unknown";
+	if version_file then
+		prosody.version = version_file:read("*a"):gsub("%s*$", "");
+		version_file:close();
+		if #prosody.version == 12 and prosody.version:match("^[a-f0-9]+$") then
+			prosody.version = "hg:"..prosody.version;
+		end
+	else
+		local hg = require"util.mercurial";
+		local hgid = hg.check_id(CFG_SOURCEDIR or ".");
+		if hgid then prosody.version = "hg:" .. hgid; end
+	end
+end
+
+function startup.log_greeting()
+	log("info", "Hello and welcome to Prosody version %s", prosody.version);
+end
+
+function startup.notify_started()
+	prosody.events.fire_event("server-started");	
+end
+
+-- Override logging config (used by prosodyctl)
+function startup.force_console_logging()
+	local original_logging_config = config.get("*", "log");
+	config.set("*", "log", { { levels = { min="info" }, to = "console" } });
+end
+
+function startup.switch_user()
+	-- Switch away from root and into the prosody user --
+	-- NOTE: This function is only used by prosodyctl.
+	-- The prosody process is built with the assumption that
+	-- it is already started as the appropriate user.
+	local switched_user, current_uid;
+
+	local want_pposix_version = "0.4.0";
+	local have_pposix, pposix = pcall(require, "util.pposix");
+
+	if have_pposix and pposix then
+		if pposix._VERSION ~= want_pposix_version then
+			print(string.format("Unknown version (%s) of binary pposix module, expected %s",
+				tostring(pposix._VERSION), want_pposix_version));
+			os.exit(1);
+		end
+		current_uid = pposix.getuid();
+		local arg_root = arg[1] == "--root";
+		if arg_root then table.remove(arg, 1); end
+		if current_uid == 0 and config.get("*", "run_as_root") ~= true and not arg_root then
+			-- We haz root!
+			local desired_user = config.get("*", "prosody_user") or "prosody";
+			local desired_group = config.get("*", "prosody_group") or desired_user;
+			local ok, err = pposix.setgid(desired_group);
+			if ok then
+				ok, err = pposix.initgroups(desired_user);
+			end
+			if ok then
+				ok, err = pposix.setuid(desired_user);
+				if ok then
+					-- Yay!
+					switched_user = true;
+				end
+			end
+			if not switched_user then
+				-- Boo!
+				print("Warning: Couldn't switch to Prosody user/group '"..tostring(desired_user).."'/'"..tostring(desired_group).."': "..tostring(err));
+			else
+				-- Make sure the Prosody user can read the config
+				local conf, err, errno = io.open(ENV_CONFIG);
+				if conf then
+					conf:close();
+				else
+					print("The config file is not readable by the '"..desired_user.."' user.");
+					print("Prosody will not be able to read it.");
+					print("Error was "..err);
+					os.exit(1);
+				end
+			end
+		end
+	
+		-- Set our umask to protect data files
+		pposix.umask(config.get("*", "umask") or "027");
+		pposix.setenv("HOME", data_path);
+		pposix.setenv("PROSODY_CONFIG", ENV_CONFIG);
+	else
+		print("Error: Unable to load pposix module. Check that Prosody is installed correctly.")
+		print("For more help send the below error to us through https://prosody.im/discuss");
+		print(tostring(pposix))
+		os.exit(1);
+	end
+end
+
+function startup.check_unwriteable()
+	local function test_writeable(filename)
+		local f, err = io.open(filename, "a");
+		if not f then
+			return false, err;
+		end
+		f:close();
+		return true;
+	end
+	
+	local unwriteable_files = {};
+	if type(original_logging_config) == "string" and original_logging_config:sub(1,1) ~= "*" then
+		local ok, err = test_writeable(original_logging_config);
+		if not ok then
+			table.insert(unwriteable_files, err);
+		end
+	elseif type(original_logging_config) == "table" then
+		for _, rule in ipairs(original_logging_config) do
+			if rule.filename then
+				local ok, err = test_writeable(rule.filename);
+				if not ok then
+					table.insert(unwriteable_files, err);
+				end
+			end
+		end
+	end
+	
+	if #unwriteable_files > 0 then
+		print("One of more of the Prosody log files are not");
+		print("writeable, please correct the errors and try");
+		print("starting prosodyctl again.");
+		print("");
+		for _, err in ipairs(unwriteable_files) do
+			print(err);
+		end
+		print("");
+		os.exit(1);
+	end
+end
+
+function startup.make_dummy_hosts()
+	-- When running under prosodyctl, we don't want to
+	-- fully initialize the server, so we populate prosody.hosts
+	-- with just enough things for most code to work correctly
+	prosody.core_post_stanza = function () end; -- TODO: mod_router!
+	local function make_host(hostname)
+		return {
+			type = "local",
+			events = prosody.events,
+			modules = {},
+			sessions = {},
+			users = require "core.usermanager".new_null_provider(hostname)
+		};
+	end
+	
+	for hostname, config in pairs(config.getconfig()) do
+		hosts[hostname] = make_host(hostname);
+	end
+end
+
+-- prosodyctl only
+function startup.prosodyctl()
+	startup.read_config();
+	startup.chdir();
+	startup.check_dependencies();
+	startup.force_console_logging();
+	startup.init_global_state();
+	startup.init_logging();
+	startup.log_dependency_warnings();
+	startup.check_unwriteable();
+	startup.load_libraries();
+	startup.init_global_protection();
+	startup.init_http_client();
+	startup.make_dummy_hosts();
+end
+
+function startup.prosody()
+	-- These actions are in a strict order, as many depend on
+	-- previous steps to have already been performed
+	startup.read_config();
+	startup.sanity_check();
+	startup.sandbox_require();
+	startup.set_function_metatable();
+	startup.check_dependencies();
+	startup.load_libraries();
+	startup.init_global_state();
+	startup.init_logging();
+	startup.chdir();
+	startup.add_global_prosody_functions();
+	startup.read_version();
+	startup.log_greeting();
+	startup.log_dependency_warnings();
+	startup.load_secondary_libraries();
+	startup.init_http_client();
+	startup.init_data_store();
+	startup.init_global_protection();
+	startup.prepare_to_start();
+--	startup.notify_started();
+end
+
+return startup;