diff --git a/CMakeLists.txt b/CMakeLists.txt index 52b7277c..a98f6e5c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -123,6 +123,7 @@ set(SOURCES src/Server.cc src/ServerShell.cc src/ServerState.cc + src/SignalWatcher.cc src/StaticGameData.cc src/TeamIndex.cc src/Text.cc diff --git a/README.md b/README.md index ee06f3ee..bf3e1448 100644 --- a/README.md +++ b/README.md @@ -148,6 +148,10 @@ There are currently no precompiled releases for Linux. To run newserv on Linux, After building newserv, edit system/config.example.json as needed **and rename it to system/config.json** (note that this step is not necessary for the precompiled releases!), set up [client patch directories](#client-patch-directories) if you're planning to play Blue Burst, then run `./newserv` in newserv's directory. +The server has an interactive shell which can be used to make changes, such as managing user accounts, updating the server's configuration, managing Episode 3 tournaments, and more. Type `help` and press Enter to see all the commands. + +On Linux and macOS, the server also responds to SIGUSR1 and SIGUSR2. SIGUSR1 does the equivalent of the shell's `reload config` command, which reloads config.json but not any dependent files (so quests, Episode 3 maps, etc. will not be reloaded). SIGUSR2 does the equivalent of the shell's `reload all` command, which reloads everything. + To use newserv in other ways (e.g. for translating data), see the end of this document. ## Client patch directories diff --git a/src/Main.cc b/src/Main.cc index 82bd7eea..f5edba0b 100644 --- a/src/Main.cc +++ b/src/Main.cc @@ -46,6 +46,7 @@ #include "Server.hh" #include "ServerShell.hh" #include "ServerState.hh" +#include "SignalWatcher.hh" #include "StaticGameData.hh" #include "Text.hh" #include "TextIndex.hh" @@ -2727,6 +2728,7 @@ Action a_run_server_replay_log( shared_ptr shell; shared_ptr replay_session; + shared_ptr signal_watcher; if (is_replay) { config_log.info("Starting proxy server"); state->proxy_server = make_shared(base, state); @@ -2842,6 +2844,11 @@ Action a_run_server_replay_log( state->http_server->listen(netloc.first, netloc.second); } } + +#ifndef PHOSG_WINDOWS + config_log.info("Enabling signal watcher"); + signal_watcher = make_shared(state); +#endif } if (!state->username.empty()) { diff --git a/src/ServerShell.cc b/src/ServerShell.cc index c157149a..91bb2bad 100644 --- a/src/ServerShell.cc +++ b/src/ServerShell.cc @@ -232,6 +232,7 @@ CommandDefinition c_reload( teams - reindex all BB teams\n\ text-index - reload in-game text\n\ word-select - regenerate the Word Select translation table\n\ + all - do all of the above\n\ Reloading will not affect items that are in use; for example, if an Episode\n\ 3 battle is in progress, it will continue to use the previous map and card\n\ definitions. Similarly, BB clients are not forced to disconnect or reload\n\ @@ -242,7 +243,16 @@ CommandDefinition c_reload( +[](CommandArgs& args) { auto types = phosg::split(args.args, ' '); for (const auto& type : types) { - if (type == "bb-keys") { + if (type == "all") { + args.s->forward_to_event_thread([s = args.s]() { + try { + s->load_all(); + } catch (const exception& e) { + fprintf(stderr, "FAILED: %s\n", e.what()); + fprintf(stderr, "Some configuration may have been reloaded. Fix the underlying issue and try again.\n"); + } + }); + } else if (type == "bb-keys") { args.s->load_bb_private_keys(true); } else if (type == "accounts") { args.s->load_accounts(true); diff --git a/src/SignalWatcher.cc b/src/SignalWatcher.cc new file mode 100644 index 00000000..b7bbd372 --- /dev/null +++ b/src/SignalWatcher.cc @@ -0,0 +1,63 @@ +#include "SignalWatcher.hh" + +#include +#include +#include + +#include +#include + +#include "ReceiveCommands.hh" +#include "SendCommands.hh" +#include "ServerState.hh" +#include "StaticGameData.hh" + +using namespace std; + +SignalWatcher::SignalWatcher(shared_ptr state) + : log("[SignalWatcher] "), + state(state), + sigusr1_event(evsignal_new(this->state->base.get(), SIGUSR1, &SignalWatcher::dispatch_on_signal, this), event_free), + sigusr2_event(evsignal_new(this->state->base.get(), SIGUSR2, &SignalWatcher::dispatch_on_signal, this), event_free) { + event_add(this->sigusr1_event.get(), nullptr); + event_add(this->sigusr2_event.get(), nullptr); +} + +void SignalWatcher::dispatch_on_signal(evutil_socket_t signal, short what, void* ctx) { + if (what != EV_SIGNAL) { + throw logic_error("dispatch_on_signal called for non-signal event"); + } + reinterpret_cast(ctx)->on_signal(signal); +} + +void SignalWatcher::on_signal(int signum) { + switch (signum) { + case SIGUSR1: + this->log.info("Received SIGUSR1; reloading config.json"); + this->state->forward_to_event_thread([s = this->state]() { + try { + s->load_config_early(); + s->load_config_late(); + fprintf(stderr, "Configuration update complete\n"); + } catch (const exception& e) { + fprintf(stderr, "FAILED: %s\n", e.what()); + fprintf(stderr, "Some configuration may have been reloaded. Fix the underlying issue and try again.\n"); + } + }); + break; + case SIGUSR2: + this->log.info("Received SIGUSR2; reloading config.json and all dependencies"); + this->state->forward_to_event_thread([s = this->state]() { + try { + s->load_all(); + fprintf(stderr, "Configuration update complete\n"); + } catch (const exception& e) { + fprintf(stderr, "FAILED: %s\n", e.what()); + fprintf(stderr, "Some configuration may have been reloaded. Fix the underlying issue and try again.\n"); + } + }); + break; + default: + this->log.warning("Unknown signal received: %d", signum); + } +} diff --git a/src/SignalWatcher.hh b/src/SignalWatcher.hh new file mode 100644 index 00000000..c241879b --- /dev/null +++ b/src/SignalWatcher.hh @@ -0,0 +1,28 @@ +#pragma once + +#include +#include +#include + +#include + +#include "ServerState.hh" + +class SignalWatcher : public std::enable_shared_from_this { +public: + explicit SignalWatcher(std::shared_ptr state); + SignalWatcher(const SignalWatcher&) = delete; + SignalWatcher(SignalWatcher&&) = delete; + SignalWatcher& operator=(const SignalWatcher&) = delete; + SignalWatcher& operator=(SignalWatcher&&) = delete; + ~SignalWatcher() = default; + +protected: + phosg::PrefixedLogger log; + std::shared_ptr state; + std::unique_ptr sigusr1_event; + std::unique_ptr sigusr2_event; + + static void dispatch_on_signal(evutil_socket_t, short, void* ctx); + void on_signal(int signum); +};