Complete C# server cutover

This commit is contained in:
Nomads_Reach
2026-08-16 02:36:32 -04:00
parent ddadf87423
commit 757e614cdd
79 changed files with 923 additions and 13129 deletions
+113 -160
View File
@@ -1,17 +1,26 @@
#include "ServerProcess.h"
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>
#include <QJsonArray>
#include <QDir>
#include <QFile>
#include <QFileInfo>
#include <QHostAddress>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
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)
@@ -21,8 +30,8 @@ ServerProcess::ServerProcess(QObject *parent)
, m_adminPort(kDefaultAdminPort)
, adminFailCount(0)
{
pythonPath = findPythonExecutable();
serverDir = findServerDirectory();
resolveServerLaunch();
}
ServerProcess::~ServerProcess() {
@@ -39,42 +48,33 @@ int ServerProcess::adminPort() const {
void ServerProcess::start(const QString &configPath) {
if (running) {
emit error("Server is already running");
emit error(QStringLiteral("Server is already running"));
return;
}
if (pythonPath.isEmpty()) {
emit error("Python not found. Please install Python 3.9+ and add it to PATH");
return;
}
if (serverDir.isEmpty()) {
emit error("Server directory not found");
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;
arguments << "consumer_server_cli.py" << "serve" << "--config" << configPath;
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(pythonPath, arguments);
process->start(serverProgram, arguments);
}
void ServerProcess::stop() {
if (!process) return;
if (process->state() == QProcess::Running) {
process->terminate();
if (!process->waitForFinished(3000)) {
@@ -82,33 +82,24 @@ void ServerProcess::stop() {
process->waitForFinished();
}
}
process->deleteLater();
process = nullptr;
running = false;
adminFailCount = 0;
}
QJsonObject ServerProcess::sendAdminCommand(const QJsonObject &request) {
if (serverDir.isEmpty()) {
return QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Server directory is unavailable for admin authentication")}
};
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 QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Admin authentication token is not available yet")}
};
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Admin authentication token is not available yet")}};
}
const QByteArray token = tokenFile.readAll().trimmed();
tokenFile.close();
if (token.isEmpty()) {
return QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Admin authentication token is empty")}
};
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Admin authentication token is empty")}};
}
QJsonObject authenticatedRequest = request;
@@ -117,128 +108,97 @@ QJsonObject ServerProcess::sendAdminCommand(const QJsonObject &request) {
QTcpSocket socket;
socket.connectToHost(QStringLiteral("127.0.0.1"), static_cast<quint16>(m_adminPort));
if (!socket.waitForConnected(kAdminTimeoutMs)) {
return QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"),
QStringLiteral("Could not connect to admin port 127.0.0.1:%1 (%2)")
.arg(m_adminPort)
.arg(socket.errorString())}
};
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 QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Failed to send admin command")}
};
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Failed to send admin command")}};
}
QByteArray buffer;
while (!buffer.contains('\n')) {
if (!socket.waitForReadyRead(kAdminTimeoutMs)) {
return QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Timed out waiting for admin response")}
};
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 int newline = buffer.indexOf('\n');
const QByteArray line = buffer.left(newline);
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 QJsonObject{
{QStringLiteral("ok"), false},
{QStringLiteral("error"), QStringLiteral("Invalid admin response JSON")}
};
return {{QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Invalid admin response JSON")}};
}
return doc.object();
}
void ServerProcess::fetchStats() {
if (!running) return;
const QJsonObject statsResponse = sendAdminCommand(QJsonObject{{QStringLiteral("cmd"), QStringLiteral("stats")}});
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."))));
emit logMessage(QStringLiteral("[ADMIN] %1").arg(statsResponse.value(QStringLiteral("error")).toString(QStringLiteral("Admin stats request failed."))));
}
return;
}
const QJsonObject clientsResponse = sendAdminCommand(QJsonObject{{QStringLiteral("cmd"), QStringLiteral("clients")}});
const QJsonObject clientsResponse = sendAdminCommand({{QStringLiteral("cmd"), QStringLiteral("clients")}});
if (clientsResponse.value(QStringLiteral("ok")).toBool()) {
const QJsonObject data = clientsResponse.value(QStringLiteral("data")).toObject();
emit clientsUpdated(data.value(QStringLiteral("clients")).toArray());
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."))));
emit logMessage(QStringLiteral("[ADMIN] %1").arg(clientsResponse.value(QStringLiteral("error")).toString(QStringLiteral("Admin clients request failed."))));
}
}
bool ServerProcess::kickPlayer(int playerId, const QString &reason) {
QJsonObject request{
const QJsonObject response = sendAdminCommand({
{QStringLiteral("cmd"), QStringLiteral("kick")},
{QStringLiteral("playerId"), playerId},
{QStringLiteral("reason"), reason}
};
const QJsonObject response = sendAdminCommand(request);
});
const bool ok = response.value(QStringLiteral("ok")).toBool();
const QString message = ok
? response.value(QStringLiteral("message")).toString(QStringLiteral("Player kicked."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Kick failed."));
emit adminCommandFinished(ok, message);
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) {
QJsonObject request{
const QJsonObject response = sendAdminCommand({
{QStringLiteral("cmd"), QStringLiteral("ban")},
{QStringLiteral("playerId"), playerId},
{QStringLiteral("reason"), reason}
};
const QJsonObject response = sendAdminCommand(request);
});
const bool ok = response.value(QStringLiteral("ok")).toBool();
const QString message = ok
? response.value(QStringLiteral("message")).toString(QStringLiteral("Player banned."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Ban failed."));
emit adminCommandFinished(ok, message);
if (ok) {
fetchStats();
}
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) {
QJsonObject request{
const QJsonObject response = sendAdminCommand({
{QStringLiteral("cmd"), QStringLiteral("unban")},
{QStringLiteral("ip"), ip}
};
const QJsonObject response = sendAdminCommand(request);
});
const bool ok = response.value(QStringLiteral("ok")).toBool();
const QString message = ok
? response.value(QStringLiteral("message")).toString(QStringLiteral("IP unbanned."))
: response.value(QStringLiteral("error")).toString(QStringLiteral("Unban failed."));
emit adminCommandFinished(ok, message);
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(QJsonObject{{QStringLiteral("cmd"), QStringLiteral("bans")}});
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.")));
emit adminCommandFinished(false, response.value(QStringLiteral("error")).toString(QStringLiteral("Could not list bans.")));
return {};
}
return response.value(QStringLiteral("data")).toObject().value(QStringLiteral("bans")).toArray();
@@ -262,44 +222,29 @@ void ServerProcess::onProcessFinished(int exitCode, QProcess::ExitStatus exitSta
running = false;
adminFailCount = 0;
emit stopped();
if (exitStatus == QProcess::NormalExit) {
emit logMessage(QString("Server exited with code %1").arg(exitCode));
} else {
emit error("Server process crashed");
}
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 error) {
QString errorString;
switch (error) {
case QProcess::FailedToStart:
errorString = "Failed to start Python process";
break;
case QProcess::Crashed:
errorString = "Server process crashed";
break;
case QProcess::Timedout:
errorString = "Server process timed out";
break;
default:
errorString = "Unknown process error";
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 this->error(errorString);
emit error(message);
}
void ServerProcess::onReadyReadStandardOutput() {
if (!process) return;
outputBuffer += process->readAllStandardOutput();
while (outputBuffer.contains('\n')) {
int newlinePos = outputBuffer.indexOf('\n');
QString line = outputBuffer.left(newlinePos);
const int newlinePos = outputBuffer.indexOf('\n');
QString line = outputBuffer.left(newlinePos).trimmed();
outputBuffer = outputBuffer.mid(newlinePos + 1);
if (!line.isEmpty()) {
line = line.trimmed();
parseLogLine(line);
emit logMessage(line);
}
@@ -308,51 +253,59 @@ void ServerProcess::onReadyReadStandardOutput() {
void ServerProcess::onReadyReadStandardError() {
if (!process) return;
QString errorOutput = process->readAllStandardError();
emit logMessage("[STDERR] " + errorOutput);
}
QString ServerProcess::findPythonExecutable() {
QProcess proc;
proc.start("python", QStringList() << "--version");
if (proc.waitForFinished(2000)) {
return "python";
}
proc.start("python3", QStringList() << "--version");
if (proc.waitForFinished(2000)) {
return "python3";
}
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("/consumer_server_cli.py")) &&
QFileInfo::exists(candidate + QStringLiteral("/admin_server.py"))) {
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;
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;
}
}
dir = QDir(QCoreApplication::applicationDirPath());
for (int i = 0; i < 8; ++i) {
const QString candidate = dir.absoluteFilePath(QStringLiteral("server"));
if (QFileInfo::exists(candidate + QStringLiteral("/consumer_server_cli.py"))) {
return QFileInfo(candidate).absoluteFilePath();
}
if (!dir.cdUp()) {
break;
}
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;
return QString();
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) {