#include "ServerProcess.h" #include #include #include #include #include #include #include #include namespace { constexpr int kDefaultAdminPort = 7779; constexpr int kAdminTimeoutMs = 3000; constexpr auto kAdminTokenFileName = ".admin-token"; QString appHostName() { #ifdef Q_OS_WIN return QStringLiteral("CommonwealthOnline.Server.exe"); #else return QStringLiteral("CommonwealthOnline.Server"); #endif } } ServerProcess::ServerProcess(QObject *parent) : QObject(parent) , process(nullptr) , running(false) , m_adminPort(kDefaultAdminPort) , adminFailCount(0) { serverDir = findServerDirectory(); resolveServerLaunch(); } ServerProcess::~ServerProcess() { stop(); } void ServerProcess::setAdminPort(int port) { m_adminPort = (port > 0 && port <= 65535) ? port : kDefaultAdminPort; } int ServerProcess::adminPort() const { return m_adminPort; } void ServerProcess::start(const QString &configPath) { if (running) { emit error(QStringLiteral("Server is already running")); return; } if (serverDir.isEmpty()) { emit error(QStringLiteral("C# server directory not found")); return; } if (serverProgram.isEmpty() && !resolveServerLaunch()) { emit error(QStringLiteral("CommonwealthOnline.Server executable was not found. Build or publish the .NET server first.")); return; } process = new QProcess(this); connect(process, SIGNAL(finished(int, QProcess::ExitStatus)), this, SLOT(onProcessFinished(int, QProcess::ExitStatus))); connect(process, SIGNAL(error(QProcess::ProcessError)), this, SLOT(onProcessError(QProcess::ProcessError))); connect(process, SIGNAL(readyReadStandardOutput()), this, SLOT(onReadyReadStandardOutput())); connect(process, SIGNAL(readyReadStandardError()), this, SLOT(onReadyReadStandardError())); connect(process, SIGNAL(started()), this, SLOT(onProcessStarted())); QStringList arguments = serverPrefixArguments; arguments << QStringLiteral("serve") << QStringLiteral("--config") << configPath; process->setWorkingDirectory(serverDir); process->start(serverProgram, arguments); } void ServerProcess::stop() { if (!process) return; if (process->state() == QProcess::Running) { process->terminate(); if (!process->waitForFinished(3000)) { process->kill(); process->waitForFinished(); } } process->deleteLater(); process = nullptr; running = false; adminFailCount = 0; } QJsonObject ServerProcess::sendAdminCommand(const QJsonObject &request) { if (serverDir.isEmpty()) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Server directory is unavailable for admin authentication")}}; } QFile tokenFile(QDir(serverDir).absoluteFilePath(QString::fromLatin1(kAdminTokenFileName))); if (!tokenFile.open(QIODevice::ReadOnly | QIODevice::Text)) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Admin authentication token is not available yet")}}; } const QByteArray token = tokenFile.readAll().trimmed(); if (token.isEmpty()) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Admin authentication token is empty")}}; } QJsonObject authenticatedRequest = request; authenticatedRequest.insert(QStringLiteral("adminToken"), QString::fromUtf8(token)); QTcpSocket socket; socket.connectToHost(QStringLiteral("127.0.0.1"), static_cast(m_adminPort)); if (!socket.waitForConnected(kAdminTimeoutMs)) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Could not connect to admin port 127.0.0.1:%1 (%2)").arg(m_adminPort).arg(socket.errorString())}}; } const QByteArray payload = QJsonDocument(authenticatedRequest).toJson(QJsonDocument::Compact) + '\n'; if (socket.write(payload) < 0 || !socket.waitForBytesWritten(kAdminTimeoutMs)) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Failed to send admin command")}}; } QByteArray buffer; while (!buffer.contains('\n')) { if (!socket.waitForReadyRead(kAdminTimeoutMs)) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Timed out waiting for admin response")}}; } buffer += socket.readAll(); if (buffer.size() > 64 * 1024) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Admin response exceeded maximum size")}}; } } const QByteArray line = buffer.left(buffer.indexOf('\n')); QJsonParseError parseError{}; const QJsonDocument doc = QJsonDocument::fromJson(line, &parseError); if (parseError.error != QJsonParseError::NoError || !doc.isObject()) { return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Invalid admin response JSON")}}; } return doc.object(); } void ServerProcess::fetchStats() { if (!running) return; const QJsonObject statsResponse = sendAdminCommand({{QStringLiteral("cmd"), QStringLiteral("stats")}}); if (statsResponse.value(QStringLiteral("ok")).toBool()) { adminFailCount = 0; emit statsUpdated(statsResponse.value(QStringLiteral("data")).toObject()); } else { ++adminFailCount; if (adminFailCount == 3 || adminFailCount == 10 || (adminFailCount % 30) == 0) { emit logMessage(QStringLiteral("[ADMIN] %1").arg(statsResponse.value(QStringLiteral("error")).toString(QStringLiteral("Admin stats request failed.")))); } return; } const QJsonObject clientsResponse = sendAdminCommand({{QStringLiteral("cmd"), QStringLiteral("clients")}}); if (clientsResponse.value(QStringLiteral("ok")).toBool()) { emit clientsUpdated(clientsResponse.value(QStringLiteral("data")).toObject().value(QStringLiteral("clients")).toArray()); } else { emit logMessage(QStringLiteral("[ADMIN] %1").arg(clientsResponse.value(QStringLiteral("error")).toString(QStringLiteral("Admin clients request failed.")))); } } bool ServerProcess::kickPlayer(int playerId, const QString &reason) { const QJsonObject response = sendAdminCommand({ {QStringLiteral("cmd"), QStringLiteral("kick")}, {QStringLiteral("playerId"), playerId}, {QStringLiteral("reason"), reason} }); const bool ok = response.value(QStringLiteral("ok")).toBool(); emit adminCommandFinished(ok, ok ? response.value(QStringLiteral("message")).toString(QStringLiteral("Player kicked.")) : response.value(QStringLiteral("error")).toString(QStringLiteral("Kick failed."))); return ok; } bool ServerProcess::banPlayer(int playerId, const QString &reason) { const QJsonObject response = sendAdminCommand({ {QStringLiteral("cmd"), QStringLiteral("ban")}, {QStringLiteral("playerId"), playerId}, {QStringLiteral("reason"), reason} }); const bool ok = response.value(QStringLiteral("ok")).toBool(); emit adminCommandFinished(ok, ok ? response.value(QStringLiteral("message")).toString(QStringLiteral("Player banned.")) : response.value(QStringLiteral("error")).toString(QStringLiteral("Ban failed."))); if (ok) fetchStats(); return ok; } bool ServerProcess::unbanIp(const QString &ip) { const QJsonObject response = sendAdminCommand({ {QStringLiteral("cmd"), QStringLiteral("unban")}, {QStringLiteral("ip"), ip} }); const bool ok = response.value(QStringLiteral("ok")).toBool(); emit adminCommandFinished(ok, ok ? response.value(QStringLiteral("message")).toString(QStringLiteral("IP unbanned.")) : response.value(QStringLiteral("error")).toString(QStringLiteral("Unban failed."))); return ok; } QJsonArray ServerProcess::listBans() { const QJsonObject response = sendAdminCommand({{QStringLiteral("cmd"), QStringLiteral("bans")}}); if (!response.value(QStringLiteral("ok")).toBool()) { emit adminCommandFinished(false, response.value(QStringLiteral("error")).toString(QStringLiteral("Could not list bans."))); return {}; } return response.value(QStringLiteral("data")).toObject().value(QStringLiteral("bans")).toArray(); } bool ServerProcess::isRunning() const { return running && process && process->state() == QProcess::Running; } QString ServerProcess::serverDirectory() const { return serverDir; } void ServerProcess::onProcessStarted() { running = true; adminFailCount = 0; emit started(); } void ServerProcess::onProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { running = false; adminFailCount = 0; emit stopped(); if (exitStatus == QProcess::NormalExit) emit logMessage(QStringLiteral("Server exited with code %1").arg(exitCode)); else emit error(QStringLiteral("Server process crashed")); } void ServerProcess::onProcessError(QProcess::ProcessError processError) { QString message; switch (processError) { case QProcess::FailedToStart: message = QStringLiteral("Failed to start CommonwealthOnline.Server"); break; case QProcess::Crashed: message = QStringLiteral("Server process crashed"); break; case QProcess::Timedout: message = QStringLiteral("Server process timed out"); break; default: message = QStringLiteral("Unknown server process error"); break; } emit error(message); } void ServerProcess::onReadyReadStandardOutput() { if (!process) return; outputBuffer += process->readAllStandardOutput(); while (outputBuffer.contains('\n')) { const int newlinePos = outputBuffer.indexOf('\n'); QString line = outputBuffer.left(newlinePos).trimmed(); outputBuffer = outputBuffer.mid(newlinePos + 1); if (!line.isEmpty()) { parseLogLine(line); emit logMessage(line); } } } void ServerProcess::onReadyReadStandardError() { if (!process) return; const QString value = QString::fromUtf8(process->readAllStandardError()).trimmed(); if (!value.isEmpty()) emit logMessage(QStringLiteral("[STDERR] ") + value); } QString ServerProcess::findServerDirectory() { QDir dir(QCoreApplication::applicationDirPath()); for (int i = 0; i < 8; ++i) { const QString candidate = dir.absoluteFilePath(QStringLiteral("server")); if (QFileInfo::exists(candidate + QStringLiteral("/CommonwealthOnline.Server.csproj")) || QFileInfo::exists(candidate + QStringLiteral("/CommonwealthOnline.Server.dll")) || QFileInfo::exists(candidate + QLatin1Char('/') + appHostName())) { return QFileInfo(candidate).absoluteFilePath(); } if (!dir.cdUp()) break; } return {}; } bool ServerProcess::resolveServerLaunch() { serverProgram.clear(); serverPrefixArguments.clear(); if (serverDir.isEmpty()) return false; const QDir dir(serverDir); const QStringList appHostCandidates = { dir.absoluteFilePath(appHostName()), dir.absoluteFilePath(QStringLiteral("publish/") + appHostName()), dir.absoluteFilePath(QStringLiteral("bin/Release/net8.0/") + appHostName()) }; for (const QString &candidate : appHostCandidates) { if (QFileInfo::exists(candidate)) { serverProgram = QFileInfo(candidate).absoluteFilePath(); return true; } } const QStringList dllCandidates = { dir.absoluteFilePath(QStringLiteral("CommonwealthOnline.Server.dll")), dir.absoluteFilePath(QStringLiteral("publish/CommonwealthOnline.Server.dll")), dir.absoluteFilePath(QStringLiteral("bin/Release/net8.0/CommonwealthOnline.Server.dll")) }; QString dllPath; for (const QString &candidate : dllCandidates) { if (QFileInfo::exists(candidate)) { dllPath = QFileInfo(candidate).absoluteFilePath(); break; } } if (dllPath.isEmpty()) return false; QProcess probe; probe.start(QStringLiteral("dotnet"), {QStringLiteral("--version")}); if (!probe.waitForFinished(3000) || probe.exitStatus() != QProcess::NormalExit || probe.exitCode() != 0) return false; serverProgram = QStringLiteral("dotnet"); serverPrefixArguments << dllPath; return true; } void ServerProcess::parseLogLine(const QString &line) { Q_UNUSED(line); }