#include #include #include #include #include #include #include #include #include #include #include #include #include #include "shared/utils.h" #include "minecctl.h" #include "minecctl-commands.h" #include "server.h" #include "rcon-commands.h" #include "misc-commands.h" #include "misc.h" #include "config.h" static void dump_server(struct server *server) { struct saddr *saddr; info("│"); info("│ ▷ server"); info("│ ┌─────"); info("│ │ name : %s", server->name); info("│ │ file_read : %s", server->file_read ? "yes" : "no"); info("│ │ filename : %s", server->scfg.filename); switch (server->scfg.type) { case SERVER_TYPE_ANNOUNCE: info("│ │ type : announce"); break; case SERVER_TYPE_PROXY: info("│ │ type : proxy"); break; case SERVER_TYPE_UNDEFINED: _fallthrough_; default: info("│ │ type : "); break; } info("│ │ pretty_name : %s", server->scfg.pretty_name); info("│ │ announce_port : %" PRIu16, server->scfg.announce_port); info("│ │ idle_timeout : %u", server->scfg.idle_timeout); switch (server->scfg.stop_method) { case SERVER_STOP_METHOD_RCON: info("│ │ stop_method : rcon"); break; case SERVER_STOP_METHOD_SYSTEMD: info("│ │ stop_method : systemd"); break; case SERVER_STOP_METHOD_EXEC: info("│ │ stop_method : exec"); break; case SERVER_STOP_METHOD_UNDEFINED: _fallthrough_; default: info("│ │ stop_method : "); break; } switch (server->scfg.start_method) { case SERVER_START_METHOD_SYSTEMD: info("│ │ start_method : systemd"); break; case SERVER_START_METHOD_EXEC: info("│ │ start_method : exec"); break; case SERVER_START_METHOD_UNDEFINED: _fallthrough_; default: info("│ │ start_method : "); break; } info("│ │ stop_exec : %s", server->scfg.stop_exec); info("│ │ start_exec : %s", server->scfg.start_exec); info("│ │ rcon_password : %s", server->scfg.rcon_password); info("│ │ systemd_service : %s", server->scfg.systemd_service); info("│ │ systemd_obj : %s", server->scfg.systemd_obj); info("│ │ locals%s", list_empty(&server->scfg.locals) ? " : none": ""); list_for_each_entry(saddr, &server->scfg.locals, list) info("│ │ ⁃ %s", saddr->addrstr); info("│ │ remotes%s", list_empty(&server->scfg.remotes) ? " : none": ""); list_for_each_entry(saddr, &server->scfg.remotes, list) info("│ │ ⁃ %s", saddr->addrstr); info("│ │ rcons%s", list_empty(&server->scfg.rcons) ? " : none": ""); list_for_each_entry(saddr, &server->scfg.rcons, list) info("│ │ ⁃ %s", saddr->addrstr); info("│ └─────"); } void dump_config(struct cfg *cfg) { struct server *server; if (!(debug_mask & ~(DBG_ERROR | DBG_INFO | DBG_VERBOSE))) return; info("Configuration"); info("┌────────────"); info("│ cfgdir : %s", cfg->cfgdir); info("│ rcon_password : %s", cfg->rcon_password); info("│ rcon_addrstr : %s", cfg->rcon_addrstr); info("│ mc_addrstr : %s", cfg->mc_addrstr); info("│ cmd : %p", cfg->cmd); info("│ force stop : %s", cfg->force_stop ? "yes" : "no"); info("│ commands%s", cfg->commands ? "" : " : none"); if (cfg->commands) { for (char *const *cmd = cfg->commands; *cmd; cmd++) info("│ ⁃ %s", *cmd); } list_for_each_entry(server, &cfg->servers, list) dump_server(server); info("└────────────"); } _noreturn_ static void usage(bool no_error) { info("Usage: %s [OPTION...] COMMAND\n" "\n" "Valid options:\n" " -p, --rcon-password=PWD use PWD when connecting via rcon\n" " (or use environment variable RCON_PASSWORD)\n" " -r, --rcon-address=ADDR connect to rcon at ADDR\n" " (or use environment variable RCON_ADDRESS)\n" " -m, --mc-address=ADDR connect to Minecraft server at ADDR\n" " (only relevant for some commands, can also\n" " use environment variable MC_ADDRESS)\n" " -c, --cfgdir=DIR look for server configuration files in DIR\n" " (default: %s)\n" " -f, --force stop server even if it has players\n" " -v, --verbose enable extra logging\n" " -d, --debug enable debugging information\n" " -h, --help print this information\n" "\n" "Valid commands:\n" " list list known servers\n" " lint check validity of server configuration files\n" " status [SERVER] show status of SERVER (or all known servers)\n" " ping [SERVER] check if SERVER is running\n" " stop [SERVER] stop SERVER\n" " stopall stop all known servers (including ADDR)\n" " pcount [SERVER] get player count for SERVER\n" " console [SERVER] interactive command line for SERVER\n" " cmd [SERVER] CMD send CMD to SERVER\n" " cmds [SERVER] CMDS... send multiple CMDS to SERVER\n" " [SERVER] CMD shorthand for \"cmd [SERVER] CMD\"\n" " [SERVER] shorthand for \"console [SERVER]\"\n" "\n" "Note: if ADDR is given as an option, SERVER must be omitted from\n" " the command and vice versa.\n", program_invocation_short_name, DEFAULT_CFG_DIR); exit(no_error ? EXIT_FAILURE : EXIT_SUCCESS); } static bool str_to_addrs(const char *str, struct list_head *list) { struct cfg_value value; char *tmp = NULL; bool rv = false; /* strtosockaddrs mangles the input string */ tmp = xstrdup(str); if (!strtosockaddrs(tmp, &value, false)) { error("Unable to parse address: %s", str); goto out; } if (value.type != CFG_VAL_TYPE_ADDRS) { error("Unexpected return value from strtosockaddrs"); goto out; } if (list_empty(&value.saddrs)) { error("Found no valid addresses for %s", str); goto out; } list_replace(&value.saddrs, list); rv = true; out: xfree(tmp); return rv; } static bool create_server_from_cmdline_args(struct cfg *cfg) { struct server *server; if (!cfg->rcon_addrstr && !cfg->mc_addrstr) return false; server = server_new(NULL); if (cfg->rcon_addrstr) { if (!str_to_addrs(cfg->rcon_addrstr, &server->scfg.rcons)) goto error; server->name = cfg->rcon_addrstr; cfg->rcon_addrstr = NULL; } if (cfg->mc_addrstr) { if (!str_to_addrs(cfg->mc_addrstr, &server->scfg.remotes)) goto error; if (!server->name) server->name = cfg->mc_addrstr; else xfree(cfg->mc_addrstr); cfg->mc_addrstr = NULL; } if (cfg->rcon_password) { server->scfg.rcon_password = cfg->rcon_password; cfg->rcon_password = NULL; } list_add(&server->list, &cfg->servers); return true; error: server_free(server); return false; } static inline void get_optional_server_arg(struct cfg *cfg, char *const **argv, bool more) { if (!cfg->rcon_addrstr) { if (!**argv) { error("Missing arguments"); usage(false); } if (!server_set_default(cfg, **argv)) { error("\"%s\" is not a known server or command", **argv); usage(false); } (*argv)++; } if (!more && **argv) { error("Too many arguments"); usage(false); } } static void parse_command(struct cfg *cfg, char *const *argv) { enum commands cmd = CMD_INVALID; assert_die(argv, "invalid arguments"); /* Shorthand for console if address is set */ if (!*argv) { if (!cfg->rcon_addrstr) { error("Missing arguments"); usage(false); } cfg->cmd = do_console; return; } for (unsigned i = 0; command_list[i].name; i++) { if (streq(*argv, command_list[i].name)) { cmd = command_list[i].cmd; argv++; break; } } switch (cmd) { case CMD_LIST: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_list; break; case CMD_LINT: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_lint; break; case CMD_STOPALL: if (*argv) { error("Too many arguments"); usage(false); } cfg->cmd = do_stop_all; break; case CMD_STATUS: get_optional_server_arg(cfg, &argv, false); cfg->cmd = do_status; break; case CMD_PING: get_optional_server_arg(cfg, &argv, false); cfg->cmd = do_ping; break; case CMD_STOP: get_optional_server_arg(cfg, &argv, false); cfg->cmd = do_stop; break; case CMD_PCOUNT: get_optional_server_arg(cfg, &argv, false); cfg->cmd = do_pcount; break; case CMD_CONSOLE: get_optional_server_arg(cfg, &argv, false); cfg->cmd = do_console; break; case CMD_COMMAND: _fallthrough_; case CMD_COMMANDS: get_optional_server_arg(cfg, &argv, true); if (!*argv) { error("Missing arguments"); usage(false); } if (cmd == CMD_COMMANDS) cfg->commands = strv_copy(argv); else cfg->commands = strv_from_strs(strv_join(argv), NULL); cfg->cmd = do_commands; break; case CMD_INVALID: /* shorthand notation */ get_optional_server_arg(cfg, &argv, true); if (!*argv) { /* !CMD = console */ cfg->cmd = do_console; } else { /* CMD... */ cfg->commands = strv_from_strs(strv_join(argv), NULL); cfg->cmd = do_commands; } break; default: die("Unreachable"); } } static void parse_cmdline(struct cfg *cfg, int argc, char *const *argv) { int c; char *e; assert_die(argc && argv && cfg, "invalid arguments"); if (argc < 2) { error("Missing arguments"); usage(false); } cfg->cfgdir = DEFAULT_CFG_DIR; while (true) { int option_index = 0; /* clang-format off */ static struct option long_options[] = { { "rcon-password", required_argument, 0, 'p' }, { "rcon-address", required_argument, 0, 'r' }, { "mc-address", required_argument, 0, 'm' }, { "cfgdir", required_argument, 0, 'c' }, { "verbose", no_argument, 0, 'v' }, { "debug", no_argument, 0, 'd' }, { "force", no_argument, 0, 'f' }, { "help", no_argument, 0, 'h' }, { 0, 0, 0, 0 } }; /* clang-format on */ c = getopt_long(argc, argv, ":p:r:m:c:vdfh", long_options, &option_index); if (c == -1) break; switch (c) { case 'p': cfg->rcon_password = xstrdup(optarg); break; case 'r': cfg->rcon_addrstr = xstrdup(optarg); break; case 'm': cfg->mc_addrstr = xstrdup(optarg); break; case 'c': cfg->cfgdir = optarg; break; case 'v': debug_mask |= DBG_VERBOSE; break; case 'd': debug_mask = ~0; break; case 'f': cfg->force_stop = true; break; case 'h': usage(true); default: error("Invalid options"); usage(false); } } if (!cfg->rcon_password) { e = getenv("RCON_PASSWORD"); if (e) cfg->rcon_password = xstrdup(e); } if (!cfg->rcon_addrstr) { e = getenv("RCON_ADDRESS"); if (e) cfg->rcon_addrstr = xstrdup(e); } if (!cfg->mc_addrstr) { e = getenv("MC_ADDRESS"); if (e) cfg->mc_addrstr = xstrdup(e); } } int main(int argc, char *const *argv) { struct cfg cfg = { .servers = LIST_HEAD_INIT(cfg.servers), }; bool success = false; debug_mask = DBG_ERROR | DBG_INFO; set_use_colors(); parse_cmdline(&cfg, argc, argv); server_load_all_known(&cfg); parse_command(&cfg, &argv[optind]); if (!cfg.cmd) { error("Failed to parse command"); goto out; } if (cfg.rcon_addrstr || cfg.mc_addrstr) if (!create_server_from_cmdline_args(&cfg)) goto out; success = cfg.cmd(&cfg); out: server_free_all(&cfg); free_password(&cfg.rcon_password); strv_free(cfg.commands); xfree(cfg.rcon_addrstr); xfree(cfg.mc_addrstr); exit(success ? EXIT_SUCCESS : EXIT_FAILURE); }