diff --git a/CMakeLists.txt b/CMakeLists.txt index 0166ae9..0b30c40 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -21,6 +21,7 @@ message("PROJ_VER=${PROJ_VER}") find_package(Qt6 COMPONENTS Core + Concurrent Gui Widgets Network @@ -104,6 +105,7 @@ add_subdirectory(singleapplication) target_link_libraries(${PROJECT_NAME} Qt::Core + Qt::Concurrent Qt::Gui Qt::Widgets Qt::Network @@ -156,6 +158,6 @@ if(BUILD_TESTING) utils/proxysettings.cpp ) target_include_directories(systemproxysession_test PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) - target_link_libraries(systemproxysession_test Qt::Core Qt::Network Qt::Widgets ${PLATFORM_LIBS}) + target_link_libraries(systemproxysession_test Qt::Core Qt::Concurrent Qt::Network Qt::Widgets ${PLATFORM_LIBS}) add_test(NAME systemproxysession_test COMMAND systemproxysession_test) endif() diff --git a/application/systemproxysession.cpp b/application/systemproxysession.cpp index 45ba9ce..82b36a3 100644 --- a/application/systemproxysession.cpp +++ b/application/systemproxysession.cpp @@ -1,5 +1,7 @@ #include "systemproxysession.h" +#include + #include "infrastructure/platformsystemproxybackend.h" SystemProxySession::SystemProxySession(QObject *parent) @@ -14,34 +16,121 @@ SystemProxySession::SystemProxySession( : QObject(parent), backend(std::move(backend)) { + connect(&operationWatcher, &QFutureWatcher::finished, + this, &SystemProxySession::handleOperationFinished); } -bool SystemProxySession::hasConflict(const SystemProxyConfig &config) +SystemProxySession::~SystemProxySession() { - return backend->hasConflict(config); + operationWatcher.waitForFinished(); } -void SystemProxySession::enable(const SystemProxyConfig &config) +bool SystemProxySession::checkConflict(const SystemProxyConfig &config) { - backend->apply(config); - if (!enabled) - { - enabled = true; - emit enabledChanged(true); - } + return startOperation(Operation::CheckConflict, config); } -void SystemProxySession::disable() +bool SystemProxySession::enable(const SystemProxyConfig &config) { - backend->clear(); - if (enabled) - { - enabled = false; - emit enabledChanged(false); - } + return startOperation(Operation::Enable, config); +} + +bool SystemProxySession::disable() +{ + return startOperation(Operation::Disable); } bool SystemProxySession::isEnabled() const { return enabled; } + +bool SystemProxySession::isBusy() const +{ + return currentOperation != Operation::None; +} + +void SystemProxySession::clearBeforeShutdown() +{ + const Operation operation = currentOperation; + disconnect(&operationWatcher, nullptr, this, nullptr); + operationWatcher.waitForFinished(); + + if (operation != Operation::Disable + && (enabled || operation == Operation::Enable)) + { + backend->clear(); + } + + currentOperation = Operation::None; + enabled = false; +} + +bool SystemProxySession::startOperation(Operation operation, const SystemProxyConfig &config) +{ + if (isBusy()) + { + return false; + } + + currentOperation = operation; + emit busyChanged(true); + + SystemProxyBackend *proxyBackend = backend.get(); + operationWatcher.setFuture(QtConcurrent::run( + [proxyBackend, operation, config]() + { + OperationResult result{operation}; + switch (operation) + { + case Operation::CheckConflict: + result.conflict = proxyBackend->hasConflict(config); + break; + case Operation::Enable: + proxyBackend->apply(config); + break; + case Operation::Disable: + proxyBackend->clear(); + break; + case Operation::None: + break; + } + return result; + } + )); + return true; +} + +void SystemProxySession::handleOperationFinished() +{ + const OperationResult result = operationWatcher.result(); + currentOperation = Operation::None; + + bool stateChanged = false; + if (result.operation == Operation::Enable && !enabled) + { + enabled = true; + stateChanged = true; + } + else if (result.operation == Operation::Disable && enabled) + { + enabled = false; + stateChanged = true; + } + + emit busyChanged(false); + + if (stateChanged) + { + emit enabledChanged(enabled); + } + + if (result.operation == Operation::CheckConflict) + { + emit conflictCheckFinished(result.conflict); + } + else + { + emit operationFinished(enabled); + } +} diff --git a/application/systemproxysession.h b/application/systemproxysession.h index 0a97d01..ac282f0 100644 --- a/application/systemproxysession.h +++ b/application/systemproxysession.h @@ -3,6 +3,7 @@ #include +#include #include #include "systemproxybackend.h" @@ -14,17 +15,42 @@ Q_OBJECT public: explicit SystemProxySession(QObject *parent = nullptr); explicit SystemProxySession(std::unique_ptr backend, QObject *parent = nullptr); + ~SystemProxySession() override; - bool hasConflict(const SystemProxyConfig &config); - void enable(const SystemProxyConfig &config); - void disable(); + bool checkConflict(const SystemProxyConfig &config); + bool enable(const SystemProxyConfig &config); + bool disable(); bool isEnabled() const; + bool isBusy() const; + void clearBeforeShutdown(); signals: void enabledChanged(bool enabled); + void busyChanged(bool busy); + void conflictCheckFinished(bool conflict); + void operationFinished(bool enabled); private: + enum class Operation + { + None, + CheckConflict, + Enable, + Disable + }; + + struct OperationResult + { + Operation operation; + bool conflict = false; + }; + + bool startOperation(Operation operation, const SystemProxyConfig &config = {}); + void handleOperationFinished(); + std::unique_ptr backend; + QFutureWatcher operationWatcher; + Operation currentOperation = Operation::None; bool enabled = false; }; diff --git a/mainwindow.cpp b/mainwindow.cpp index 195a770..0c0a910 100644 --- a/mainwindow.cpp +++ b/mainwindow.cpp @@ -40,6 +40,29 @@ MainWindow::MainWindow(QWidget *parent) : ui->setupUi(this); systemProxySession = new SystemProxySession(this); + connect(systemProxySession, &SystemProxySession::busyChanged, this, + [this](bool busy) + { + ui->pushButton2->setEnabled(!busy); + ui->disableProxyAction->setEnabled(!busy); + }); + connect(systemProxySession, &SystemProxySession::enabledChanged, this, + [this](bool enabled) + { + ui->pushButton2->setText(enabled ? "清除系统代理" : "设置系统代理"); + if (!enabled && connectionSession != nullptr && !connectionSession->isActive()) + { + ui->pushButton2->hide(); + } + }); + connect(systemProxySession, &SystemProxySession::operationFinished, this, + [this](bool enabled) + { + if (enabled && connectionSession != nullptr && !connectionSession->isActive()) + { + systemProxySession->disable(); + } + }); setupTrayIcon(); setupProfileMenu(); @@ -101,16 +124,16 @@ MainWindow::MainWindow(QWidget *parent) : return; } - if (systemProxySession->isEnabled()) - { - ui->pushButton2->click(); - } - else - { - systemProxySession->disable(); - } - - addLog("已清理系统代理设置"); + connect(systemProxySession, &SystemProxySession::operationFinished, this, + [this](bool enabled) + { + if (!enabled) + { + addLog("已清理系统代理设置"); + } + }, + Qt::SingleShotConnection); + systemProxySession->disable(); }); // 文件-清理登录数据 @@ -812,9 +835,9 @@ void MainWindow::cleanUpWhenQuit() settings->sync(); // 清除系统代理 - if (systemProxySession != nullptr && systemProxySession->isEnabled()) + if (systemProxySession != nullptr) { - systemProxySession->disable(); + systemProxySession->clearBeforeShutdown(); } } diff --git a/tests/systemproxysession_test.cpp b/tests/systemproxysession_test.cpp index df2043b..6b47aa9 100644 --- a/tests/systemproxysession_test.cpp +++ b/tests/systemproxysession_test.cpp @@ -1,7 +1,11 @@ +#include #include #include #include +#include +#include +#include #include "application/systemproxysession.h" @@ -15,6 +19,8 @@ public: int applyCalls = 0; int clearCalls = 0; SystemProxyConfig lastConfig; + QSemaphore operationStarted; + QSemaphore allowOperationToFinish; bool hasConflict(const SystemProxyConfig &config) override { @@ -27,6 +33,8 @@ public: { ++applyCalls; lastConfig = config; + operationStarted.release(); + allowOperationToFinish.acquire(); } void clear() override @@ -35,7 +43,19 @@ public: } }; -bool delegatesPlatformOperationsAndTracksOwnedState() +bool waitUntil(const std::function &condition) +{ + QElapsedTimer timer; + timer.start(); + while (!condition() && timer.elapsed() < 1000) + { + QCoreApplication::processEvents(); + QThread::msleep(1); + } + return condition(); +} + +bool delegatesPlatformOperationsAsynchronouslyAndTracksOwnedState() { auto backend = std::make_unique(); FakeSystemProxyBackend *fake = backend.get(); @@ -43,38 +63,74 @@ bool delegatesPlatformOperationsAndTracksOwnedState() SystemProxySession session(std::move(backend)); const SystemProxyConfig config{1081, 1080, "localhost"}; - if (!session.hasConflict(config) + bool conflictResult = false; + QObject::connect(&session, &SystemProxySession::conflictCheckFinished, + [&](bool conflict) { conflictResult = conflict; }); + if (!session.checkConflict(config) + || !waitUntil([&]() { return !session.isBusy(); }) + || !conflictResult || fake->conflictChecks != 1 || session.isEnabled()) { - qCritical() << "delegatesPlatformOperationsAndTracksOwnedState failed at conflict check"; + qCritical() << "delegatesPlatformOperationsAsynchronouslyAndTracksOwnedState failed at conflict check"; return false; } - session.enable(config); - if (!session.isEnabled() + if (!session.enable(config) + || !fake->operationStarted.tryAcquire(1, 1000) + || !session.isBusy() + || session.isEnabled() + || session.disable()) + { + qCritical() << "delegatesPlatformOperationsAsynchronouslyAndTracksOwnedState failed while enabling"; + return false; + } + + fake->allowOperationToFinish.release(); + if (!waitUntil([&]() { return !session.isBusy(); }) + || !session.isEnabled() || fake->applyCalls != 1 || fake->lastConfig.httpPort != 1081 || fake->lastConfig.socksPort != 1080 || fake->lastConfig.bypass != "localhost") { - qCritical() << "delegatesPlatformOperationsAndTracksOwnedState failed at enable"; + qCritical() << "delegatesPlatformOperationsAsynchronouslyAndTracksOwnedState failed after enabling"; return false; } - session.disable(); - if (session.isEnabled() || fake->clearCalls != 1) + if (!session.disable() + || !waitUntil([&]() { return !session.isBusy(); }) + || session.isEnabled() + || fake->clearCalls != 1) { - qCritical() << "delegatesPlatformOperationsAndTracksOwnedState failed at disable"; + qCritical() << "delegatesPlatformOperationsAsynchronouslyAndTracksOwnedState failed at disable"; return false; } - session.disable(); - if (fake->clearCalls != 2) + if (!session.disable() + || !waitUntil([&]() { return !session.isBusy(); }) + || fake->clearCalls != 2) { qCritical() << "disable must also support clearing externally-owned proxy state"; return false; } + + if (!session.enable(config) + || !fake->operationStarted.tryAcquire(1, 1000)) + { + qCritical() << "clearBeforeShutdown failed to start enable"; + return false; + } + fake->allowOperationToFinish.release(); + session.clearBeforeShutdown(); + if (session.isBusy() + || session.isEnabled() + || fake->applyCalls != 2 + || fake->clearCalls != 3) + { + qCritical() << "clearBeforeShutdown must clear an in-flight enable"; + return false; + } return true; } } @@ -82,5 +138,5 @@ bool delegatesPlatformOperationsAndTracksOwnedState() int main(int argc, char *argv[]) { QCoreApplication app(argc, argv); - return delegatesPlatformOperationsAndTracksOwnedState() ? 0 : 1; + return delegatesPlatformOperationsAsynchronouslyAndTracksOwnedState() ? 0 : 1; } diff --git a/utils/proxysettings.cpp b/utils/proxysettings.cpp index 92845de..e87580e 100644 --- a/utils/proxysettings.cpp +++ b/utils/proxysettings.cpp @@ -1,7 +1,10 @@ +#include +#include #include #include #include #include +#include #include "utils.h" #if defined(Q_OS_WINDOWS) @@ -13,6 +16,31 @@ const QString macOSNetworkSetupPath = "/usr/sbin/networksetup"; +namespace +{ +void showProxyError(const QString &title, const QString &message) +{ + auto *application = qobject_cast(QCoreApplication::instance()); + if (application == nullptr) + { + qWarning() << title << message; + return; + } + + if (QThread::currentThread() == application->thread()) + { + QMessageBox::critical(nullptr, title, message); + return; + } + + QMetaObject::invokeMethod( + application, + [title, message]() { QMessageBox::critical(nullptr, title, message); }, + Qt::QueuedConnection + ); +} +} + void windowsSetProxyForAllConnections(const QString &proxyServer, const QString &bypass) { #if defined(Q_OS_WINDOWS) @@ -135,12 +163,12 @@ QStringList macOSGetActiveNetworkServices() process.waitForFinished(); if (process.error() != QProcess::UnknownError) { - QMessageBox::critical(nullptr, "获取网络服务失败", "执行命令失败:" + process.errorString()); + showProxyError("获取网络服务失败", "执行命令失败:" + process.errorString()); return {}; } if (process.exitCode() != 0) { - QMessageBox::critical(nullptr, "获取网络服务失败", "无法获取网络服务:" + process.readAllStandardError()); + showProxyError("获取网络服务失败", "无法获取网络服务:" + process.readAllStandardError()); return {}; } /* @@ -199,12 +227,12 @@ bool macOSIsSystemProxySet(macOSProxyType proxyType, const QString networkServic process.waitForFinished(); if (process.error() != QProcess::UnknownError) { - QMessageBox::critical(nullptr, "获取系统代理设置失败", "执行命令失败:" + process.errorString()); + showProxyError("获取系统代理设置失败", "执行命令失败:" + process.errorString()); return true; } if (process.exitCode() != 0) { - QMessageBox::critical(nullptr, "获取系统代理设置失败", "无法获取系统代理设置:" + process.readAllStandardError()); + showProxyError("获取系统代理设置失败", "无法获取系统代理设置:" + process.readAllStandardError()); return true; } QString output = process.readAllStandardOutput(); @@ -241,12 +269,12 @@ void macOSSetSystemProxy(macOSProxyType proxyType, const QString &networkService setProcess.waitForFinished(); if (setProcess.error() != QProcess::UnknownError) { - QMessageBox::critical(nullptr, "设置系统代理失败", "执行命令失败:" + setProcess.errorString()); + showProxyError("设置系统代理失败", "执行命令失败:" + setProcess.errorString()); return; } if (setProcess.exitCode() != 0) { - QMessageBox::critical(nullptr, "设置系统代理失败", "无法设置系统代理:" + setProcess.readAllStandardError()); + showProxyError("设置系统代理失败", "无法设置系统代理:" + setProcess.readAllStandardError()); return; } enableArgs << networkService << "on"; @@ -255,12 +283,12 @@ void macOSSetSystemProxy(macOSProxyType proxyType, const QString &networkService enableProcess.waitForFinished(); if (enableProcess.error() != QProcess::UnknownError) { - QMessageBox::critical(nullptr, "启用系统代理失败", "执行命令失败:" + enableProcess.errorString()); + showProxyError("启用系统代理失败", "执行命令失败:" + enableProcess.errorString()); return; } if (enableProcess.exitCode() != 0) { - QMessageBox::critical(nullptr, "启用系统代理失败", "无法启用系统代理:" + enableProcess.readAllStandardError()); + showProxyError("启用系统代理失败", "无法启用系统代理:" + enableProcess.readAllStandardError()); return; } } @@ -286,12 +314,12 @@ void macOSDisableSystemProxy(macOSProxyType proxyType, const QString &networkSer process.waitForFinished(); if (process.error() != QProcess::UnknownError) { - QMessageBox::critical(nullptr, "禁用系统代理失败", "执行命令失败:" + process.errorString()); + showProxyError("禁用系统代理失败", "执行命令失败:" + process.errorString()); return; } if (process.exitCode() != 0) { - QMessageBox::critical(nullptr, "禁用系统代理失败", "无法禁用系统代理:" + process.readAllStandardError()); + showProxyError("禁用系统代理失败", "无法禁用系统代理:" + process.readAllStandardError()); return; } } @@ -307,12 +335,12 @@ void macOSSetProxyBypass(const QString &networkService, const QString &bypass) process.waitForFinished(); if (process.error() != QProcess::UnknownError) { - QMessageBox::critical(nullptr, "设置代理绕过失败", "执行命令失败:" + process.errorString()); + showProxyError("设置代理绕过失败", "执行命令失败:" + process.errorString()); return; } if (process.exitCode() != 0) { - QMessageBox::critical(nullptr, "设置代理绕过失败", "无法设置代理绕过:" + process.readAllStandardError()); + showProxyError("设置代理绕过失败", "无法设置代理绕过:" + process.readAllStandardError()); return; } } @@ -427,7 +455,7 @@ void linuxSetSystemProxy(const QString &proxyServer, int httpPort, int socksPort if (results.count(true) != actions.size()) { - QMessageBox::critical(nullptr, "设置系统代理失败", "存在失败的命令"); + showProxyError("设置系统代理失败", "存在失败的命令"); } } diff --git a/zjuconnectmode.cpp b/zjuconnectmode.cpp index 773f28f..6d3d015 100644 --- a/zjuconnectmode.cpp +++ b/zjuconnectmode.cpp @@ -283,6 +283,11 @@ void MainWindow::initZjuConnect() connect(ui->pushButton2, &QPushButton::clicked, [&]() { + if (systemProxySession->isBusy()) + { + return; + } + if (!systemProxySession->isEnabled()) { int http_port = settings->value("ZJUConnect/HTTPPort").toInt(); @@ -292,44 +297,61 @@ void MainWindow::initZjuConnect() socks_port, settings->value("Common/SystemProxyBypass").toString() }; - if (systemProxySession->hasConflict(proxyConfig)) - { - bool suppressed = settings->value("Common/SuppressProxyOverrideWarning", false).toBool(); - if (suppressed) { - addLog("跳过系统代理覆盖警告,因为已设置了不再提示"); - } else { - QMessageBox msgBox(QMessageBox::Warning, "警告", - "当前已存在系统代理配置(可能是 Clash 或其它代理软件)\n是否覆盖当前系统代理配置?", - QMessageBox::Yes | QMessageBox::No, this); - - QCheckBox *dontShowCheckBox = new QCheckBox("不再提示"); - msgBox.setCheckBox(dontShowCheckBox); - - if (msgBox.exec() == QMessageBox::No) + connect(systemProxySession, &SystemProxySession::conflictCheckFinished, this, + [this, proxyConfig, http_port, socks_port](bool conflict) { - return; - } + if (!connectionSession->isActive()) + { + return; + } - if (dontShowCheckBox->isChecked()) - { - settings->setValue("Common/SuppressProxyOverrideWarning", true); - settings->sync(); - } - } - } + if (conflict) + { + bool suppressed = settings->value( + "Common/SuppressProxyOverrideWarning", false + ).toBool(); + if (suppressed) + { + addLog("跳过系统代理覆盖警告,因为已设置了不再提示"); + } + else + { + QMessageBox msgBox( + QMessageBox::Warning, + "警告", + "当前已存在系统代理配置(可能是 Clash 或其它代理软件)\n是否覆盖当前系统代理配置?", + QMessageBox::Yes | QMessageBox::No, + this + ); - addLog("设置系统代理:HTTP端口 " + QString::number(http_port) + ",SOCKS5 端口 " + QString::number(socks_port)); - systemProxySession->enable(proxyConfig); - ui->pushButton2->setText("清除系统代理"); + QCheckBox *dontShowCheckBox = new QCheckBox("不再提示"); + msgBox.setCheckBox(dontShowCheckBox); + + if (msgBox.exec() == QMessageBox::No) + { + return; + } + + if (dontShowCheckBox->isChecked()) + { + settings->setValue("Common/SuppressProxyOverrideWarning", true); + settings->sync(); + } + } + } + + addLog( + "设置系统代理:HTTP端口 " + QString::number(http_port) + + ",SOCKS5 端口 " + QString::number(socks_port) + ); + systemProxySession->enable(proxyConfig); + }, + Qt::SingleShotConnection); + systemProxySession->checkConflict(proxyConfig); } else { systemProxySession->disable(); - ui->pushButton2->setText("设置系统代理"); - if (!connectionSession->isActive()) - { - ui->pushButton2->hide(); - } } });