view util/prosodyctl/shell.lua @ 14016:1df1782ccdfe

prosodyctl shell: Improve process exit codes This fixes a bug introduced in 3326c8a29a86 where prosodyctl shell would always exit with non-zero error codes, even on success. Now it will exit with 0 for success, or 1 if any errors were printed. It also updates the other exit codes in the shell command to help distinguish between different error cases. These are based on the sysexits.h codes, although it is not recommended to rely on that interface.
author Matthew Wild <mwild1@gmail.com>
date Fri, 19 Dec 2025 15:32:26 +0000
parents 3326c8a29a86
children 3c2bfa6a69df
line wrap: on
line source

local config = require "prosody.core.configmanager";
local human_io = require "prosody.util.human.io";
local server = require "prosody.net.server";
local st = require "prosody.util.stanza";
local path = require "prosody.util.paths";
local parse_args = require "prosody.util.argparse".parse;
local tc = require "prosody.util.termcolours";
local isatty = require "prosody.util.pposix".isatty;
local term_width = require"prosody.util.human.io".term_width;

local have_readline, readline = pcall(require, "readline");

local adminstream = require "prosody.util.adminstream";

if have_readline then
	readline.set_readline_name("prosody");
	readline.set_options({
			histfile = path.join(prosody.paths.data, ".shell_history");
			ignoredups = true;
		});
end

local function read_line(prompt_string)
	if have_readline then
		return readline.readline(prompt_string);
	else
		io.write(prompt_string);
		return io.read("*line");
	end
end

local function send_line(client, line, interactive)
	client.send(st.stanza("repl-input", { width = tostring(term_width(-1)), repl = interactive == false and "0" or "1" }):text(line));
end

local function repl(client)
	local line = read_line(client.prompt_string or "prosody> ");
	if not line or line == "quit" or line == "exit" or line == "bye" then
		if not line then
			print("");
		end
		if have_readline then
			readline.save_history();
		end
		os.exit(0, true);
	end
	send_line(client, line);
end

local function printbanner()
	local banner = config.get("*", "console_banner");
	if banner then return print(banner); end
	print([[
                     ____                \   /     _
                    |  _ \ _ __ ___  ___  _-_   __| |_   _
                    | |_) | '__/ _ \/ __|/ _ \ / _` | | | |
                    |  __/| | | (_) \__ \ |_| | (_| | |_| |
                    |_|   |_|  \___/|___/\___/ \__,_|\__, |
                    A study in simplicity            |___/

]]);
	print("Welcome to the Prosody administration console. For a list of commands, type: help");
	print("You may find more help on using this console in our online documentation at ");
	print("https://prosody.im/doc/console\n");
end

local function check()
	local lfs = require "lfs";
	local socket_path = path.resolve_relative_path(prosody.paths.data, config.get("*", "admin_socket") or "prosody.sock");
	local state = lfs.attributes(socket_path, "mode");
	return state == "socket";
end

local function start(arg) --luacheck: ignore 212/arg
	local client = adminstream.client();
	local opts, err, where = parse_args(arg);
	local ttyout = isatty(io.stdout);

	if not opts then
		if err == "param-not-found" then
			print("Unknown command-line option: "..tostring(where));
		elseif err == "missing-value" then
			print("Expected a value to follow command-line option: "..where);
		end
		os.exit(64);
	end

	if arg[1] then
		if arg[2] then
			arg[1] = ("{"..string.rep("%q", #arg, ", ").."}"):format(table.unpack(arg, 1, #arg));
		end

		client.events.add_handler("connected", function()
			send_line(client, arg[1], false);
			return true;
		end, 1);

		local errors = 0; -- TODO This is weird, but works for now.
		client.events.add_handler("received", function(stanza)
			if stanza.name == "repl-output" or stanza.name == "repl-result" then
				local dest = io.stdout;
				if stanza.name == "repl-result" or stanza.attr.type == "error" then
					if stanza.attr.type == "error" then
						errors = errors + 1;
					end
					dest = io.stderr;
				end
				if stanza.attr.eol == "0" then
					dest:write(stanza:get_text());
				else
					dest:write(stanza:get_text(), "\n");
				end
			end
			if stanza.name == "repl-result" then
				os.exit(errors > 0 and 1 or 0);
			end
			return true;
		end, 1);
	end

	client.events.add_handler("connected", function ()
		if not opts.quiet then
			printbanner();
		end
		repl(client);
	end);

	client.events.add_handler("disconnected", function ()
		print("--- session closed ---");
		os.exit(0, true);
	end);

	client.events.add_handler("received", function (stanza)
		if stanza.name ~= "repl-request-input" then
			return;
		end
		if stanza.attr.type == "password" then
			local password = human_io.read_password();
			client.send(st.stanza("repl-requested-input", {
				type = stanza.attr.type;
				id = stanza.attr.id;
				status = password and "submit" or "cancel";
			}):text(password or ""));
		else
			io.stderr:write("Internal error - unexpected input request type "..tostring(stanza.attr.type).."\n");
			os.exit(76);
		end
		return true;
	end, 2);


	client.events.add_handler("received", function (stanza)
		if stanza.name == "repl-output" or stanza.name == "repl-result" then
			local result_prefix = stanza.attr.type == "error" and "!" or "|";
			local out = result_prefix.." "..stanza:get_text();
			if ttyout and stanza.attr.type == "error" then
				out = tc.getstring(tc.getstyle("red"), out);
			end
			print(out);
		end
		if stanza.name == "repl-result" then
			repl(client);
		end
	end);

	client.prompt_string = config.get("*", "admin_shell_prompt");

	local socket_path = path.resolve_relative_path(prosody.paths.data, opts.socket or config.get("*", "admin_socket") or "prosody.sock");
	local conn = adminstream.connection(socket_path, client.listeners);
	local ok, err = conn:connect();
	if not ok then
		if err == "no unix socket support" then
			print("** LuaSocket unix socket support not available or incompatible, ensure your");
			print("** version is up to date.");
		else
			print("** Unable to connect to server - is it running? Is mod_admin_shell enabled?");
			print("** Connection error: "..err);
		end
		os.exit(69);
	end
	server.loop();
end

return {
	shell = start;
	available = check;
};