From 15d69eb929c9a98806ff1561de5477877915788e Mon Sep 17 00:00:00 2001 From: BruebachL <44814898+BruebachL@users.noreply.github.com> Date: Tue, 10 Nov 2026 12:16:36 +0100 Subject: [PATCH] [Server] Add a servatrice-dev executable on the in-memory database (#7864) --- ...ervatrice_in_memory_database_interface.cpp | 63 ++++-- .../servatrice_in_memory_database_interface.h | 28 ++- servatrice/CMakeLists.txt | 9 + servatrice/dev/main.cpp | 193 ++++++++++++++++++ servatrice/src/servatrice.cpp | 7 +- servatrice/src/servatrice.h | 2 + tests/CMakeLists.txt | 11 + tests/servatrice_in_memory_database_test.cpp | 72 +++++++ 8 files changed, 358 insertions(+), 27 deletions(-) create mode 100644 servatrice/dev/main.cpp create mode 100644 tests/servatrice_in_memory_database_test.cpp diff --git a/libcockatrice_server_dev/libcockatrice/server/dev/servatrice_in_memory_database_interface.cpp b/libcockatrice_server_dev/libcockatrice/server/dev/servatrice_in_memory_database_interface.cpp index 6e4cc1af5..463b7048d 100644 --- a/libcockatrice_server_dev/libcockatrice/server/dev/servatrice_in_memory_database_interface.cpp +++ b/libcockatrice_server_dev/libcockatrice/server/dev/servatrice_in_memory_database_interface.cpp @@ -1,28 +1,34 @@ #include "libcockatrice/server/dev/servatrice_in_memory_database_interface.h" Servatrice_InMemoryDatabaseInterface::Servatrice_InMemoryDatabaseInterface(QObject *parent) - : Server_DatabaseInterface(parent) + : Server_DatabaseInterface(parent), state(std::make_shared()) { } void Servatrice_InMemoryDatabaseInterface::addUser(const ServerInfo_User &user) { - users.insert(QString::fromStdString(user.name()), user); + state->users.insert(QString::fromStdString(user.name()), user); } void Servatrice_InMemoryDatabaseInterface::setAuthenticationResult(AuthenticationResult result) { - authenticationResult = result; + state->authenticationResult = result; } void Servatrice_InMemoryDatabaseInterface::setActiveUserCount(int count) { - activeUserCount = count; + state->activeUserCount = count; } -const QList &Servatrice_InMemoryDatabaseInterface::getLogEntries() const +void Servatrice_InMemoryDatabaseInterface::addRoom(const BootstrapRoomRecord &room) { - return logEntries; + state->rooms.append(room); +} + +QList Servatrice_InMemoryDatabaseInterface::getLogEntries() const +{ + QMutexLocker locker(&state->mutex); + return state->logEntries; } AuthenticationResult Servatrice_InMemoryDatabaseInterface::checkUserPassword(Server_ProtocolHandler *, @@ -33,14 +39,18 @@ AuthenticationResult Servatrice_InMemoryDatabaseInterface::checkUserPassword(Ser int &, bool) { - return authenticationResult; + return state->authenticationResult; } ServerInfo_User Servatrice_InMemoryDatabaseInterface::getUserData(const QString &name, bool withId) { - const auto it = users.constFind(name); - if (it == users.constEnd()) { - return {}; + const auto it = state->users.constFind(name); + if (it == state->users.constEnd()) { + // The MySQL implementation always names the requested user, with the base user level. + ServerInfo_User user; + user.set_name(name.toStdString()); + user.set_user_level(ServerInfo_User::IsUser); + return user; } ServerInfo_User user = *it; @@ -52,17 +62,31 @@ ServerInfo_User Servatrice_InMemoryDatabaseInterface::getUserData(const QString int Servatrice_InMemoryDatabaseInterface::getNextGameId() { - return nextGameId++; + QMutexLocker locker(&state->mutex); + return state->nextGameId++; } int Servatrice_InMemoryDatabaseInterface::getNextReplayId() { - return nextReplayId++; + QMutexLocker locker(&state->mutex); + return state->nextReplayId++; } int Servatrice_InMemoryDatabaseInterface::getActiveUserCount(QString) { - return activeUserCount; + return state->activeUserCount; +} + +QList Servatrice_InMemoryDatabaseInterface::getBootstrapRooms(int) +{ + return state->rooms; +} + +Server_DatabaseInterface *Servatrice_InMemoryDatabaseInterface::createWorkerInterface(int) +{ + auto *worker = new Servatrice_InMemoryDatabaseInterface(); + worker->state = state; + return worker; } void Servatrice_InMemoryDatabaseInterface::logMessage(const int, @@ -74,10 +98,11 @@ void Servatrice_InMemoryDatabaseInterface::logMessage(const int, const QString &targetName, const QString &channel) { - logEntries.append(LogEntry{.senderName = senderName, - .message = logMessage, - .targetType = targetType, - .targetId = targetId, - .targetName = targetName, - .channel = channel}); + QMutexLocker locker(&state->mutex); + state->logEntries.append(LogEntry{.senderName = senderName, + .message = logMessage, + .targetType = targetType, + .targetId = targetId, + .targetName = targetName, + .channel = channel}); } diff --git a/libcockatrice_server_dev/libcockatrice/server/dev/servatrice_in_memory_database_interface.h b/libcockatrice_server_dev/libcockatrice/server/dev/servatrice_in_memory_database_interface.h index b7cc7fa6a..66e76cb28 100644 --- a/libcockatrice_server_dev/libcockatrice/server/dev/servatrice_in_memory_database_interface.h +++ b/libcockatrice_server_dev/libcockatrice/server/dev/servatrice_in_memory_database_interface.h @@ -5,7 +5,9 @@ #include #include +#include #include +#include /** * @brief In-memory Server_DatabaseInterface for tests and the dev server. @@ -37,8 +39,10 @@ public: void setAuthenticationResult(AuthenticationResult result); /** @brief Sets the value getActiveUserCount() reports. */ void setActiveUserCount(int count); + /** @brief Adds a room the SQL room bootstrap returns, in insertion order. */ + void addRoom(const BootstrapRoomRecord &room); - const QList &getLogEntries() const; + QList getLogEntries() const; AuthenticationResult checkUserPassword(Server_ProtocolHandler * /* handler */, const QString & /* user */, @@ -51,6 +55,8 @@ public: int getNextGameId() override; int getNextReplayId() override; int getActiveUserCount(QString connectionType = QString()) override; + QList getBootstrapRooms(int serverId) override; + Server_DatabaseInterface *createWorkerInterface(int instanceId) override; void logMessage(const int /* senderId */, const QString &senderName, const QString & /* senderIp */, @@ -61,12 +67,20 @@ public: const QString &channel = QString()) override; private: - QHash users; - AuthenticationResult authenticationResult = NotLoggedIn; - int activeUserCount = 0; - int nextGameId = 1; - int nextReplayId = 1; - QList logEntries; + /** @brief The state every interface created by createWorkerInterface() shares. */ + struct State + { + QHash users; + AuthenticationResult authenticationResult = NotLoggedIn; + int activeUserCount = 0; + int nextGameId = 1; + int nextReplayId = 1; + QList rooms; + QList logEntries; + mutable QMutex mutex; + }; + + std::shared_ptr state; }; #endif // SERVATRICE_IN_MEMORY_DATABASE_INTERFACE_H diff --git a/servatrice/CMakeLists.txt b/servatrice/CMakeLists.txt index 35c50f432..49b0e36b2 100644 --- a/servatrice/CMakeLists.txt +++ b/servatrice/CMakeLists.txt @@ -139,6 +139,15 @@ target_precompile_headers(servatrice PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtco target_link_libraries(servatrice PRIVATE servatrice_core) +# The dev server runs the shipped startup on the in-memory database. It is never installed. +add_executable(servatrice-dev dev/main.cpp) + +target_precompile_headers(servatrice-dev PRIVATE "${CMAKE_SOURCE_DIR}/cmake/pch/qtcore_pch.h") + +target_include_directories(servatrice-dev PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/src) + +target_link_libraries(servatrice-dev PRIVATE servatrice_core libcockatrice_server_dev) + # install rules if(UNIX) if(APPLE) diff --git a/servatrice/dev/main.cpp b/servatrice/dev/main.cpp new file mode 100644 index 000000000..39195f565 --- /dev/null +++ b/servatrice/dev/main.cpp @@ -0,0 +1,193 @@ +/** @file main.cpp + * @brief Servatrice dev server: the real server startup on an in-memory database. + */ + +#include "main.h" + +#include "libcockatrice/rng/rng_abstract.h" +#include "libcockatrice/rng/rng_sfmt.h" +#include "libcockatrice/server/dev/servatrice_in_memory_database_interface.h" +#include "libcockatrice/utility/cryptoutil.h" +#include "servatrice.h" +#include "server_logger.h" +#include "server_room.h" +#include "settingscache.h" +#include "smtpclient.h" +#include "version_string.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace +{ + +/** @brief The room the dev server always offers, so a client has somewhere to log in to. */ +void seedDevDatabase(Servatrice_InMemoryDatabaseInterface &database) +{ + Server_DatabaseInterface::BootstrapRoomRecord lobby; + lobby.id = 1; + lobby.name = "Dev Lobby"; + lobby.description = "In-memory room for local development."; + lobby.permissionLevel = "none"; + lobby.privateLevel = "none"; + lobby.autoJoin = true; + lobby.chatHistorySize = 100; + lobby.gameTypes = {"Commander"}; + database.addRoom(lobby); + + // AuthenticationNone on the shipped server reports every login as an unknown user, which is + // what makes a client connect as a guest. + database.setAuthenticationResult(UnknownUser); +} + +/** @brief The shipped server with its database replaced by the in-memory implementation. */ +class DevServatrice : public Servatrice +{ +public: + Servatrice_InMemoryDatabaseInterface *getInMemoryDatabase() const + { + return inMemoryDatabase; + } + +protected: + Server_DatabaseInterface *createMainDatabaseInterface() override + { + inMemoryDatabase = new Servatrice_InMemoryDatabaseInterface(this); + seedDevDatabase(*inMemoryDatabase); + return inMemoryDatabase; + } + +private: + Servatrice_InMemoryDatabaseInterface *inMemoryDatabase = nullptr; +}; + +/** @brief Routes Qt log output through the server logger, as the shipped binary does. */ +void logThroughServerLogger(QtMsgType, const QMessageLogContext &, const QString &message) +{ + logger->logMessage(message); +} + +void dumpState(Servatrice &server, const Servatrice_InMemoryDatabaseInterface &database) +{ + std::cout << "servatrice-dev: users=" << server.getUsersCount() << std::endl; + + for (const auto *room : server.getRooms()) { + std::cout << "servatrice-dev: room id=" << room->getId() << " name=\"" << room->getName().toStdString() << "\"" + << std::endl; + } + + std::cout << "servatrice-dev: log entries=" << database.getLogEntries().size() << std::endl; +} + +} // namespace + +int main(int argc, char *argv[]) +{ + QCoreApplication app(argc, argv); + QCoreApplication::setOrganizationName("Cockatrice"); + QCoreApplication::setApplicationName("Servatrice-dev"); + QCoreApplication::setApplicationVersion(VERSION_STRING); + + QCommandLineParser parser; + parser.setApplicationDescription("Servatrice on an in-memory database."); + parser.addHelpOption(); + parser.addVersionOption(); + + QCommandLineOption portOption({"p", "port"}, "TCP port to listen on; 0 picks an ephemeral port.", "port", "0"); + QCommandLineOption seedOption("seed", "Seed for the game RNG; random when omitted.", "seed"); + QCommandLineOption dumpStateOption("dump-state", "Print the server state after startup and keep serving."); + parser.addOption(portOption); + parser.addOption(seedOption); + parser.addOption(dumpStateOption); + parser.process(app); + + bool portOk = false; + const int port = parser.value(portOption).toInt(&portOk); + if (!portOk || port < 0 || port > 65535) { + std::cerr << "servatrice-dev: invalid port" << std::endl; + return EXIT_FAILURE; + } + + const QString seedValue = parser.value(seedOption); + bool seedOk = false; + const quint32 seed = + seedValue.isEmpty() ? static_cast(CryptoUtil::randomUInt64()) : seedValue.toUInt(&seedOk); + if (!seedValue.isEmpty() && !seedOk) { + std::cerr << "servatrice-dev: invalid seed" << std::endl; + return EXIT_FAILURE; + } + + // The shipped server treats DATABASE_URL as "use MySQL"; the dev server has no database. + qunsetenv("DATABASE_URL"); + + // Never touch the user's servatrice.ini: the dev server writes its throwaway settings to a + // temporary file it deletes on exit. + const QString configPath = + QDir(QDir::tempPath()) + .filePath("servatrice-dev-" + QString::number(QCoreApplication::applicationPid()) + ".ini"); + settingsCache = new SettingsCache(configPath); + settingsCache->setValue("server/port", port); + settingsCache->setValue("server/number_pools", 1); + settingsCache->setValue("server/websocket_number_pools", 0); + settingsCache->setValue("database/type", "none"); + settingsCache->setValue("authentication/method", "none"); + settingsCache->setValue("rooms/method", "sql"); + settingsCache->setValue("server/writelog", false); + settingsCache->sync(); + + loggerThread = new QThread; + loggerThread->setObjectName("logger"); + logger = new ServerLogger(true, false, QString()); + logger->moveToThread(loggerThread); + loggerThread->start(); + QMetaObject::invokeMethod(logger, "startLog", Qt::BlockingQueuedConnection, Q_ARG(QString, QString())); + qInstallMessageHandler(logThroughServerLogger); + + std::cout << "servatrice-dev: rng seed=" << seed << std::endl; + rng = new RNG_SFMT(seed); + smtpClient = new SmtpClient(); + + auto *server = new DevServatrice(); + QObject::connect(server, &QObject::destroyed, &app, &QCoreApplication::quit, Qt::QueuedConnection); + + int retval = EXIT_SUCCESS; + if (!server->initServer()) { + std::cerr << "servatrice-dev: initialization failed" << std::endl; + retval = EXIT_FAILURE; + } else { + const int listeningPort = server->getGameServerPort(); + if (listeningPort <= 0) { + std::cerr << "servatrice-dev: the server is not listening" << std::endl; + retval = EXIT_FAILURE; + } else { + std::cout << "servatrice-dev: listening on port " << listeningPort << std::endl; + if (parser.isSet(dumpStateOption)) { + dumpState(*server, *server->getInMemoryDatabase()); + } + retval = QCoreApplication::exec(); + } + } + + qInstallMessageHandler(nullptr); + + delete smtpClient; + delete rng; + delete settingsCache; + QFile::remove(configPath); + + logger->deleteLater(); + loggerThread->wait(); + delete loggerThread; + + google::protobuf::ShutdownProtobufLibrary(); + return retval; +} diff --git a/servatrice/src/servatrice.cpp b/servatrice/src/servatrice.cpp index 5576ef1d3..e0a6d654e 100644 --- a/servatrice/src/servatrice.cpp +++ b/servatrice/src/servatrice.cpp @@ -261,7 +261,7 @@ void Servatrice_IslServer::incomingConnection(qintptr socketDescriptor) } Servatrice::Servatrice(QObject *parent) - : Server(parent), authenticationMethod(AuthenticationNone), uptime(0), txBytes(0), rxBytes(0), + : Server(parent), authenticationMethod(AuthenticationNone), gameServer(nullptr), uptime(0), txBytes(0), rxBytes(0), shutdownTimer(nullptr) { } @@ -319,6 +319,11 @@ Server_DatabaseInterface *Servatrice::createMainDatabaseInterface() return new Servatrice_DatabaseInterface(-1, this); } +int Servatrice::getGameServerPort() const +{ + return gameServer != nullptr && gameServer->isListening() ? gameServer->serverPort() : -1; +} + bool Servatrice::initServer() { diff --git a/servatrice/src/servatrice.h b/servatrice/src/servatrice.h index 5c22fdea8..7f8d83cef 100644 --- a/servatrice/src/servatrice.h +++ b/servatrice/src/servatrice.h @@ -259,6 +259,8 @@ public: { return getAuthenticationStrictness() == AuthenticationStrict; } + /** @brief The port the game server listens on, or -1 when it is not listening. */ + int getGameServerPort() const; /** @brief Key sealing legacy password backups, or empty when they are disabled. */ QByteArray getLegacyBackupKey() const; /** @brief Days a legacy password backup may go unused before it is purged. */ diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 02e8e72f2..b825b3e58 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -430,6 +430,17 @@ cockatrice_add_test( LABELS server ) +cockatrice_add_test( + NAME + servatrice_in_memory_database_test + SOURCES + servatrice_in_memory_database_test.cpp + LIBS + libcockatrice_server_dev + LABELS + server +) + cockatrice_add_test( NAME private_chat_paging_test diff --git a/tests/servatrice_in_memory_database_test.cpp b/tests/servatrice_in_memory_database_test.cpp new file mode 100644 index 000000000..20218990d --- /dev/null +++ b/tests/servatrice_in_memory_database_test.cpp @@ -0,0 +1,72 @@ +/** @file servatrice_in_memory_database_test.cpp + * @brief Tests for the in-memory server database shared between the main interface and its workers. + * @ingroup Tests + */ + +#include "libcockatrice/server/dev/servatrice_in_memory_database_interface.h" + +#include +#include +#include +#include + +RNG_Abstract *rng = nullptr; // referenced by the server_remote library + +namespace +{ + +TEST(ServatriceInMemoryDatabaseTest, WorkersShareTheSeededState) +{ + Servatrice_InMemoryDatabaseInterface mainDatabase; + mainDatabase.setAuthenticationResult(UnknownUser); + + ServerInfo_User user; + user.set_name("tester"); + user.set_id(7); + mainDatabase.addUser(user); + + Server_DatabaseInterface::BootstrapRoomRecord room; + room.id = 3; + room.name = "Lobby"; + mainDatabase.addRoom(room); + + std::unique_ptr worker(mainDatabase.createWorkerInterface(0)); + ASSERT_NE(nullptr, worker); + + QString reason; + int secondsLeft = 0; + EXPECT_EQ(UnknownUser, worker->checkUserPassword(nullptr, "tester", "", "", reason, secondsLeft, false)); + + const ServerInfo_User fetched = worker->getUserData("tester", true); + EXPECT_EQ(7, fetched.id()); + EXPECT_FALSE(worker->getUserData("tester", false).has_id()); + + const auto rooms = worker->getBootstrapRooms(0); + ASSERT_EQ(1, rooms.size()); + EXPECT_EQ(3, rooms.first().id); + EXPECT_EQ("Lobby", rooms.first().name); + + // A worker records the messages it logs, and the main interface sees them. + worker->logMessage(1, "tester", "127.0.0.1", "hello", Server_DatabaseInterface::MessageTargetRoom, 3, "Lobby"); + EXPECT_EQ(1, mainDatabase.getLogEntries().size()); +} + +TEST(ServatriceInMemoryDatabaseTest, UnknownUsersAreNamedLikeTheMySqlImplementation) +{ + Servatrice_InMemoryDatabaseInterface database; + + const ServerInfo_User guest = database.getUserData("stranger", true); + EXPECT_EQ("stranger", QString::fromStdString(guest.name())); + EXPECT_EQ(ServerInfo_User::IsUser, guest.user_level()); +} + +TEST(ServatriceInMemoryDatabaseTest, IdsAdvancePerInterfaceState) +{ + Servatrice_InMemoryDatabaseInterface database; + + EXPECT_EQ(1, database.getNextGameId()); + EXPECT_EQ(2, database.getNextGameId()); + EXPECT_EQ(1, database.getNextReplayId()); +} + +} // namespace