Mercurial > prosody-hg
comparison plugins/mod_admin_shell.lua @ 14113:b9688ac25255 13.0
mod_admin_shell: Improve per-host command cleanup
In some cases, host command handlers were not being cleaned up after the
source module was unloaded, for example. This is because mod_admin_shell saw
that the handler being removed did not match the installed handler. But this
is expected for the new per-host commands, because the installed handler is a
wrapper function which routes between the hosts.
The wrapper function itself also held a reference via upvalue to the first
command object which added it, just so it could read the argument names and
such. This could cause command objects, and anything referenced by the
handler, to stay around in memory longer than expected, even after a module
has been unloaded or a host deactivated.
The new logic breaks out the handler function to eliminate any unexpected
upvalues. It improves the cleanup logic when a command is removed. Finally,
it also cleans up commands when mod_admin_shell itself is unloaded.
| author | Matthew Wild <mwild1@gmail.com> |
|---|---|
| date | Fri, 27 Mar 2026 12:43:10 +0000 |
| parents | 2aeb5dcb36bf |
| children | 44a8c8d4146a 0e9e3efa381f |
comparison
equal
deleted
inserted
replaced
| 14111:1cf6e3f49d10 | 14113:b9688ac25255 |
|---|---|
| 2815 | 2815 |
| 2816 -- host_commands[section..":"..name][host] = handler | 2816 -- host_commands[section..":"..name][host] = handler |
| 2817 -- host_commands[section..":"..name][false] = metadata | 2817 -- host_commands[section..":"..name][false] = metadata |
| 2818 local host_commands = {}; | 2818 local host_commands = {}; |
| 2819 | 2819 |
| 2820 -- Remove command handlers for a host (specify name to clear just one command) | |
| 2821 function clear_host_commands(host, name) | |
| 2822 for command_name, command_info in pairs(host_commands) do | |
| 2823 if not name or name == command_name then | |
| 2824 command_info[host] = nil; | |
| 2825 local has_any_host_handlers_left; | |
| 2826 for k in pairs(command_info) do | |
| 2827 if k ~= false then | |
| 2828 has_any_host_handlers_left = true; | |
| 2829 break; | |
| 2830 end | |
| 2831 end | |
| 2832 if not has_any_host_handlers_left then | |
| 2833 host_commands[command_name] = nil; | |
| 2834 end | |
| 2835 if name then | |
| 2836 break; | |
| 2837 end | |
| 2838 end | |
| 2839 end | |
| 2840 end | |
| 2841 | |
| 2842 -- This returns a wrapper function which routes to the correct handler for a host | |
| 2843 local function new_host_command_handler(qualified_name, selector_name, selector_index) | |
| 2844 return function (...) | |
| 2845 local selected_host = select(2, jid_split((select(selector_index, ...)))); | |
| 2846 if type(selected_host) ~= "string" then | |
| 2847 return nil, "Invalid or missing argument '"..selector_name.."'"; | |
| 2848 end | |
| 2849 if not prosody.hosts[selected_host] then | |
| 2850 return nil, "Unknown host: "..selected_host; | |
| 2851 end | |
| 2852 local host_handler = host_commands[qualified_name][selected_host]; | |
| 2853 if not host_handler then | |
| 2854 return nil, "This command is not available on "..selected_host; | |
| 2855 end | |
| 2856 return host_handler(...); | |
| 2857 end; | |
| 2858 end | |
| 2859 | |
| 2820 local function new_item_handlers(command_host) | 2860 local function new_item_handlers(command_host) |
| 2821 local function on_command_added(event) | 2861 local function on_command_added(event) |
| 2822 local command = event.item; | 2862 local command = event.item; |
| 2823 local mod_name = event.source and ("mod_"..event.source.name) or "<unknown module>"; | 2863 local mod_name = event.source and ("mod_"..event.source.name) or "<unknown module>"; |
| 2824 if not schema.validate(command_metadata_schema, command) or type(command.handler) ~= "function" then | 2864 if not schema.validate(command_metadata_schema, command) or type(command.handler) ~= "function" then |
| 2848 return; | 2888 return; |
| 2849 end | 2889 end |
| 2850 host_command_info = { | 2890 host_command_info = { |
| 2851 [false] = { | 2891 [false] = { |
| 2852 host_selector = command.host_selector; | 2892 host_selector = command.host_selector; |
| 2853 handler = function (...) | 2893 handler = new_host_command_handler(qualified_name, command.host_selector, selector_index); |
| 2854 local selected_host = select(2, jid_split((select(selector_index, ...)))); | |
| 2855 if type(selected_host) ~= "string" then | |
| 2856 return nil, "Invalid or missing argument '"..command.host_selector.."'"; | |
| 2857 end | |
| 2858 if not prosody.hosts[selected_host] then | |
| 2859 return nil, "Unknown host: "..selected_host; | |
| 2860 end | |
| 2861 local host_handler = host_commands[qualified_name][selected_host]; | |
| 2862 if not host_handler then | |
| 2863 return nil, "This command is not available on "..selected_host; | |
| 2864 end | |
| 2865 return host_handler(...); | |
| 2866 end; | |
| 2867 }; | 2894 }; |
| 2868 }; | 2895 }; |
| 2869 host_commands[qualified_name] = host_command_info; | 2896 host_commands[qualified_name] = host_command_info; |
| 2870 end | 2897 end |
| 2871 if host_command_info[command_host] then | 2898 if host_command_info[command_host] then |
| 2872 module:log("warn", "Command %s() is already registered - overwriting with %s", qualified_name, mod_name); | 2899 module:log("warn", "Command %s() is already registered on %s - overwriting with %s", qualified_name, command_host, mod_name); |
| 2873 end | 2900 end |
| 2874 host_command_info[command_host] = handler; | 2901 host_command_info[command_host] = handler; |
| 2875 end | 2902 end |
| 2876 | 2903 |
| 2877 local section_t = def_env[command.section]; | 2904 local section_t = def_env[command.section]; |
| 2928 return; | 2955 return; |
| 2929 end | 2956 end |
| 2930 | 2957 |
| 2931 local section_t = def_env[command.section]; | 2958 local section_t = def_env[command.section]; |
| 2932 if not section_t or section_t[command.name] ~= handler then | 2959 if not section_t or section_t[command.name] ~= handler then |
| 2933 return; | 2960 -- Handler doesn't match, but it could be the host-selector wrapper |
| 2961 -- Let's try removing a per-host command | |
| 2962 local qualified_name = command.section..":"..command.name; | |
| 2963 local host_command_handlers = host_commands[qualified_name]; | |
| 2964 if host_command_handlers then | |
| 2965 -- Bingo! | |
| 2966 module:log("debug", "Removing command %s from %s", qualified_name, command_host); | |
| 2967 clear_host_commands(command_host, qualified_name); | |
| 2968 elseif section_t then | |
| 2969 module:log("warn", "Not removing shell command: handler mismatch: %q ~= %q", section_t and section_t[command.name], handler); | |
| 2970 return; | |
| 2971 end | |
| 2934 end | 2972 end |
| 2935 | 2973 |
| 2936 section_t[command.name] = nil; | 2974 section_t[command.name] = nil; |
| 2937 if next(section_t) == nil then -- Delete section if empty | 2975 if next(section_t) == nil then -- Delete section if empty |
| 2938 def_env[command.section] = nil; | 2976 def_env[command.section] = nil; |
| 2939 end | 2977 end |
| 2940 | |
| 2941 if command_host then | |
| 2942 local host_command_info = host_commands[command.section..":"..command.name]; | |
| 2943 if host_command_info then | |
| 2944 -- Remove our host handler | |
| 2945 host_command_info[command_host] = nil; | |
| 2946 -- Clean up entire command entry if there are no per-host handlers left | |
| 2947 local any_hosts = false; | |
| 2948 for k in pairs(host_command_info) do | |
| 2949 if k then -- metadata is false, ignore it | |
| 2950 any_hosts = true; | |
| 2951 break; | |
| 2952 end | |
| 2953 end | |
| 2954 if not any_hosts then | |
| 2955 host_commands[command.section..":"..command.name] = nil; | |
| 2956 end | |
| 2957 end | |
| 2958 end | |
| 2959 end | 2978 end |
| 2960 return on_command_added, on_command_removed; | 2979 return on_command_added, on_command_removed; |
| 2961 end | 2980 end |
| 2962 | 2981 |
| 2963 module:handle_items("shell-command", new_item_handlers()); | 2982 module:handle_items("shell-command", new_item_handlers()); |
| 2964 | 2983 |
| 2965 function module.add_host(host_module) | 2984 function module.add_host(host_module) |
| 2966 host_module:handle_items("shell-command", new_item_handlers(host_module.host)); | 2985 host_module:handle_items("shell-command", new_item_handlers(host_module.host)); |
| 2986 function host_module.unload() | |
| 2987 -- Due to non-deterministic unload order, we might not see item | |
| 2988 -- removal events for all commands before we get unloaded, so | |
| 2989 -- we clear everything from the host explicitly when we're | |
| 2990 -- unloaded (also sensible if we are unloaded manually) | |
| 2991 clear_host_commands(host_module.host); | |
| 2992 end | |
| 2967 end | 2993 end |
| 2968 | 2994 |
| 2969 function module.unload() | 2995 function module.unload() |
| 2970 stanza_watchers.cleanup(); | 2996 stanza_watchers.cleanup(); |
| 2971 end | 2997 end |
