From 62229ad78174923f4997bb0f0024b4b8b4d0986a Mon Sep 17 00:00:00 2001 From: Chenx Dust Date: Mon, 10 Aug 2026 18:58:51 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=94=B9=E8=BF=9B=E7=9A=84=E7=8A=B6?= =?UTF-8?q?=E6=80=81=E6=8C=87=E7=A4=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- application/connectionsession.cpp | 6 +++- application/coreprocess.h | 1 + core/connectionsessionstate.cpp | 16 ++++++--- core/connectionsessionstate.h | 3 +- .../coreprocess/coreoutputparser.cpp | 4 +++ infrastructure/coreprocess/coreoutputparser.h | 1 + .../coreprocess/zjuconnectprocess.cpp | 3 ++ .../coordinators/connectionuicontroller.cpp | 5 ++- presentation/main/mainwindow.cpp | 5 +++ resource/mainwindow-dark.qss | 2 +- resource/mainwindow-light.qss | 2 +- tests/connectionsession_test.cpp | 34 ++++++++++++++++++- tests/connectionsessionstate_test.cpp | 24 ++++++++++--- tests/coreoutputparser_test.cpp | 1 + 14 files changed, 93 insertions(+), 14 deletions(-) diff --git a/application/connectionsession.cpp b/application/connectionsession.cpp index 0d72b94..29a733c 100644 --- a/application/connectionsession.cpp +++ b/application/connectionsession.cpp @@ -21,7 +21,11 @@ ConnectionSession::ConnectionSession(CoreProcess *coreProcess, QObject *parent) }); connect(coreProcess, &CoreProcess::started, this, [this]() { - sessionState.processStarted(); + emit stateChanged(sessionState.state()); + }); + connect(coreProcess, &CoreProcess::connectionEstablished, this, [this]() + { + sessionState.connectionEstablished(); emit stateChanged(sessionState.state()); }); connect(coreProcess, &CoreProcess::finished, diff --git a/application/coreprocess.h b/application/coreprocess.h index 81eabbf..c0b3c09 100644 --- a/application/coreprocess.h +++ b/application/coreprocess.h @@ -30,6 +30,7 @@ signals: void ssoAuth(); void askSudoPass(); void started(); + void connectionEstablished(); void finished(); }; diff --git a/core/connectionsessionstate.cpp b/core/connectionsessionstate.cpp index d0c45cc..e9e2e16 100644 --- a/core/connectionsessionstate.cpp +++ b/core/connectionsessionstate.cpp @@ -42,7 +42,7 @@ bool ConnectionSessionState::requestStart(const ReconnectPolicy &policy) return true; } -void ConnectionSessionState::processStarted() +void ConnectionSessionState::connectionEstablished() { if (currentState == ConnectionState::Starting) { @@ -76,6 +76,7 @@ bool ConnectionSessionState::requestStop() ProcessFinishAction ConnectionSessionState::processFinished() { + const bool connectionWasEstablished = currentState == ConnectionState::Running; if (desiredConnected && reconnectPolicy.enabled && isReconnectable(currentError)) @@ -85,9 +86,16 @@ ProcessFinishAction ConnectionSessionState::processFinished() } desiredConnected = false; - currentState = currentError == ZJU_ERROR::NONE - ? ConnectionState::Disconnected - : ConnectionState::Failed; + if (connectionWasEstablished) + { + currentState = ConnectionState::Interrupted; + } + else + { + currentState = currentError == ZJU_ERROR::NONE + ? ConnectionState::Disconnected + : ConnectionState::Failed; + } return ProcessFinishAction::Complete; } diff --git a/core/connectionsessionstate.h b/core/connectionsessionstate.h index 8b4d213..7715310 100644 --- a/core/connectionsessionstate.h +++ b/core/connectionsessionstate.h @@ -10,6 +10,7 @@ enum class ConnectionState Running, Stopping, Reconnecting, + Interrupted, Failed, }; @@ -35,7 +36,7 @@ public: int reconnectDelayMs() const; bool requestStart(const ReconnectPolicy &policy); - void processStarted(); + void connectionEstablished(); void recordError(ZJU_ERROR error); bool requestStop(); ProcessFinishAction processFinished(); diff --git a/infrastructure/coreprocess/coreoutputparser.cpp b/infrastructure/coreprocess/coreoutputparser.cpp index 5f0d41b..f7fd65e 100644 --- a/infrastructure/coreprocess/coreoutputparser.cpp +++ b/infrastructure/coreprocess/coreoutputparser.cpp @@ -26,6 +26,10 @@ CoreOutputEvent CoreOutputParser::parse(const QString &output) { return CoreOutputEvent::SsoCallback; } + if (output.contains("VPN client started")) + { + return CoreOutputEvent::ClientStarted; + } if (output.contains("graph check code still required after second login attempt") || output.contains("The characters are incorrect or has expired!")) { diff --git a/infrastructure/coreprocess/coreoutputparser.h b/infrastructure/coreprocess/coreoutputparser.h index 108f979..b628b7a 100644 --- a/infrastructure/coreprocess/coreoutputparser.h +++ b/infrastructure/coreprocess/coreoutputparser.h @@ -13,6 +13,7 @@ enum class CoreOutputEvent SmsCode, TotpCode, SsoCallback, + ClientStarted, CaptchaFailed, AccessDenied, ListenFailed, diff --git a/infrastructure/coreprocess/zjuconnectprocess.cpp b/infrastructure/coreprocess/zjuconnectprocess.cpp index 16d1e96..1f95fa9 100644 --- a/infrastructure/coreprocess/zjuconnectprocess.cpp +++ b/infrastructure/coreprocess/zjuconnectprocess.cpp @@ -104,6 +104,9 @@ void ZjuConnectProcess::processOutputLines(const QList &lines) case CoreOutputEvent::SsoCallback: emit ssoAuth(); break; + case CoreOutputEvent::ClientStarted: + emit connectionEstablished(); + break; case CoreOutputEvent::CaptchaFailed: emit error(ZJU_ERROR::CAPTCHA_FAILED); break; diff --git a/presentation/coordinators/connectionuicontroller.cpp b/presentation/coordinators/connectionuicontroller.cpp index 7d7e650..e2155d7 100644 --- a/presentation/coordinators/connectionuicontroller.cpp +++ b/presentation/coordinators/connectionuicontroller.cpp @@ -133,7 +133,10 @@ ConnectionUiController::ConnectionUiController( this->connectButton->setText("连接服务器"); this->trayConnectAction->setText("连接服务器"); this->proxyButton->hide(); - showConnectionError(error); + if (this->connectionSession->state() != ConnectionState::Interrupted) + { + showConnectionError(error); + } } ); connect( diff --git a/presentation/main/mainwindow.cpp b/presentation/main/mainwindow.cpp index a56f325..a7f4db4 100644 --- a/presentation/main/mainwindow.cpp +++ b/presentation/main/mainwindow.cpp @@ -482,6 +482,11 @@ void MainWindow::updateConnectionState(ConnectionState state) title = "正在重连"; detail = "连接中断,正在按当前策略重新尝试。"; break; + case ConnectionState::Interrupted: + propertyValue = "failed"; + title = "连接已断开"; + detail = "VPN 核心意外退出,请查看右侧日志。"; + break; case ConnectionState::Failed: propertyValue = "failed"; title = "连接失败"; diff --git a/resource/mainwindow-dark.qss b/resource/mainwindow-dark.qss index da820c9..99af5a2 100644 --- a/resource/mainwindow-dark.qss +++ b/resource/mainwindow-dark.qss @@ -76,7 +76,7 @@ QMenu::item:selected { } #statusIndicator[connectionState="running"] { - background: #55b9e7; + background: #5acb83; } #statusIndicator[connectionState="stopping"] { diff --git a/resource/mainwindow-light.qss b/resource/mainwindow-light.qss index 93dae3c..7ff2641 100644 --- a/resource/mainwindow-light.qss +++ b/resource/mainwindow-light.qss @@ -76,7 +76,7 @@ QMenu::item:selected { } #statusIndicator[connectionState="running"] { - background: #278bb9; + background: #2e8b57; } #statusIndicator[connectionState="stopping"] { diff --git a/tests/connectionsession_test.cpp b/tests/connectionsession_test.cpp index 8079303..d2190d9 100644 --- a/tests/connectionsession_test.cpp +++ b/tests/connectionsession_test.cpp @@ -33,6 +33,11 @@ public: emit finished(); } + void establishConnection() + { + emit connectionEstablished(); + } + void requestSudoPassword() { emit askSudoPass(); @@ -53,7 +58,7 @@ bool delegatesProcessLifecycleThroughPort() if (!session.start(profile, {}) || coreProcess->startCalls != 1 - || session.state() != ConnectionState::Running + || session.state() != ConnectionState::Starting || !session.isActive() || session.start(profile, {})) { @@ -61,6 +66,13 @@ bool delegatesProcessLifecycleThroughPort() return false; } + coreProcess->establishConnection(); + if (session.state() != ConnectionState::Running) + { + qCritical() << "connection establishment was not reflected"; + return false; + } + session.submitInput("input"); session.stop(); if (coreProcess->lastInput != "input" @@ -82,6 +94,25 @@ bool delegatesProcessLifecycleThroughPort() return true; } +bool distinguishesInterruptedConnectionFromStartupFailure() +{ + auto *coreProcess = new FakeCoreProcess(); + ConnectionSession session(coreProcess); + ConnectionProfile profile; + + session.start(profile, {}); + coreProcess->establishConnection(); + emit coreProcess->error(ZJU_ERROR::OTHER); + coreProcess->complete(); + + if (session.state() != ConnectionState::Interrupted) + { + qCritical() << "established connection was reported as startup failure"; + return false; + } + return true; +} + bool emptySudoPasswordStopsTheSession() { auto *coreProcess = new FakeCoreProcess(); @@ -133,6 +164,7 @@ int main(int argc, char *argv[]) { QCoreApplication application(argc, argv); return delegatesProcessLifecycleThroughPort() + && distinguishesInterruptedConnectionFromStartupFailure() && emptySudoPasswordStopsTheSession() && cancelledInteractiveInputSubmitsNewlineBeforeStopping() ? 0 : 1; } diff --git a/tests/connectionsessionstate_test.cpp b/tests/connectionsessionstate_test.cpp index ef19f7a..3ac5d05 100644 --- a/tests/connectionsessionstate_test.cpp +++ b/tests/connectionsessionstate_test.cpp @@ -16,7 +16,7 @@ bool normalLifecycle() return false; } - session.processStarted(); + session.connectionEstablished(); if (session.state() != ConnectionState::Running) { qCritical() << "normalLifecycle failed at running"; @@ -43,7 +43,7 @@ bool reconnectsOnlyEligibleFailures() { ConnectionSessionState session; session.requestStart({true, 2500}); - session.processStarted(); + session.connectionEstablished(); session.recordError(ZJU_ERROR::AUTH_EXPIRED); if (session.processFinished() != ProcessFinishAction::Reconnect @@ -61,7 +61,6 @@ bool reconnectsOnlyEligibleFailures() return false; } - session.processStarted(); session.recordError(ZJU_ERROR::INVALID_DETAIL); if (session.processFinished() != ProcessFinishAction::Complete || session.state() != ConnectionState::Failed) @@ -76,7 +75,7 @@ bool keepsFirstErrorAndCancelsPendingReconnect() { ConnectionSessionState session; session.requestStart({true, 1000}); - session.processStarted(); + session.connectionEstablished(); session.recordError(ZJU_ERROR::AUTH_EXPIRED); session.recordError(ZJU_ERROR::OTHER); if (session.error() != ZJU_ERROR::AUTH_EXPIRED) @@ -95,6 +94,22 @@ bool keepsFirstErrorAndCancelsPendingReconnect() } return true; } + +bool establishedConnectionEndsAsInterrupted() +{ + ConnectionSessionState session; + session.requestStart({false, 1000}); + session.connectionEstablished(); + session.recordError(ZJU_ERROR::OTHER); + + if (session.processFinished() != ProcessFinishAction::Complete + || session.state() != ConnectionState::Interrupted) + { + qCritical() << "establishedConnectionEndsAsInterrupted failed"; + return false; + } + return true; +} } int main(int argc, char *argv[]) @@ -103,6 +118,7 @@ int main(int argc, char *argv[]) return normalLifecycle() && reconnectsOnlyEligibleFailures() && keepsFirstErrorAndCancelsPendingReconnect() + && establishedConnectionEndsAsInterrupted() ? 0 : 1; } diff --git a/tests/coreoutputparser_test.cpp b/tests/coreoutputparser_test.cpp index 1368a06..5e82d92 100644 --- a/tests/coreoutputparser_test.cpp +++ b/tests/coreoutputparser_test.cpp @@ -21,6 +21,7 @@ bool recognizesCoreProtocol() {"Please enter your SMS code:", CoreOutputEvent::SmsCode}, {"Please enter your TOTP code:", CoreOutputEvent::TotpCode}, {"Please enter the callback url:", CoreOutputEvent::SsoCallback}, + {"VPN client started", CoreOutputEvent::ClientStarted}, {"graph check code still required after second login attempt", CoreOutputEvent::CaptchaFailed}, {"Access is denied.", CoreOutputEvent::AccessDenied}, {"listen failed on 127.0.0.1", CoreOutputEvent::ListenFailed},