#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"; } ServerProcess::ServerProcess(QObject *parent) : QObject(parent) , process(nullptr) , running(false) , m_adminPort(kDefaultAdminPort) , adminFailCount(0) { pythonPath = findPythonExecutable(); serverDir = findServerDirectory(); } 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("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"); 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; process->setWorkingDirectory(serverDir); process->start(pythonPath, arguments); } void ServerProcess::stop() { if (!process) return; if (process->state() == QProcess::Running) { process->terminate(); if (!process->waitForFinished(3000)) { process->kill(); process->waitForFinished(); } } 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")} }; } 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")} }; } const QByteArray token = tokenFile.readAll().trimmed(); tokenFile.close(); if (token.isEmpty()) { return QJsonObject{ {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 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())} }; } 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")} }; } QByteArray buffer; while (!buffer.contains('\n')) { if (!socket.waitForReadyRead(kAdminTimeoutMs)) { return QJsonObject{ {QStringLiteral("ok"), false}, {QStringLiteral("error"), QStringLiteral("Timed out waiting for admin response")} }; } buffer += socket.readAll(); } const int newline = buffer.indexOf('\n'); const QByteArray line = buffer.left(newline); 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 doc.object(); } void ServerProcess::fetchStats() { if (!running) return; const QJsonObject statsResponse = sendAdminCommand(QJsonObject{{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(QJsonObject{{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()); } 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) { QJsonObject request{ {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); return ok; } bool ServerProcess::banPlayer(int playerId, const QString &reason) { QJsonObject request{ {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(); } return ok; } bool ServerProcess::unbanIp(const QString &ip) { QJsonObject request{ {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); return ok; } QJsonArray ServerProcess::listBans() { const QJsonObject response = sendAdminCommand(QJsonObject{{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(QString("Server exited with code %1").arg(exitCode)); } else { emit error("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"; } emit this->error(errorString); } void ServerProcess::onReadyReadStandardOutput() { if (!process) return; outputBuffer += process->readAllStandardOutput(); while (outputBuffer.contains('\n')) { int newlinePos = outputBuffer.indexOf('\n'); QString line = outputBuffer.left(newlinePos); outputBuffer = outputBuffer.mid(newlinePos + 1); if (!line.isEmpty()) { line = line.trimmed(); parseLogLine(line); emit logMessage(line); } } } 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 ""; } 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"))) { return QFileInfo(candidate).absoluteFilePath(); } if (!dir.cdUp()) { break; } } 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; } } return QString(); } void ServerProcess::parseLogLine(const QString &line) { Q_UNUSED(line); }