314 lines
12 KiB
C++
314 lines
12 KiB
C++
#include "ServerProcess.h"
|
|
|
|
#include <QCoreApplication>
|
|
#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)
|
|
: 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<quint16>(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);
|
|
}
|