From fae819296598100e41646e0bebc7d0bce45614f7 Mon Sep 17 00:00:00 2001 From: David Härdeman Date: Wed, 10 Jun 2020 11:22:29 +0200 Subject: Add initial systemd integration --- cfgdir.c | 17 +++- main.c | 2 + main.h | 2 + meson.build | 31 +++--- server.c | 134 +++++++++++++++++++++---- server.h | 17 +++- systemd.c | 321 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ systemd.h | 22 +++++ 8 files changed, 508 insertions(+), 38 deletions(-) create mode 100644 systemd.c create mode 100644 systemd.h diff --git a/cfgdir.c b/cfgdir.c index f961011..9fa0c78 100644 --- a/cfgdir.c +++ b/cfgdir.c @@ -32,9 +32,7 @@ enum scfg_keys { SCFG_KEY_START_EXEC, SCFG_KEY_RCON, SCFG_KEY_RCON_PASSWORD, - /* SCFG_KEY_SYSTEMD_SERVICE, - */ }; struct cfg_key_value_map scfg_key_map[] = { @@ -86,6 +84,10 @@ struct cfg_key_value_map scfg_key_map[] = { .key_name = "rcon_password", .key_value = SCFG_KEY_RCON_PASSWORD, .value_type = CFG_VAL_TYPE_STRING, + }, { + .key_name = "systemd_service", + .key_value = SCFG_KEY_SYSTEMD_SERVICE, + .value_type = CFG_VAL_TYPE_STRING, }, { .key_name = NULL, .key_value = SCFG_KEY_INVALID, @@ -166,6 +168,9 @@ scfg_parse(struct cfg *cfg, struct server *scfg) } else if (!strcmp(value.str, "rcon")) { if (server_set_stop_method(cfg, scfg, SERVER_STOP_METHOD_RCON)) break; + } else if (!strcmp(value.str, "systemd")) { + if (server_set_stop_method(cfg, scfg, SERVER_STOP_METHOD_SYSTEMD)) + break; } return; @@ -173,6 +178,9 @@ scfg_parse(struct cfg *cfg, struct server *scfg) if (!strcmp(value.str, "exec")) { if (server_set_start_method(cfg, scfg, SERVER_START_METHOD_EXEC)) break; + } else if (!strcmp(value.str, "systemd")) { + if (server_set_start_method(cfg, scfg, SERVER_START_METHOD_SYSTEMD)) + break; } return; @@ -201,6 +209,11 @@ scfg_parse(struct cfg *cfg, struct server *scfg) return; break; + case SCFG_KEY_SYSTEMD_SERVICE: + if (!server_set_systemd_service(cfg, scfg, value.str)) + return; + break; + case SCFG_KEY_INVALID: default: break; diff --git a/main.c b/main.c index da962b4..eace2d0 100644 --- a/main.c +++ b/main.c @@ -19,6 +19,7 @@ #include "server.h" #include "cfgdir.h" #include "announce.h" +#include "systemd.h" int debuglvl = 0; @@ -62,6 +63,7 @@ cfg_free(struct uring_task *task) struct cfg *cfg = container_of(task, struct cfg, task); fprintf(stderr, "%s: called\n", __func__); + systemd_delete(cfg); free(cfg); fprintf(stderr, "All resources free, exiting\n"); exiting = true; diff --git a/main.h b/main.h index e6ced6f..086cd92 100644 --- a/main.h +++ b/main.h @@ -41,6 +41,8 @@ struct cfg { struct inotify_ev *iev; struct signalfd_ev *sev; struct announce *aev; + struct sd_bus *sd_bus; + bool sd_bus_failed; struct uring_task task; struct list_head servers; }; diff --git a/meson.build b/meson.build index 7a89f88..1a6986d 100644 --- a/meson.build +++ b/meson.build @@ -1,18 +1,25 @@ project('mcproxy', 'c', default_options : ['c_std=gnu18']) -uring = dependency('liburing') + +liburing = dependency('liburing') +libsystemd = dependency('libsystemd') + +mcproxy_sources = [ + 'main.c', + 'uring.c', + 'server.c', + 'proxy.c', + 'announce.c', + 'cfgdir.c', + 'config.c', + 'rcon.c', + 'idle.c', + 'systemd.c', + 'utils.c'] executable('ctest', 'ctest.c') executable('stest', 'stest.c') executable('mcproxy', - ['main.c', - 'uring.c', - 'server.c', - 'proxy.c', - 'announce.c', - 'cfgdir.c', - 'config.c', - 'rcon.c', - 'idle.c', - 'utils.c'], - dependencies: uring) + mcproxy_sources, + dependencies : [liburing, + libsystemd]) diff --git a/server.c b/server.c index 7ec0804..ee93a24 100644 --- a/server.c +++ b/server.c @@ -18,6 +18,7 @@ #include "utils.h" #include "idle.h" #include "rcon.h" +#include "systemd.h" struct server_local { struct sockaddr_in46 addr; @@ -67,6 +68,8 @@ server_free(struct uring_task *task) free(scfg->pretty_name); free(scfg->start_exec); free(scfg->stop_exec); + free(scfg->systemd_service); + free(scfg->systemd_obj); free(scfg->name); free(scfg); } @@ -323,6 +326,9 @@ server_start(struct cfg *cfg, struct server *scfg) case SERVER_START_METHOD_EXEC: return server_exec(cfg, scfg, scfg->start_exec); + case SERVER_START_METHOD_SYSTEMD: + return systemd_service_start(cfg, scfg); + case SERVER_START_METHOD_UNDEFINED: default: break; @@ -342,6 +348,9 @@ server_stop(struct cfg *cfg, struct server *scfg) case SERVER_STOP_METHOD_EXEC: return server_exec(cfg, scfg, scfg->stop_exec); + case SERVER_STOP_METHOD_SYSTEMD: + return systemd_service_stop(cfg, scfg); + case SERVER_STOP_METHOD_RCON: rcon_init(cfg, scfg); return true; @@ -360,65 +369,112 @@ server_commit(struct cfg *cfg, struct server *scfg) struct server_local *local; uint16_t port; - if (!scfg || !scfg->name) + if (!scfg || !scfg->name) { + fprintf(stderr, "%s: called with invalid parameters\n", __func__); return false; + } - if (!list_empty(&scfg->proxys)) + if (!list_empty(&scfg->proxys)) { + fprintf(stderr, "%s(%s): proxys not empty?\n", __func__, scfg->name); return false; + } /* FIXME: running? */ if (scfg->stop_method == SERVER_STOP_METHOD_RCON && - (list_empty(&scfg->rcons) || !scfg->rcon_password)) + (list_empty(&scfg->rcons) || !scfg->rcon_password)) { + fprintf(stderr, "%s(%s): rcon stop method but missing rcon password\n", __func__, scfg->name); + return false; + } + + if ((scfg->start_method == SERVER_START_METHOD_SYSTEMD || + scfg->stop_method == SERVER_STOP_METHOD_SYSTEMD) && + !scfg->systemd_service) { + fprintf(stderr, "%s(%s): systemd start/stop method but missing systemd service\n", __func__, scfg->name); + return false; + } + + if (scfg->systemd_service && !scfg->systemd_obj) { + scfg->systemd_obj = systemd_service_object_path(cfg, + scfg->systemd_service); + if (!scfg->systemd_obj) { + fprintf(stderr, "%s(%s): failed to create systemd object path (%s)\n", __func__, scfg->name, scfg->systemd_service); + return false; + } + } + + if (scfg->idle_timeout > 0 && + scfg->stop_method == SERVER_STOP_METHOD_UNDEFINED) { + fprintf(stderr, "%s(%s): idle_timeout set but missing stop method\n", __func__, scfg->name); return false; + } switch (scfg->type) { case SERVER_TYPE_ANNOUNCE: - if (scfg->announce_port < 1) + if (scfg->announce_port < 1) { + fprintf(stderr, "%s(%s): missing announce port\n", __func__, scfg->name); return false; - if (scfg->idle_timeout > 0 && - scfg->stop_method == SERVER_STOP_METHOD_UNDEFINED) - return false; - if (scfg->start_method != SERVER_START_METHOD_UNDEFINED) + } + + if (scfg->start_method != SERVER_START_METHOD_UNDEFINED) { + fprintf(stderr, "%s(%s): can't set start_method for announce server\n", __func__, scfg->name); return false; - if (!list_empty(&scfg->locals)) + } + + if (!list_empty(&scfg->locals)) { + fprintf(stderr, "%s(%s): can't set local addresses for announce server\n", __func__, scfg->name); return false; - if (!list_empty(&scfg->remotes)) + } + + if (!list_empty(&scfg->remotes)) { + fprintf(stderr, "%s(%s): can't set remote addresses for announce server\n", __func__, scfg->name); return false; + } + break; case SERVER_TYPE_PROXY: - if (scfg->announce_port >= 1) + if (scfg->announce_port >= 1) { + fprintf(stderr, "%s(%s): can't set announce port for proxy server\n", __func__, scfg->name); return false; - if (scfg->idle_timeout > 0 && - scfg->stop_method == SERVER_STOP_METHOD_UNDEFINED) - return false; - if (list_empty(&scfg->locals)) + } + + if (list_empty(&scfg->locals)) { + fprintf(stderr, "%s(%s): missing local addresses for proxy server\n", __func__, scfg->name); return false; - if (list_empty(&scfg->remotes)) + } + + if (list_empty(&scfg->remotes)) { + fprintf(stderr, "%s(%s): missing remote addresses for proxy server\n", __func__, scfg->name); return false; + } list_for_each_entry(local, &scfg->locals, list) { port = sockaddr_port(&local->addr); - if (port == 0) + if (port == 0) { + fprintf(stderr, "%s(%s): invalid local port\n", __func__, scfg->name); return false; + } if (scfg->announce_port < 1) scfg->announce_port = port; if (scfg->announce_port != port) { - fprintf(stderr, "Multiple announce ports!?\n"); + fprintf(stderr, "%s(%s): multiple local ports\n", __func__, scfg->name); return false; } } - if (scfg->announce_port < 1) + if (scfg->announce_port < 1) { + fprintf(stderr, "%s(%s): can't determine which port to announce\n", __func__, scfg->name); return false; + } break; default: + fprintf(stderr, "%s(%s): can't determine server type\n", __func__, scfg->name); return false; } @@ -426,12 +482,16 @@ server_commit(struct cfg *cfg, struct server *scfg) char *suffix; suffix = strrchr(scfg->name, '.'); - if (!suffix || suffix == scfg->name) + if (!suffix || suffix == scfg->name) { + fprintf(stderr, "%s(%s): invalid server name\n", __func__, scfg->name); return false; + } scfg->pretty_name = strndup(scfg->name, suffix - scfg->name); - if (!scfg->pretty_name) + if (!scfg->pretty_name) { + fprintf(stderr, "%s(%s): failed to create display name\n", __func__, scfg->name); return false; + } } /* FIXME: config, dont reread config if server running, make sure fd is available before this is called */ @@ -443,6 +503,11 @@ server_commit(struct cfg *cfg, struct server *scfg) idle_init(cfg, scfg); + if (scfg->systemd_service) { + fprintf(stderr, "Checking if systemd service is running\n"); + systemd_service_running(cfg, scfg); + } + return true; } @@ -496,6 +561,33 @@ server_set_rcon_password(struct cfg *cfg, struct server *scfg, return set_property(cfg, scfg, &scfg->rcon_password, password); } +bool +server_set_systemd_service(struct cfg *cfg, struct server *scfg, + const char *service) +{ + const char *suffix; + char *tmp; + + if (!cfg || !scfg || empty_str(service) || scfg->systemd_service) + return false; + + suffix = strrchr(service, '.'); + if (!suffix || strcmp(suffix, ".service")) { + tmp = malloc(strlen(service) + strlen(".service") + 1); + if (tmp) + sprintf(tmp, "%s.service", service); + } else + tmp = strdup(service); + + if (!tmp) { + perror("malloc/strdup"); + return false; + } + + scfg->systemd_service = tmp; + return true; +} + bool server_set_stop_method(struct cfg *cfg, struct server *scfg, enum server_stop_method stop_method) diff --git a/server.h b/server.h index 495f7c4..8e0b15f 100644 --- a/server.h +++ b/server.h @@ -10,11 +10,13 @@ enum server_type { enum server_stop_method { SERVER_STOP_METHOD_UNDEFINED, SERVER_STOP_METHOD_RCON, + SERVER_STOP_METHOD_SYSTEMD, SERVER_STOP_METHOD_EXEC }; enum server_start_method { SERVER_START_METHOD_UNDEFINED, + SERVER_START_METHOD_SYSTEMD, SERVER_START_METHOD_EXEC }; @@ -31,12 +33,14 @@ struct server { enum server_stop_method stop_method; enum server_start_method start_method; + + /* For calling external start/stop executables */ char *stop_exec; char *start_exec; - /* For config files */ - char buf[4096]; - size_t len; + /* For systemd services */ + char *systemd_service; + char *systemd_obj; /* For rcon connections */ struct rcon *rcon; @@ -52,6 +56,10 @@ struct server { unsigned idle_timeout; unsigned idle_count; + /* For config files */ + char buf[4096]; + size_t len; + struct uring_task exec_task; struct uring_task task; struct list_head list; @@ -81,6 +89,9 @@ bool server_add_rcon(struct cfg *cfg, struct server *scfg, bool server_set_rcon_password(struct cfg *cfg, struct server *scfg, const char *password); +bool server_set_systemd_service(struct cfg *cfg, struct server *scfg, + const char *service); + bool server_set_stop_method(struct cfg *cfg, struct server *scfg, enum server_stop_method stop_method); diff --git a/systemd.c b/systemd.c new file mode 100644 index 0000000..f556a55 --- /dev/null +++ b/systemd.c @@ -0,0 +1,321 @@ +#include +#include +#include + +#include "main.h" +#include "server.h" +#include "systemd.h" + +#define SYSTEMD_DBUS_SERVICE "org.freedesktop.systemd1" +#define SYSTEMD_DBUS_INTERFACE "org.freedesktop.systemd1.Unit" +#define SYSTEMD_DBUS_PATH_PREFIX "/org/freedesktop/systemd1/unit/" + +static inline char +tohex(uint8_t val) +{ + static const char hex[] = "0123456789abcdef"; + + return hex[val & 0x0f]; +} + +/* + * Creates an escaped D-Bus object path for a given systemd service + * + * Escaping rules are documented here: + * https://dbus.freedesktop.org/doc/dbus-specification.html + * + * Essentially, everyting but a-z, A-Z, 0-9 is replaced by _xx where xx is + * the hexadecimal value of the character. + * + * Example: minecraft@world1.service -> minecraft_40world1_2eservice + */ +char * +systemd_service_object_path(struct cfg *cfg, const char *service) +{ + char *r; + char *d; + const char *s; + + if (empty_str(service)) { + fprintf(stderr, "%s: called with NULL arg\n", __func__); + return NULL; + } + + r = malloc(strlen(SYSTEMD_DBUS_PATH_PREFIX) + strlen(service) * 3 + 1); + if (!r) + return NULL; + + memcpy(r, SYSTEMD_DBUS_PATH_PREFIX, strlen(SYSTEMD_DBUS_PATH_PREFIX)); + d = r + strlen(SYSTEMD_DBUS_PATH_PREFIX); + + for (s = service; *s; s++) { + if ((*s >= 'a' && *s <= 'z') || + (*s >= 'A' && *s <= 'Z') || + (*s >= '0' && *s <= '9')) { + *(d++) = *s; + continue; + } + + *(d++) = '_'; + *(d++) = tohex(*s >> 4); + *(d++) = tohex(*s); + } + + *d = '\0'; + return r; +} + +void +systemd_delete(struct cfg *cfg) +{ + if (!cfg->sd_bus) + return; + + sd_bus_unref(cfg->sd_bus); + cfg->sd_bus = NULL; +} + +static sd_bus * +get_bus(struct cfg *cfg) +{ + int r; + + if (cfg->sd_bus_failed) + return NULL; + + if (!cfg->sd_bus) { + r = sd_bus_open_user(&cfg->sd_bus); + if (r < 0) { + fprintf(stderr, "Failed to connect to user system bus: %s\n", strerror(-r)); + cfg->sd_bus_failed = true; + return NULL; + } + } + + return cfg->sd_bus; +} + +/* + * Check if a systemd service is running. + * + * This is equivalent to (assuming service minecraft@world1): + * gdbus call --session + * --dest org.freedesktop.systemd1 + * --object-path /org/freedesktop/systemd1/unit/minecraft_40world1_2eservice + * --method org.freedesktop.DBus.Properties.Get + * "org.freedesktop.systemd1.Unit" + * "ActiveState" + */ +bool +systemd_service_running(struct cfg *cfg, struct server *server) +{ + sd_bus *bus = get_bus(cfg); + sd_bus_error error = SD_BUS_ERROR_NULL; + char *status = NULL; + int r; + bool running = false; + + if (!bus || !server->systemd_service || !server->systemd_obj) + return false; + + r = sd_bus_get_property_string(bus, + SYSTEMD_DBUS_SERVICE, + server->systemd_obj, + SYSTEMD_DBUS_INTERFACE, + "ActiveState", + &error, + &status); + if (r < 0) { + fprintf(stderr, "%s: failed to get status for service %s (%s): %s\n", + __func__, server->systemd_service, server->systemd_obj, error.message); + goto out; + } + + if (!strcmp(status, "active")) { + running = true; + fprintf(stderr, "Systemd service %s (%s) is active\n", + server->systemd_service, server->systemd_obj); + } else + fprintf(stderr, "Systemd service %s (%s) is not active\n", + server->systemd_service, server->systemd_obj); + +out: + free(status); + sd_bus_error_free(&error); + return running; +} + +static bool +systemd_service_action(struct cfg *cfg, struct server *server, const char *action) +{ + sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus_message *m = NULL; + sd_bus *bus = get_bus(cfg); + const char *path; + bool performed = false; + int r; + + if (!bus || !server->systemd_service || !server->systemd_obj || !action) + return false; + + r = sd_bus_call_method(bus, + SYSTEMD_DBUS_SERVICE, + server->systemd_obj, + SYSTEMD_DBUS_INTERFACE, + action, + &error, + &m, + "s", + "fail"); + if (r < 0) { + fprintf(stderr, "Failed to perform action %s on systemd service %s: %s\n", + action, server->systemd_service, error.message); + goto out; + } + + r = sd_bus_message_read(m, "o", &path); + if (r < 0) { + fprintf(stderr, "Failed to parse response message: %s\n", strerror(-r)); + goto out; + } + + fprintf(stderr, "Queued %s on service %s as %s\n", + action, server->systemd_service, path); + performed = true; + +out: + sd_bus_error_free(&error); + sd_bus_message_unref(m); + return performed; +} + +/* + * Stop systemd service. + * + * This is equivalent to (assuming service minecraft@world1): + * gdbus call --session + * --dest org.freedesktop.systemd1 + * --object-path /org/freedesktop/systemd1/unit/minecraft_40world1_2eservice + * --method org.freedesktop.systemd1.Unit.Stop "fail" + */ +bool +systemd_service_stop(struct cfg *cfg, struct server *server) +{ + return systemd_service_action(cfg, server, "Stop"); +} + +/* + * Start systemd service. + * + * This is equivalent to (assuming service minecraft@world1): + * gdbus call --session + * --dest org.freedesktop.systemd1 + * --object-path /org/freedesktop/systemd1/unit/minecraft_40world1_2eservice + * --method org.freedesktop.systemd1.Unit.Start "fail" + */ +bool +systemd_service_start(struct cfg *cfg, struct server *server) +{ + return systemd_service_action(cfg, server, "Start"); +} + + +#if 0 + + + + +int main(int argc, char *argv[]) { + sd_bus_error error = SD_BUS_ERROR_NULL; + sd_bus_message *m = NULL; + sd_bus *bus = NULL; + const char *path; + int r; + + /* Connect to the system bus */ + r = sd_bus_open_user(&bus); + if (r < 0) { + fprintf(stderr, "Failed to connect to system bus: %s\n", strerror(-r)); + goto finish; + } + + /* Issue the method call and store the respons message in m */ + r = sd_bus_call_method(bus, + "org.freedesktop.systemd1", /* service to contact */ + "/org/freedesktop/systemd1", /* object path */ + "org.freedesktop.systemd1.Manager", /* interface name */ + "StartUnit", /* method name */ + &error, /* object to return error in */ + &m, /* return message on success */ + "ss", /* input signature */ + "apa.service", /* first argument */ + "fail"); /* second argument */ + if (r < 0) { + fprintf(stderr, "Failed to issue method call: %s\n", error.message); + goto finish; + } + + /* Parse the response message */ + r = sd_bus_message_read(m, "o", &path); + if (r < 0) { + fprintf(stderr, "Failed to parse response message: %s\n", strerror(-r)); + goto finish; + } + printf("Queued service job as %s.\n", path); + + sd_bus_error_free(&error); + sd_bus_message_unref(m); + error = SD_BUS_ERROR_NULL; + m = NULL; + + /* + gdbus call --session + --dest org.freedesktop.systemd1 + --object-path /org/freedesktop/systemd1/unit/myapa_40local2_2eservice + --method org.freedesktop.systemd1.Unit.Start "fail" + + gdbus introspect --session + --dest org.freedesktop.systemd1 + --object-path /org/freedesktop/systemd1/unit/myapa_40local_2eservice + + gdbus call --session + --dest org.freedesktop.systemd1 + --object-path /org/freedesktop/systemd1/unit/myapa_40basker_2eservice + --method org.freedesktop.DBus.Properties.Get + "org.freedesktop.systemd1.Unit" + "ActiveState" + */ + char *res; + r = sd_bus_get_property_string(bus, + "org.freedesktop.systemd1", + "/org/freedesktop/systemd1/unit/apa_2eservice", + "org.freedesktop.systemd1.Unit", + "ActiveState", + &error, + &res); + if (r < 0) { + fprintf(stderr, "Failed to issue prop call: %s\n", error.message); + goto finish; + } + + fprintf(stderr, "RESULT: %s\n", res); +/* + int sd_bus_get_property_string(sd_bus *bus, + const char *destination, + const char *path, + const char *interface, + const char *member, + sd_bus_error *ret_error, + char **ret); + */ +/* +finish: + sd_bus_error_free(&error); + sd_bus_message_unref(m); + sd_bus_unref(bus); + */ + +finish: + return r < 0 ? EXIT_FAILURE : EXIT_SUCCESS; +} +#endif diff --git a/systemd.h b/systemd.h new file mode 100644 index 0000000..ccf63dd --- /dev/null +++ b/systemd.h @@ -0,0 +1,22 @@ +#ifndef foosystemdhfoo +#define foosystemdhfoo + +char *systemd_service_object_path(struct cfg *cfg, const char *service); + +void systemd_delete(struct cfg *cfg); + +bool systemd_service_running(struct cfg *cfg, struct server *server); + +bool systemd_service_stop(struct cfg *cfg, struct server *server); + +bool systemd_service_start(struct cfg *cfg, struct server *server); + +/* +void idle_refdump(struct idle *idle); + +void idle_delete(struct cfg *cfg, struct server *server); + +void idle_init(struct cfg *cfg, struct server *server); +*/ + +#endif -- cgit v1.2.3