diff options
-rw-r--r-- | minecctl/minecctl-commands.h | 5 | ||||
-rw-r--r-- | minecctl/minecctl.c | 25 | ||||
-rw-r--r-- | minecctl/minecctl.h | 7 | ||||
-rw-r--r-- | minecctl/misc-commands.c | 241 | ||||
-rw-r--r-- | minecctl/misc-commands.h | 2 | ||||
-rw-r--r-- | minecctl/misc.c | 73 | ||||
-rw-r--r-- | minecctl/misc.h | 14 | ||||
-rw-r--r-- | minecctl/rcon-commands.c | 33 | ||||
-rw-r--r-- | minecctl/server.c | 117 | ||||
-rw-r--r-- | minecctl/server.h | 4 |
10 files changed, 448 insertions, 73 deletions
diff --git a/minecctl/minecctl-commands.h b/minecctl/minecctl-commands.h index 909c48d..16cf511 100644 --- a/minecctl/minecctl-commands.h +++ b/minecctl/minecctl-commands.h @@ -4,6 +4,7 @@ enum commands { CMD_INVALID = 0, + CMD_INIT, CMD_LIST, CMD_LINT, CMD_INFO, @@ -20,6 +21,10 @@ static struct command_list { const char *name; enum commands cmd; } command_list[] = { { + .name = "init", + .cmd = CMD_INIT, + }, + { .name = "list", .cmd = CMD_LIST, }, diff --git a/minecctl/minecctl.c b/minecctl/minecctl.c index 56e386e..b374f18 100644 --- a/minecctl/minecctl.c +++ b/minecctl/minecctl.c @@ -20,7 +20,6 @@ #include "rcon-commands.h" #include "misc-commands.h" #include "misc.h" -#include "config.h" static void dump_server(struct server *server) { @@ -131,12 +130,15 @@ void dump_config(struct cfg *cfg) info("Configuration"); info("┌────────────"); - info("│ cfgdir : %s", cfg->cfgdir); + info("│ dir_path : %s", cfg->dir_path); + info("│ dir : %p", cfg->dir); 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("│ default set : %s", cfg->default_set ? "yes" : "no"); + info("│ servers loaded: %s", cfg->server_list_loaded ? "yes" : "no"); info("│ commands%s", cfg->commands ? "" : " : none"); if (cfg->commands) { for (char *const *cmd = cfg->commands; *cmd; cmd++) @@ -154,6 +156,7 @@ _noreturn_ static void usage(bool no_error) info("Usage: %s [OPTIONS...] COMMAND\n" "\n" "Valid commands:\n" + " init perform initial setup\n" " list list known servers\n" " lint check validity of server configuration files\n" " info [SERVER] show information about a running SERVER (or all known servers)\n" @@ -176,7 +179,6 @@ _noreturn_ static void usage(bool no_error) " (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" @@ -186,7 +188,7 @@ _noreturn_ static void usage(bool no_error) " the command and vice versa.\n" "\n" "See the minecctl(1) man page for details.\n", - program_invocation_short_name, DEFAULT_CFG_DIR); + program_invocation_short_name); exit(no_error ? EXIT_FAILURE : EXIT_SUCCESS); } @@ -315,6 +317,13 @@ static void parse_command(struct cfg *cfg, char *const *argv) } switch (cmd) { + case CMD_INIT: + if (*argv) { + error("Too many arguments"); + usage(false); + } + cfg->cmd = do_init; + break; case CMD_LIST: if (*argv) { error("Too many arguments"); @@ -402,8 +411,6 @@ static void parse_cmdline(struct cfg *cfg, int argc, char *const *argv) usage(false); } - cfg->cfgdir = DEFAULT_CFG_DIR; - while (true) { int option_index = 0; /* clang-format off */ @@ -437,7 +444,7 @@ static void parse_cmdline(struct cfg *cfg, int argc, char *const *argv) cfg->mc_addrstr = xstrdup(optarg); break; case 'c': - cfg->cfgdir = optarg; + cfg->dir_path = optarg; break; case 'v': debug_mask |= DBG_VERBOSE; @@ -488,8 +495,6 @@ int main(int argc, char *const *argv) parse_cmdline(&cfg, argc, argv); - server_load_all_known(&cfg); - parse_command(&cfg, &argv[optind]); if (!cfg.cmd) { @@ -509,5 +514,7 @@ out: strv_free(cfg.commands); xfree(cfg.rcon_addrstr); xfree(cfg.mc_addrstr); + if (cfg.dir) + closedir(cfg.dir); exit(success ? EXIT_SUCCESS : EXIT_FAILURE); } diff --git a/minecctl/minecctl.h b/minecctl/minecctl.h index 16199a3..46c616a 100644 --- a/minecctl/minecctl.h +++ b/minecctl/minecctl.h @@ -2,14 +2,19 @@ #ifndef foominecctlhfoo #define foominecctlhfoo +#include <sys/types.h> +#include <dirent.h> + struct cfg { - const char *cfgdir; + const char *dir_path; + DIR *dir; char *rcon_password; char *rcon_addrstr; char *mc_addrstr; char **commands; bool force_stop; bool default_set; + bool server_list_loaded; bool (*cmd)(struct cfg *cfg); struct list_head servers; }; diff --git a/minecctl/misc-commands.c b/minecctl/misc-commands.c index e23de37..054279b 100644 --- a/minecctl/misc-commands.c +++ b/minecctl/misc-commands.c @@ -1,4 +1,13 @@ /* SPDX-License-Identifier: GPL-2.0 */ +#include <limits.h> +#include <stdlib.h> +#include <unistd.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <string.h> +#include <errno.h> + #include "shared/utils.h" #include "minecctl.h" #include "server.h" @@ -6,11 +15,239 @@ #include "rcon-commands.h" #include "mc-commands.h" #include "shared/systemd.h" +#include "misc.h" + +static bool create_link(int dfd, const char *source, const char *target) +{ + if (symlinkat(target, dfd, source) < 0) { + error("Unable to create link %s -> %s: %m", source, target); + return false; + } + + return true; +} + +static bool write_cfg_file(int dfd, const char *name, const char *content) +{ + int fd; + ssize_t len = strlen(content); + ssize_t done = 0, r; + bool rv = true; + + fd = openat(dfd, name, O_WRONLY | O_CLOEXEC | O_CREAT | O_EXCL, 0644); + if (fd < 0) { + error("Unable to create file %s: %m", name); + return false; + } + + while (done < len) { + r = write(fd, content + done, len - done); + if (r < 0) { + if (errno == EAGAIN || errno == EINTR) + continue; + error("Unable to write file %s: %m", name); + rv = false; + break; + } + + done += r; + } + + close(fd); + return rv; +} + +static const char txt_server_readme[] = "This is the server readme\n"; +static const char txt_server_eula[] = "This is the server eula\n"; +static const char txt_server_properties[] = "This is the server properties\n"; +static const char txt_server_conf[] = "This is the server conf\n"; +static const char txt_minecproxy_conf[] = "This is the minecproxy conf\n"; +static const char txt_minecproxy_service[] = "This is the minecproxy service\n"; +static const char txt_minecserver_service[] = "This is the minecserver service\n"; + +static bool find_user_service(int xfd, const char *service) +{ + int dfd; + /* FIXME: Make this a macro, make paths #defines */ + char sub_path[STRLEN("systemd/user/") + strlen(service) + 1]; + char etc_path[STRLEN("/etc/systemd/user/") + strlen(service) + 1]; + char usr_path[STRLEN("/usr/lib/systemd/user/") + strlen(service) + 1]; + + sprintf(sub_path, "systemd/user/%s", service); + sprintf(etc_path, "/etc/systemd/user/%s", service); + sprintf(usr_path, "/usr/lib/systemd/user/%s", service); + + /* + * We need to check (in order of precedence): + * ~/.config/systemd/user/ - user-created + * /etc/systemd/user/ - admin-created + * ~/.local/share/systemd/user/ - user-installed packages + * /usr/lib/systemd/user/ - system-installed packages + */ + + if (faccessat(xfd, sub_path, R_OK, 0) == 0) { + info("User service %s already installed in " + "$XDG_CONFIG_HOME/systemd/user/", service); + return true; + } + + if (access(etc_path, R_OK) == 0) { + info("User service %s already installed in " + "/etc/systemd/user/", service); + return true; + } + + dfd = open_xdg_shared_dir(); + if (dfd >= 0) { + if (faccessat(dfd, sub_path, R_OK, 0) == 0) { + info("User service %s already installed in " + "$XDG_DATA_HOME/systemd/user/", service); + close(dfd); + return true; + } + close(dfd); + } + + if (access(usr_path, R_OK) == 0) { + info("User service %s already installed in " + "/usr/lib/systemd/user/", service); + return true; + } + + return false; +} + +static bool create_user_service(int xfd, const char *service, + const char *content) +{ + int sfd, ufd = -1; + bool rv = false; + + if (find_user_service(xfd, service)) + return true; + + sfd = open_subdir(xfd, "systemd", true); + if (sfd < 0) + goto out; + + ufd = open_subdir(sfd, "user", true); + if (ufd < 0) + goto out; + + if (!write_cfg_file(ufd, service, content)) + goto out; + + info("Created user service file $XDG_CONFIG_HOME/systemd/user/%s", + service); + rv = true; + +out: + if (ufd >= 0) + close(ufd); + if (sfd >= 0) + close(sfd); + return rv; +} + +static bool create_config_tree(int xfd) +{ + int mfd = -1, sfd = -1, efd = -1, cfd = -1; + bool rv = false; + + mfd = open_subdir(xfd, "minecproxy", true); + if (mfd < 0) + goto out; + + sfd = open_subdir(mfd, "servers", true); + if (sfd < 0) + goto out; + + if (!write_cfg_file(sfd, "README.TXT", txt_server_readme)) + goto out; + + efd = open_subdir(sfd, "example-server", true); + if (efd < 0) + goto out; + + if (!write_cfg_file(efd, "eula.txt", txt_server_eula)) + goto out; + + if (!write_cfg_file(efd, "server.properties", txt_server_properties)) + goto out; + + if (!create_link(efd, "server.jar", "../server.jar")) + goto out; + + cfd = open_subdir(mfd, "config", true); + if (cfd < 0) + goto out; + + if (!write_cfg_file(cfd, "example-server.mcserver", txt_server_conf)) + goto out; + + if (!write_cfg_file(cfd, "minecproxy.conf", txt_minecproxy_conf)) + goto out; + + info("Created configuration in $XDG_CONFIG_HOME/minecproxy"); + rv = true; + +out: + if (mfd >= 0) + close(mfd); + if (sfd >= 0) + close(sfd); + if (efd >= 0) + close(efd); + if (cfd >= 0) + close(cfd); + return rv; +} + +bool do_init(struct cfg *cfg) +{ + int xfd; + bool rv = false; + + if (cfg->dir_path) { + xfd = open(cfg->dir_path, O_PATH | O_CLOEXEC | O_DIRECTORY); + if (xfd < 0) + error("Failed to open dir %s: %m", cfg->dir_path); + } else + xfd = open_xdg_cfg_dir(true); + + if (xfd < 0) + goto out; + + if (!create_config_tree(xfd)) + goto out; + + if (cfg->dir_path) { + info("Option -c used, not creating systemd services"); + rv = true; + goto out; + } + + if (!create_user_service(xfd, "minecserver@.service", + txt_minecserver_service)) + goto out; + + if (!create_user_service(xfd, "minecproxy.service", + txt_minecproxy_service)) + goto out; + + rv = true; +out: + if (xfd >= 0) + close(xfd); + return rv; +} bool do_list(struct cfg *cfg) { struct server *server; + server_load_all_known(cfg); + /* server->scfg.filename check excludes servers created from cmdline */ list_for_each_entry(server, &cfg->servers, list) if (server->scfg.filename) @@ -112,10 +349,8 @@ bool do_pcount(struct cfg *cfg) const char *error; server = server_get_default(cfg); - if (!server) { - error("failed to get default server"); + if (!server) return false; - } if (do_rcon_pcount(cfg, server, &online, &max, &error)) info("Rcon says %u/%u", online, max); diff --git a/minecctl/misc-commands.h b/minecctl/misc-commands.h index ebb6ccd..95d0ba0 100644 --- a/minecctl/misc-commands.h +++ b/minecctl/misc-commands.h @@ -2,6 +2,8 @@ #ifndef foomisccommandshfoo #define foomisccommandshfoo +bool do_init(struct cfg *cfg); + bool do_list(struct cfg *cfg); bool do_lint(struct cfg *cfg); diff --git a/minecctl/misc.c b/minecctl/misc.c index 525b09a..26cbc86 100644 --- a/minecctl/misc.c +++ b/minecctl/misc.c @@ -5,11 +5,84 @@ #include <stdarg.h> #include <string.h> #include <termios.h> +#include <sys/types.h> +#include <sys/stat.h> +#include <fcntl.h> +#include <pwd.h> +#include <errno.h> #include "shared/utils.h" #include "misc.h" #include "minecctl.h" +static const char *get_homedir() +{ + const char *e; + struct passwd *passwd; + uid_t uid; + + e = getenv("HOME"); + if (e && e[0] == '/') + return e; + + uid = getuid(); + if (uid == 0) + return "/root"; + + passwd = getpwuid(uid); + if (passwd && passwd->pw_dir[0] == '/') + return passwd->pw_dir; + + return NULL; +} + +int open_subdir(int dfd, const char *subdir, bool nofail) +{ + int sfd; + + if (nofail && mkdirat(dfd, subdir, 0777) < 0 && errno != EEXIST) { + error("Unable to create subdirectory %s: %m", subdir); + return -1; + } + + sfd = openat(dfd, subdir, O_PATH | O_CLOEXEC | O_DIRECTORY); + if (sfd < 0 && nofail) + error("Unable to open subdirectory %s: %m", subdir); + + return sfd; +} + +int open_xdg_dir(const char *envname, const char *altpath, bool nofail) +{ + const char *e; + const char *h; + int dfd, hfd; + + e = getenv(envname); + if (e && e[0] == '/') { + dfd = open(e, O_PATH | O_CLOEXEC | O_DIRECTORY); + if (dfd < 0) + error("Unable to open $%s(%s): %m", envname, e); + return dfd; + } + + h = get_homedir(); + if (!h) { + error("Unable to determine home directory"); + return -1; + } + + hfd = open(h, O_PATH | O_CLOEXEC | O_DIRECTORY); + if (hfd < 0) { + error("Unable to open $HOME(%s): %m", h); + return -1; + } + + dfd = open_subdir(hfd, altpath, nofail); + close(hfd); + return dfd; +} + /* FIXME: Can be shared */ void set_use_colors() { diff --git a/minecctl/misc.h b/minecctl/misc.h index 3182ef8..d1c6816 100644 --- a/minecctl/misc.h +++ b/minecctl/misc.h @@ -2,6 +2,20 @@ #ifndef foomischfoo #define foomischfoo +int open_subdir(int dfd, const char *subdir, bool nofail); + +int open_xdg_dir(const char *envname, const char *altpath, bool nofail); + +static inline int open_xdg_shared_dir() +{ + return open_xdg_dir("XDG_DATA_HOME", ".local/share", false); +} + +static inline int open_xdg_cfg_dir(bool nofail) +{ + return open_xdg_dir("XDG_CONFIG_HOME", ".config", nofail); +} + void set_use_colors(); char **strv_copy(char *const *strv); diff --git a/minecctl/rcon-commands.c b/minecctl/rcon-commands.c index bab0004..44bb286 100644 --- a/minecctl/rcon-commands.c +++ b/minecctl/rcon-commands.c @@ -112,7 +112,7 @@ static int rcon_login(struct cfg *cfg, struct server *server) fd = connect_any(&server->scfg.rcons, &saddr, &error); if (fd < 0) { - error("%s: unable to connect - %s", server->name, error); + verbose("%s: unable to connect - %s", server->name, error); goto error; } else verbose("%s: connected to %s", server->name, saddr->addrstr); @@ -344,24 +344,28 @@ static bool stop_one_server(struct cfg *cfg, struct server *server) bool rv; fd = rcon_login(cfg, server); - if (fd < 0) + if (fd < 0) { + info("• %s: %sfail%s - unable to login", + server->name, ansi_red, ansi_normal); return false; + } - if (cfg->force_stop) { + if (!cfg->force_stop) { unsigned current; if (!get_player_count(fd, ¤t, NULL)) { - error("%s: unable to get player count, not stopping", - server->name); + info("• %s: %sfail%s - unable to get player count", + server->name, ansi_red, ansi_normal); return false; } else if (current > 0) { - error("%s: has active players (use -f to force)", - server->name); + info("• %s: %sfail%s - has active players " + "(use -f to force)", + server->name, ansi_red, ansi_normal); return false; } } - info("%s: sending stop command", server->name); + info("• %s: sending stop command", server->name); rv = send_cmd(fd, "stop"); close(fd); @@ -379,19 +383,12 @@ bool do_stop(struct cfg *cfg) bool do_stop_all(struct cfg *cfg) { struct server *server; - unsigned lineno; - const char *error; bool rv = true; + server_read_all_configs(cfg, false); + list_for_each_entry(server, &cfg->servers, list) { - if (!server_read_config(cfg, server, &lineno, &error)) { - info("• %s: %sfail%s - invalid configuration file " - "(line %u: %s)", - server->name, ansi_red, ansi_normal, - lineno, error); - rv = false; - } - /* FIXME: error checks */ + /* FIXME: print more info, error checks */ stop_one_server(cfg, server); } diff --git a/minecctl/server.c b/minecctl/server.c index 599be1f..91e7842 100644 --- a/minecctl/server.c +++ b/minecctl/server.c @@ -8,16 +8,81 @@ #include "minecctl.h" #include "server.h" #include "misc.h" +#include "config.h" #define INVALID(msg) do { *error = (msg); return false; } while(0) +static DIR *open_cfg_dir(const char *cmdline_path) +{ + int dfd, mfd; + DIR *dir; + + if (cmdline_path) { + dir = opendir(cmdline_path); + if (!dir) + error("opendir(%s): %m", cmdline_path); + return dir; + } + + /* First, attempt per-user config dir... */ + dfd = open_xdg_cfg_dir(false); + if (dfd >= 0) { + mfd = openat(dfd, "minecproxy/config", + O_CLOEXEC | O_DIRECTORY | O_RDONLY); + close(dfd); + if (mfd >= 0) { + dir = fdopendir(mfd); + if (!dir) + error("fdopendir: %m"); + return dir; + } + } + + /* ...and fallback on the system dir */ + dir = opendir(DEFAULT_CFG_DIR); + if (!dir) + error("Failed to open configuration directory: %m"); + return dir; +} + +void server_load_all_known(struct cfg *cfg) +{ + struct dirent *dent; + + if (cfg->server_list_loaded) + return; + + cfg->server_list_loaded = true; + cfg->dir = open_cfg_dir(cfg->dir_path); + if (!cfg->dir) { + error("Failed to open config directory"); + return; + } + + while ((dent = readdir(cfg->dir))) { + struct server *server; + char *suffix; + + if (!config_valid_server_filename(dent, NULL)) + continue; + + server = server_new(dent->d_name); + + suffix = strrchr(dent->d_name, '.'); + assert_die(suffix, "Error parsing filename"); + *suffix = '\0'; + server->name = xstrdup(dent->d_name); + + list_add(&server->list, &cfg->servers); + } +} + bool server_read_config(struct cfg *cfg, struct server *server, unsigned *lineno, const char **error) { char buf[4096]; size_t off = 0; ssize_t r; - int dfd; int fd; if (!error) @@ -35,19 +100,16 @@ bool server_read_config(struct cfg *cfg, struct server *server, if (server->file_read) return true; + if (!cfg->dir) + INVALID("Configuration directory not opened"); + *lineno = 0; server->file_read = true; - dfd = open(cfg->cfgdir, O_DIRECTORY | O_PATH | O_CLOEXEC); - if (dfd < 0) - INVALID("failed to open configuration directory"); - - fd = openat(dfd, server->scfg.filename, O_RDONLY | O_CLOEXEC); + fd = openat(dirfd(cfg->dir), server->scfg.filename, O_RDONLY | O_CLOEXEC); if (fd < 0) INVALID("failed to open configuration file"); - close(dfd); - while (true) { r = read(fd, buf + off, sizeof(buf) - off - 1); if (r < 0) @@ -85,6 +147,8 @@ bool server_read_all_configs(struct cfg *cfg, bool print_results) const char *error; bool rv = true; + server_load_all_known(cfg); + /* server->scfg.filename check excludes servers created from cmdline */ list_for_each_entry(server, &cfg->servers, list) { if (!server->scfg.filename) @@ -111,6 +175,8 @@ bool server_read_all_configs(struct cfg *cfg, bool print_results) ansi_normal); } } + + return rv; } struct server *server_get_default(struct cfg *cfg) @@ -124,7 +190,7 @@ struct server *server_get_default(struct cfg *cfg) die("No servers defined"); if (!server_read_config(cfg, server, &lineno, &error)) { - error("server_read_config error: %s", error); + error("%s: server_read_config error - %s", server->name, error); return NULL; } @@ -137,6 +203,8 @@ bool server_set_default(struct cfg *cfg, const char *name) assert_die(cfg, "invalid arguments"); + server_load_all_known(cfg); + list_for_each_entry(server, &cfg->servers, list) { if (streq(name, server->name)) { list_rotate_to_front(&server->list, &cfg->servers); @@ -148,37 +216,6 @@ bool server_set_default(struct cfg *cfg, const char *name) return false; } -void server_load_all_known(struct cfg *cfg) -{ - struct dirent *dent; - DIR *dir; - - dir = opendir(cfg->cfgdir); - if (!dir) { - info("Can't open config directory %s: %m", cfg->cfgdir); - return; - } - - while ((dent = readdir(dir))) { - struct server *server; - char *suffix; - - if (!config_valid_server_filename(dent, NULL)) - continue; - - server = server_new(dent->d_name); - - suffix = strrchr(dent->d_name, '.'); - assert_die(suffix, "Error parsing filename"); - *suffix = '\0'; - server->name = xstrdup(dent->d_name); - - list_add(&server->list, &cfg->servers); - } - - closedir(dir); -} - void server_free_all(struct cfg *cfg) { struct server *server, *tmp; diff --git a/minecctl/server.h b/minecctl/server.h index 556187d..4520d34 100644 --- a/minecctl/server.h +++ b/minecctl/server.h @@ -13,6 +13,8 @@ struct server { struct list_head list; }; +void server_load_all_known(struct cfg *cfg); + bool server_read_config(struct cfg *cfg, struct server *server, unsigned *lineno, const char **error); @@ -22,8 +24,6 @@ struct server *server_get_default(struct cfg *cfg); bool server_set_default(struct cfg *cfg, const char *name); -void server_load_all_known(struct cfg *cfg); - void server_free_all(struct cfg *cfg); void server_free(struct server *server); |