From 99e6c18f15be4a3cc6dcfa4de73519fc6337e444 Mon Sep 17 00:00:00 2001 From: cd-amn Date: Tue, 2 Jun 2026 18:40:30 +0400 Subject: [PATCH] feat: run xray-core in a forked worker process --- ipc/ipcserver.cpp | 133 +++++++++++++++++++++++++++++++++++++++- ipc/ipcserver.h | 16 +++++ service/server/main.cpp | 122 ++++++++++++++++++++++++++++++++++++ service/server/xray.cpp | 26 +------- service/server/xray.h | 5 -- 5 files changed, 269 insertions(+), 33 deletions(-) diff --git a/ipc/ipcserver.cpp b/ipc/ipcserver.cpp index 94bb9c553..7c6e96a57 100644 --- a/ipc/ipcserver.cpp +++ b/ipc/ipcserver.cpp @@ -1,9 +1,12 @@ #include "ipcserver.h" +#include #include #include +#include #include #include +#include #include #include #include @@ -16,10 +19,15 @@ #include "logger.h" #include "router.h" #include "killswitch.h" -#include "xray.h" #include "../client/daemon/daemon.h" +#ifdef Q_OS_MAC +#include "router_mac.h" +#include "core/utils/networkUtilities.h" +#include +#endif + #ifdef Q_OS_WIN #include "tapcontroller_win.h" #endif @@ -345,13 +353,112 @@ bool IpcServer::refreshKillSwitch(bool enabled) return KillSwitch::instance()->refresh(enabled); } +void IpcServer::onXrayWorkerLine(const QByteArray& line) +{ + const QJsonObject ev = QJsonDocument::fromJson(line).object(); + const QString name = ev.value("ev").toString(); + if (name == "log") { + const QString level = ev.value("level").toString(); + const QString msg = ev.value("msg").toString(); + if (level == QLatin1String("warn")) { + qWarning().noquote() << "[xray-worker]" << msg; + } else if (level == QLatin1String("error") || level == QLatin1String("fatal")) { + qCritical().noquote() << "[xray-worker]" << msg; + } else if (level == QLatin1String("info")) { + qInfo().noquote() << "[xray-worker]" << msg; + } else { + qDebug().noquote() << "[xray-worker]" << msg; + } + } else if (name == "ready" || name == "failed") { + if (m_xrayStartLoop) { + m_xrayStartResult = (name == "ready"); + m_xrayStartLoop->quit(); + } + } +} + bool IpcServer::xrayStart(const QString& cfg) { #ifdef MZ_DEBUG qDebug() << "IpcServer::xrayStart"; #endif - return Xray::getInstance().startXray(cfg); + if (!m_xrayProcess || m_xrayProcess->state() == QProcess::NotRunning) { + m_xrayProcess = QSharedPointer::create(); + m_xrayStdoutBuf.clear(); + + QObject::connect(m_xrayProcess.data(), &QProcess::readyReadStandardOutput, this, [this]() { + m_xrayStdoutBuf.append(m_xrayProcess->readAllStandardOutput()); + int nl; + while ((nl = m_xrayStdoutBuf.indexOf('\n')) >= 0) { + const QByteArray line = m_xrayStdoutBuf.left(nl); + m_xrayStdoutBuf.remove(0, nl + 1); + onXrayWorkerLine(line); + } + }); + + QObject::connect(m_xrayProcess.data(), &QProcess::errorOccurred, this, + [this](QProcess::ProcessError err) { + qCritical().noquote().nospace() << "[xray-worker] process error: " << err; + if (m_xrayStartLoop) { + m_xrayStartResult = false; + m_xrayStartLoop->quit(); + } + }); + + QObject::connect(m_xrayProcess.data(), + QOverload::of(&QProcess::finished), + this, [this](int code, QProcess::ExitStatus status) { + qDebug().noquote().nospace() << "[xray-worker] finished, code=" << code << " status=" << status; + if (m_xrayStartLoop) { + m_xrayStartResult = false; + m_xrayStartLoop->quit(); + } + }); + + m_xrayProcess->setProgram(QCoreApplication::applicationFilePath()); + m_xrayProcess->setArguments({QStringLiteral("--xray-worker")}); + m_xrayProcess->start(); + + if (!m_xrayProcess->waitForStarted(5000)) { + qCritical().noquote() << "[xray-worker] failed to start"; + m_xrayProcess.reset(); + return false; + } + } + +#ifdef Q_OS_MAC + const auto gatewayAndIface = NetworkUtilities::getGatewayAndIface(); + m_xrayUplinkGateway = gatewayAndIface.first; + m_xrayUplinkIface = gatewayAndIface.second.name(); + if (!m_xrayUplinkIface.isEmpty() && !m_xrayUplinkGateway.isEmpty()) { + if (!RouterMac::Instance().routeAddXray(m_xrayUplinkIface, m_xrayUplinkGateway)) { + qWarning() << "[xray] failed to install xray routes on" << m_xrayUplinkIface; + } + } +#endif + + const QJsonObject startCmd{{QStringLiteral("op"), QStringLiteral("start")}, + {QStringLiteral("config"), cfg}}; + m_xrayProcess->write(QJsonDocument(startCmd).toJson(QJsonDocument::Compact) + '\n'); + + QEventLoop loop; + m_xrayStartLoop = &loop; + m_xrayStartResult = false; + loop.exec(); + m_xrayStartLoop.clear(); + + if (!m_xrayStartResult) { +#ifdef Q_OS_MAC + if (!m_xrayUplinkIface.isEmpty()) { + RouterMac::Instance().routeDeleteXray(m_xrayUplinkIface, m_xrayUplinkGateway); + m_xrayUplinkIface.clear(); + m_xrayUplinkGateway.clear(); + } +#endif + } + + return m_xrayStartResult; } bool IpcServer::xrayStop() @@ -360,5 +467,25 @@ bool IpcServer::xrayStop() qDebug() << "IpcServer::xrayStop"; #endif - return Xray::getInstance().stopXray(); + if (m_xrayProcess && m_xrayProcess->state() != QProcess::NotRunning) { + const QJsonObject stopCmd{{QStringLiteral("op"), QStringLiteral("stop")}}; + m_xrayProcess->write(QJsonDocument(stopCmd).toJson(QJsonDocument::Compact) + '\n'); + + if (!m_xrayProcess->waitForFinished(3000)) { + qWarning().noquote() << "[xray-worker] did not exit after stop, killing"; + m_xrayProcess->kill(); + m_xrayProcess->waitForFinished(1000); + } + } + m_xrayProcess.reset(); + +#ifdef Q_OS_MAC + if (!m_xrayUplinkIface.isEmpty()) { + RouterMac::Instance().routeDeleteXray(m_xrayUplinkIface, m_xrayUplinkGateway); + m_xrayUplinkIface.clear(); + m_xrayUplinkGateway.clear(); + } +#endif + + return true; } diff --git a/ipc/ipcserver.h b/ipc/ipcserver.h index 2e0a9724b..29e66ea0b 100644 --- a/ipc/ipcserver.h +++ b/ipc/ipcserver.h @@ -1,9 +1,14 @@ #ifndef IPCSERVER_H #define IPCSERVER_H +#include +#include #include #include +#include +#include #include +#include #include #include "../client/daemon/interfaceconfig.h" #include "../client/mozilla/pinghelper.h" @@ -72,6 +77,17 @@ private: QMap m_processes; PingHelper m_pingHelper; + + QSharedPointer m_xrayProcess; + QByteArray m_xrayStdoutBuf; + QPointer m_xrayStartLoop; + bool m_xrayStartResult = false; +#ifdef Q_OS_MAC + QString m_xrayUplinkIface; + QString m_xrayUplinkGateway; +#endif + + void onXrayWorkerLine(const QByteArray& line); }; #endif // IPCSERVER_H diff --git a/service/server/main.cpp b/service/server/main.cpp index f119c2fc5..382c3ae43 100644 --- a/service/server/main.cpp +++ b/service/server/main.cpp @@ -1,21 +1,139 @@ +#include #include +#include +#include + +#include +#include +#include #include "version.h" #include "localserver.h" #include "logger.h" #include "systemservice.h" +#include "xray.h" #include "core/utils/utilities.h" #ifdef Q_OS_WIN #include "platforms/windows/daemon/windowsdaemontunnel.h" +#include namespace { int s_argc = 0; char** s_argv = nullptr; } // namespace +#else +#include #endif +namespace { + +constexpr const char* kXrayWorkerArg = "--xray-worker"; + +void writeWorkerEvent(const QJsonObject& obj) +{ + const QByteArray bytes = QJsonDocument(obj).toJson(QJsonDocument::Compact) + '\n'; + std::fwrite(bytes.constData(), 1, bytes.size(), stdout); + std::fflush(stdout); +} + +void workerMessageHandler(QtMsgType type, const QMessageLogContext&, const QString& msg) +{ + const char* level = "debug"; + switch (type) { + case QtDebugMsg: level = "debug"; break; + case QtInfoMsg: level = "info"; break; + case QtWarningMsg: level = "warn"; break; + case QtCriticalMsg: level = "error"; break; + case QtFatalMsg: level = "fatal"; break; + } + writeWorkerEvent({{"ev", "log"}, {"level", QString::fromLatin1(level)}, {"msg", msg}}); +} + +int readStdinChunk(char* buf, int cap) +{ +#ifdef Q_OS_WIN + DWORD n = 0; + BOOL ok = ReadFile(GetStdHandle(STD_INPUT_HANDLE), buf, cap, &n, nullptr); + return ok ? static_cast(n) : -1; +#else + return static_cast(::read(STDIN_FILENO, buf, cap)); +#endif +} + +int runXrayWorker(int argc, char** argv) +{ + qInstallMessageHandler(workerMessageHandler); + QCoreApplication app(argc, argv); + + auto* xray = new Xray; + auto* buf = new QByteArray; + + auto exitWorker = [xray](int code) { + xray->stopXray(); + std::fflush(stdout); + std::_Exit(code); + }; + + auto handleLine = [xray, exitWorker](const QByteArray& line) { + const QJsonObject cmd = QJsonDocument::fromJson(line).object(); + const QString op = cmd.value("op").toString(); + if (op == "start") { + const QString cfg = cmd.value("config").toString(); + const bool ok = xray->startXray(cfg); + writeWorkerEvent({{"ev", ok ? "ready" : "failed"}}); + } else if (op == "stop") { + writeWorkerEvent({{"ev", "stopped"}}); + exitWorker(0); + } + }; + + auto onChunk = [buf, handleLine](const QByteArray& data) { + buf->append(data); + int nl; + while ((nl = buf->indexOf('\n')) >= 0) { + const QByteArray line = buf->left(nl); + buf->remove(0, nl + 1); + handleLine(line); + } + }; + + // Detached reader thread: stdin is an anonymous pipe (the parent's write end). + // QSocketNotifier doesn't work on anonymous pipes on Windows, so use a blocking + // read in a thread and dispatch events back to the main thread. On EOF the + // worker exits immediately via _Exit to avoid races with QCoreApplication + // teardown while this thread is still alive. + std::thread([onChunk, exitWorker]() { + char chunk[4096]; + while (true) { + const int n = readStdinChunk(chunk, sizeof(chunk)); + if (n <= 0) { + // Parent gone or pipe error: shut xray down and exit. + exitWorker(0); + return; + } + QMetaObject::invokeMethod(qApp, [onChunk, data = QByteArray(chunk, n)]() { + onChunk(data); + }, Qt::QueuedConnection); + } + }).detach(); + + return app.exec(); +} + +bool hasXrayWorkerArg(int argc, char** argv) +{ + for (int i = 1; i < argc; ++i) { + if (std::strcmp(argv[i], kXrayWorkerArg) == 0) { + return true; + } + } + return false; +} + +} // namespace + int runApplication(int argc, char** argv) { QCoreApplication app(argc,argv); @@ -44,6 +162,10 @@ int runApplication(int argc, char** argv) int main(int argc, char **argv) { + if (hasXrayWorkerArg(argc, argv)) { + return runXrayWorker(argc, argv); + } + Utils::initializePath(Logger::systemLogDir()); if (argc >= 2) { diff --git a/service/server/xray.cpp b/service/server/xray.cpp index 076a4484e..c882d7a1c 100644 --- a/service/server/xray.cpp +++ b/service/server/xray.cpp @@ -1,8 +1,5 @@ #include "xray.h" #include "core/utils/networkUtilities.h" -#ifdef Q_OS_MAC -#include "router_mac.h" -#endif #include #include @@ -34,9 +31,7 @@ bool Xray::startXray(const QString &cfg) { qDebug() << "Xray::startXray()"; - const auto gatewayAndIface = NetworkUtilities::getGatewayAndIface(); - const QString defaultGateway = gatewayAndIface.first; - const QNetworkInterface defaultIface = gatewayAndIface.second; + const QNetworkInterface defaultIface = NetworkUtilities::getGatewayAndIface().second; #ifdef Q_OS_LINUX m_defaultIfaceName = defaultIface.name().toUtf8(); #else @@ -46,17 +41,6 @@ bool Xray::startXray(const QString &cfg) qDebug() << "[xray] using uplink interface:" << defaultIface.name() << "(" << defaultIface.index() << ")"; } -#ifdef Q_OS_MAC - m_uplinkIfaceName = defaultIface.name(); - m_uplinkGateway = defaultGateway; - if (!m_uplinkIfaceName.isEmpty()) { - const bool installed = RouterMac::Instance().routeAddXray(m_uplinkIfaceName, m_uplinkGateway); - if (!installed) { - qWarning() << "[xray] failed to install xray routes on" << m_uplinkIfaceName; - } - } -#endif - if (auto err = amnezia_xray_setsockcallback(ctxSockCallback, this); err != nullptr) { qDebug() << "[xray] sockopt failed: " << err; amnezia_xray_free(err); @@ -91,14 +75,6 @@ bool Xray::stopXray() success = false; } -#ifdef Q_OS_MAC - if (!m_uplinkIfaceName.isEmpty()) { - RouterMac::Instance().routeDeleteXray(m_uplinkIfaceName, m_uplinkGateway); - } - m_uplinkIfaceName.clear(); - m_uplinkGateway.clear(); -#endif - return success; } diff --git a/service/server/xray.h b/service/server/xray.h index 45704137a..f54d9902f 100644 --- a/service/server/xray.h +++ b/service/server/xray.h @@ -31,11 +31,6 @@ private: #else int m_defaultIfaceIdx; #endif - -#ifdef Q_OS_MAC - QString m_uplinkIfaceName; - QString m_uplinkGateway; -#endif }; #endif // XRAY_H