added server
This commit is contained in:
@@ -0,0 +1,336 @@
|
||||
#include "ServerProcess.h"
|
||||
#include <QCoreApplication>
|
||||
#include <QJsonDocument>
|
||||
#include <QJsonObject>
|
||||
#include <QJsonArray>
|
||||
#include <QDir>
|
||||
#include <QFileInfo>
|
||||
#include <QHostAddress>
|
||||
|
||||
namespace {
|
||||
constexpr int kDefaultAdminPort = 7779;
|
||||
constexpr int kAdminTimeoutMs = 3000;
|
||||
}
|
||||
|
||||
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) {
|
||||
QTcpSocket socket;
|
||||
// Force IPv4 loopback — matches server admin bind on 127.0.0.1.
|
||||
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())}
|
||||
};
|
||||
}
|
||||
|
||||
const QByteArray payload = QJsonDocument(request).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;
|
||||
// Avoid spamming the log every second while the admin port is still starting.
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: accept server dirs without admin_server.py so older layouts still launch,
|
||||
// but prefer ones that include the admin channel when present.
|
||||
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);
|
||||
}
|
||||
Reference in New Issue
Block a user