diff options
Diffstat (limited to 'core/modulemanager.lua')
-rw-r--r-- | core/modulemanager.lua | 370 |
1 files changed, 283 insertions, 87 deletions
diff --git a/core/modulemanager.lua b/core/modulemanager.lua index 24708232..535c227b 100644 --- a/core/modulemanager.lua +++ b/core/modulemanager.lua @@ -1,123 +1,319 @@ +-- Prosody IM +-- Copyright (C) 2008-2010 Matthew Wild +-- Copyright (C) 2008-2010 Waqas Hussain +-- +-- This project is MIT/X11 licensed. Please see the +-- COPYING file in the source package for more information. +-- -local log = require "util.logger".init("modulemanager") +local logger = require "util.logger"; +local log = logger.init("modulemanager"); +local config = require "core.configmanager"; +local pluginloader = require "util.pluginloader"; +local set = require "util.set"; -local loadfile, pcall = loadfile, pcall; -local setmetatable, setfenv, getfenv = setmetatable, setfenv, getfenv; -local pairs, ipairs = pairs, ipairs; -local t_insert = table.insert; -local type = type; +local new_multitable = require "util.multitable".new; -local tostring, print = tostring, print; +local hosts = hosts; +local prosody = prosody; +local pcall, xpcall = pcall, xpcall; +local setmetatable, rawget = setmetatable, rawget; +local ipairs, pairs, type, tostring, t_insert = ipairs, pairs, type, tostring, table.insert; + +local debug_traceback = debug.traceback; +local unpack, select = unpack, select; +pcall = function(f, ...) + local n = select("#", ...); + local params = {...}; + return xpcall(function() return f(unpack(params, 1, n)) end, function(e) return tostring(e).."\n"..debug_traceback(); end); +end + +local autoload_modules = {"presence", "message", "iq", "offline", "c2s", "s2s"}; +local component_inheritable_modules = {"tls", "dialback", "iq", "s2s"}; + +-- We need this to let modules access the real global namespace local _G = _G; module "modulemanager" -local handler_info = {}; -local handlers = {}; - -local modulehelpers = setmetatable({}, { __index = _G }); - -function modulehelpers.add_iq_handler(origin_type, xmlns, handler) - if not (origin_type and handler and xmlns) then return false; end - handlers[origin_type] = handlers[origin_type] or {}; - handlers[origin_type].iq = handlers[origin_type].iq or {}; - if not handlers[origin_type].iq[xmlns] then - handlers[origin_type].iq[xmlns]= handler; - handler_info[handler] = getfenv(2).module; - log("debug", "mod_%s now handles tag 'iq' with query namespace '%s'", getfenv(2).module.name, xmlns); - else - log("warning", "mod_%s wants to handle tag 'iq' with query namespace '%s' but mod_%s already handles that", getfenv(2).module.name, xmlns, handler_info[handlers[origin_type].iq[xmlns]].module.name); - end -end +local api = _G.require "core.moduleapi"; -- Module API container + +-- [host] = { [module] = module_env } +local modulemap = { ["*"] = {} }; -function modulehelpers.add_handler(origin_type, tag, xmlns, handler) - if not (origin_type and tag and xmlns and handler) then return false; end - handlers[origin_type] = handlers[origin_type] or {}; - if not handlers[origin_type][tag] then - handlers[origin_type][tag] = handlers[origin_type][tag] or {}; - handlers[origin_type][tag][xmlns]= handler; - handler_info[handler] = getfenv(2).module; - log("debug", "mod_%s now handles tag '%s'", getfenv(2).module.name, tag); - elseif handler_info[handlers[origin_type][tag]] then - log("warning", "mod_%s wants to handle tag '%s' but mod_%s already handles that", getfenv(2).module.name, tag, handler_info[handlers[origin_type][tag]].module.name); +-- Load modules when a host is activated +function load_modules_for_host(host) + local component = config.get(host, "component_module"); + + local global_modules_enabled = config.get("*", "modules_enabled"); + local global_modules_disabled = config.get("*", "modules_disabled"); + local host_modules_enabled = config.get(host, "modules_enabled"); + local host_modules_disabled = config.get(host, "modules_disabled"); + + if host_modules_enabled == global_modules_enabled then host_modules_enabled = nil; end + if host_modules_disabled == global_modules_disabled then host_modules_disabled = nil; end + + local global_modules = set.new(autoload_modules) + set.new(global_modules_enabled) - set.new(global_modules_disabled); + if component then + global_modules = set.intersection(set.new(component_inheritable_modules), global_modules); + end + local modules = (global_modules + set.new(host_modules_enabled)) - set.new(host_modules_disabled); + + -- COMPAT w/ pre 0.8 + if modules:contains("console") then + log("error", "The mod_console plugin has been renamed to mod_admin_telnet. Please update your config."); + modules:remove("console"); + modules:add("admin_telnet"); + end + + if component then + load(host, component); + end + for module in modules do + load(host, module); end end +prosody.events.add_handler("host-activated", load_modules_for_host); +prosody.events.add_handler("host-deactivated", function (host) + modulemap[host] = nil; +end); -function loadall() - load("saslauth"); - load("legacyauth"); - load("roster"); - load("register"); - load("tls"); - load("vcard"); -end +--- Private helpers --- -function load(name) - local mod, err = loadfile("plugins/mod_"..name..".lua"); - if not mod then - log("error", "Unable to load module '%s': %s", name or "nil", err or "nil"); - return; +local function do_unload_module(host, name) + local mod = get_module(host, name); + if not mod then return nil, "module-not-loaded"; end + + if module_has_method(mod, "unload") then + local ok, err = call_module_method(mod, "unload"); + if (not ok) and err then + log("warn", "Non-fatal error unloading module '%s' on '%s': %s", name, host, err); + end end - local pluginenv = setmetatable({ module = { name = name } }, { __index = modulehelpers }); + for object, event, handler in mod.module.event_handlers:iter(nil, nil, nil) do + object.remove_handler(event, handler); + end - setfenv(mod, pluginenv); - local success, ret = pcall(mod); - if not success then - log("error", "Error initialising module '%s': %s", name or "nil", ret or "nil"); - return; + if mod.module.items then -- remove items + local events = (host == "*" and prosody.events) or hosts[host].events; + for key,t in pairs(mod.module.items) do + for i = #t,1,-1 do + local value = t[i]; + t[i] = nil; + events.fire_event("item-removed/"..key, {source = mod.module, item = value}); + end + end end + mod.module.loaded = false; + modulemap[host][name] = nil; + return true; end -function handle_stanza(origin, stanza) - local name, xmlns, origin_type = stanza.name, stanza.attr.xmlns, origin.type; +local function do_load_module(host, module_name, state) + if not (host and module_name) then + return nil, "insufficient-parameters"; + elseif not hosts[host] and host ~= "*"then + return nil, "unknown-host"; + end - if name == "iq" and xmlns == "jabber:client" and handlers[origin_type] then - log("debug", "Stanza is an <iq/>"); - local child = stanza.tags[1]; - if child then - local xmlns = child.attr.xmlns; - log("debug", "Stanza has xmlns: %s", xmlns); - local handler = handlers[origin_type][name][xmlns]; - if handler then - log("debug", "Passing stanza to mod_%s", handler_info[handler].name); - return handler(origin, stanza) or true; + if not modulemap[host] then + modulemap[host] = hosts[host].modules; + end + + if modulemap[host][module_name] then + log("warn", "%s is already loaded for %s, so not loading again", module_name, host); + return nil, "module-already-loaded"; + elseif modulemap["*"][module_name] then + local mod = modulemap["*"][module_name]; + if module_has_method(mod, "add_host") then + local _log = logger.init(host..":"..module_name); + local host_module_api = setmetatable({ + host = host, event_handlers = new_multitable(), items = {}; + _log = _log, log = function (self, ...) return _log(...); end; + },{ + __index = modulemap["*"][module_name].module; + }); + local host_module = setmetatable({ module = host_module_api }, { __index = mod }); + host_module_api.environment = host_module; + modulemap[host][module_name] = host_module; + local ok, result, module_err = call_module_method(mod, "add_host", host_module_api); + if not ok or result == false then + modulemap[host][module_name] = nil; + return nil, ok and module_err or result; end + return host_module; + end + return nil, "global-module-already-loaded"; + end + + + local _log = logger.init(host..":"..module_name); + local api_instance = setmetatable({ name = module_name, host = host, + _log = _log, log = function (self, ...) return _log(...); end, event_handlers = new_multitable(), + reloading = not not state, saved_state = state~=true and state or nil } + , { __index = api }); + + local pluginenv = setmetatable({ module = api_instance }, { __index = _G }); + api_instance.environment = pluginenv; + + local mod, err = pluginloader.load_code(module_name, nil, pluginenv); + if not mod then + log("error", "Unable to load module '%s': %s", module_name or "nil", err or "nil"); + return nil, err; + end + + api_instance.path = err; + + modulemap[host][module_name] = pluginenv; + local ok, err = pcall(mod); + if ok then + -- Call module's "load" + if module_has_method(pluginenv, "load") then + ok, err = call_module_method(pluginenv, "load"); + if not ok then + log("warn", "Error loading module '%s' on '%s': %s", module_name, host, err or "nil"); + end end - elseif handlers[origin_type] then - local handler = handlers[origin_type][name]; - if handler then - handler = handler[xmlns]; - if handler then - log("debug", "Passing stanza to mod_%s", handler_info[handler].name); - return handler(origin, stanza) or true; + api_instance.reloading, api_instance.saved_state = nil, nil; + + if api_instance.host == "*" then + if not api_instance.global then -- COMPAT w/pre-0.9 + if host ~= "*" then + log("warn", "mod_%s: Setting module.host = '*' deprecated, call module:set_global() instead", module_name); + end + api_instance:set_global(); + end + modulemap[host][module_name] = nil; + modulemap[api_instance.host][module_name] = pluginenv; + if host ~= api_instance.host and module_has_method(pluginenv, "add_host") then + -- Now load the module again onto the host it was originally being loaded on + ok, err = do_load_module(host, module_name); end end end - log("debug", "Stanza unhandled by any modules, xmlns: %s", stanza.attr.xmlns); - return false; -- we didn't handle it + if not ok then + modulemap[api_instance.host][module_name] = nil; + log("error", "Error initializing module '%s' on '%s': %s", module_name, host, err or "nil"); + end + return ok and pluginenv, err; end -do - local event_handlers = {}; - - function modulehelpers.add_event_hook(name, handler) - if not event_handlers[name] then - event_handlers[name] = {}; +local function do_reload_module(host, name) + local mod = get_module(host, name); + if not mod then return nil, "module-not-loaded"; end + + local _mod, err = pluginloader.load_code(name); -- checking for syntax errors + if not _mod then + log("error", "Unable to load module '%s': %s", name or "nil", err or "nil"); + return nil, err; + end + + local saved; + if module_has_method(mod, "save") then + local ok, ret, err = call_module_method(mod, "save"); + if ok then + saved = ret; + else + log("warn", "Error saving module '%s:%s' state: %s", host, name, ret); + if not config.get(host, "force_module_reload") then + log("warn", "Aborting reload due to error, set force_module_reload to ignore this"); + return nil, "save-state-failed"; + else + log("warn", "Continuing with reload (using the force)"); + end end - t_insert(event_handlers[name] , handler); end - - function fire_event(name, ...) - local event_handlers = event_handlers[name]; - if event_handlers then - for name, handler in ipairs(event_handlers) do - handler(...); + + mod.module.reloading = true; + do_unload_module(host, name); + local ok, err = do_load_module(host, name, saved or true); + if ok then + mod = get_module(host, name); + if module_has_method(mod, "restore") then + local ok, err = call_module_method(mod, "restore", saved or {}) + if (not ok) and err then + log("warn", "Error restoring module '%s' from '%s': %s", name, host, err); end end end + return ok and mod, err; +end + +--- Public API --- + +-- Load a module and fire module-loaded event +function load(host, name) + local mod, err = do_load_module(host, name); + if mod then + (hosts[mod.module.host] or prosody).events.fire_event("module-loaded", { module = name, host = mod.module.host }); + end + return mod, err; +end + +-- Unload a module and fire module-unloaded +function unload(host, name) + local ok, err = do_unload_module(host, name); + if ok then + (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); + end + return ok, err; +end + +function reload(host, name) + local mod, err = do_reload_module(host, name); + if mod then + modulemap[host][name].module.reloading = true; + (hosts[host] or prosody).events.fire_event("module-reloaded", { module = name, host = host }); + mod.module.reloading = nil; + elseif not is_loaded(host, name) then + (hosts[host] or prosody).events.fire_event("module-unloaded", { module = name, host = host }); + end + return mod, err; +end + +function get_module(host, name) + return modulemap[host] and modulemap[host][name]; +end + +function get_items(key, host) + local result = {}; + local modules = modulemap[host]; + if not key or not host or not modules then return nil; end + + for _, module in pairs(modules) do + local mod = module.module; + if mod.items and mod.items[key] then + for _, value in ipairs(mod.items[key]) do + t_insert(result, value); + end + end + end + + return result; +end + +function get_modules(host) + return modulemap[host]; +end + +function is_loaded(host, name) + return modulemap[host] and modulemap[host][name] and true; +end + +function module_has_method(module, method) + return type(rawget(module.module, method)) == "function"; +end + +function call_module_method(module, method, ...) + local f = rawget(module.module, method); + if type(f) == "function" then + return pcall(f, ...); + else + return false, "no-such-method"; + end end return _M; |