From 1f4216e1ca12977a6fccdee65e51a9a1bb8bfd40 Mon Sep 17 00:00:00 2001 From: donghualin Date: Fri, 26 Jun 2026 17:12:53 +0800 Subject: [PATCH] fix(network): prevent ping-pong switching in multi-NIC scenario Replace m_isSwitching flag with a cooldown timer in InternetChecker to avoid frequent route switching. The cooldown starts at the beginning of switchInternetAccess() and lasts for connectivityCheckInterval, covering both the switching process and post-switch stabilization. Add a proactive accessibility check at the entry of switchInternetAccess() to verify the primary connection before initiating a switch, skipping unnecessary switching when the current connection is already functional. Restore realStartCheck() to its original structure, removing the snapshot logic (m_checkStartPrimaryId) that checked for primary connection changes during the check, since the proactive check in InternetChecker handles this case more cleanly. Also fix reapplyConnection to pass the correct flags parameter. PMS:BUG-351163 --- config/org.deepin.dde.network.json | 11 +++ .../src/system/connectivitychecker.cpp | 73 ++++++++++--------- .../src/system/connectivitychecker.h | 3 +- .../src/system/internetchecker.cpp | 29 ++++++-- .../src/system/internetchecker.h | 2 +- .../src/utils/httpmanager.cpp | 37 ++++++---- .../src/utils/settingconfig.cpp | 17 +++++ .../src/utils/settingconfig.h | 2 + 8 files changed, 116 insertions(+), 58 deletions(-) diff --git a/config/org.deepin.dde.network.json b/config/org.deepin.dde.network.json index 4139cf4c..d12f4d09 100644 --- a/config/org.deepin.dde.network.json +++ b/config/org.deepin.dde.network.json @@ -349,6 +349,17 @@ "description":"if check the network is unavailable and then switch to the available network", "permissions":"readwrite", "visibility":"private" + }, + "reapplyFlags":{ + "value":2, + "serial":0, + "flags":["global"], + "name":"reapplyFlags", + "name[zh_CN]":"reapplyConnection标志位", + "description[zh_CN]":"reapplyConnection的标志位,只能是0、1、2三个值之一。0:无标志;1:保留外部配置(PRESERVE_EXTERNAL);2:非阻塞(NONBLOCKING)", + "description":"Flags for reapplyConnection, must be 0, 1 or 2. 0: none; 1: PRESERVE_EXTERNAL; 2: NONBLOCKING", + "permissions":"readwrite", + "visibility":"private" } } } diff --git a/network-service-plugin/src/system/connectivitychecker.cpp b/network-service-plugin/src/system/connectivitychecker.cpp index f22a7937..086843ca 100755 --- a/network-service-plugin/src/system/connectivitychecker.cpp +++ b/network-service-plugin/src/system/connectivitychecker.cpp @@ -94,8 +94,10 @@ void LocalConnectionvityChecker::onConnectivityChanged(network::service::Connect if (m_internetChecker && (connectivity == network::service::Connectivity::Limited || connectivity == network::service::Connectivity::Noconnectivity || connectivity == network::service::Connectivity::Unknownconnectivity)) { - // 在开启网络检测的情况下,如果当前网络不可用,则启动切换主连接的工作流程,尝试恢复网络连通性。 - QMetaObject::invokeMethod(m_internetChecker, &InternetChecker::switchInternetAccess, Qt::QueuedConnection); + bool checkPrimary = m_statusChecker->primaryConnectionChanged(); + QMetaObject::invokeMethod(m_internetChecker, [this, checkPrimary]() { + m_internetChecker->switchInternetAccess(checkPrimary); + }, Qt::QueuedConnection); } qCInfo(DSM) << "Connectivity changed, incomming: " << static_cast(connectivity) << ", current: " << static_cast(m_connectivity); if (m_connectivity == connectivity) @@ -115,6 +117,7 @@ StatusChecker::StatusChecker(QObject *parent) , m_connectivity(network::service::Connectivity::Unknownconnectivity) , m_checkCount(0) , m_isStop(false) + , m_primaryConnectionChanged(false) { qRegisterMetaType("NetworkManager::ActiveConnection::State"); qRegisterMetaType("NetworkManager::Device::State"); @@ -179,6 +182,11 @@ QString StatusChecker::detectionConnectionId() const return m_primaryId; } +bool StatusChecker::primaryConnectionChanged() const +{ + return m_primaryConnectionChanged; +} + void StatusChecker::initDeviceConnect(const QList> &devices) { for (const QSharedPointer &device : devices) { @@ -234,6 +242,12 @@ void StatusChecker::initConnectivityChecker() void StatusChecker::stop() { m_isStop = true; + if (m_checkTimer && m_checkTimer->isActive()) + m_checkTimer->stop(); + if (m_timer && m_timer->isActive()) + m_timer->stop(); + if (m_pendingCheckTimer && m_pendingCheckTimer->isActive()) + m_pendingCheckTimer->stop(); } void StatusChecker::onUpdataActiveState(const QSharedPointer &networks) @@ -261,17 +275,11 @@ void StatusChecker::realStartCheck() return; } - NetworkManager::ActiveConnection::Ptr pConnection = NetworkManager::primaryConnection(); - if (!pConnection.isNull()) { - m_primaryId = pConnection->connection()->uuid(); - m_checkStartPrimaryId = m_primaryId; - } else { - m_checkStartPrimaryId.clear(); - } + NetworkManager::ActiveConnection::Ptr prePrimary = NetworkManager::primaryConnection(); + QString prePrimaryId = !prePrimary.isNull() ? prePrimary->connection()->uuid() : QString(); + int httpTimeout = SettingConfig::instance()->httpRequestTimeout(); bool networkIsOk = false; - network::service::Connectivity detectedConnectivity = m_connectivity; - QString detectedPortalUrl; for (const QString &url : m_checkUrls) { network::service::HttpManager http; network::service::HttpReply *httpReply = http.get(url, httpTimeout); @@ -280,41 +288,35 @@ void StatusChecker::realStartCheck() break; } if (httpReply->isTimeout()) { - qCWarning(DSM) << "check network time out, network is unavaibled "; - detectedConnectivity = network::service::Connectivity::Limited; + // 如果是读取超时了,则无需进行第二次检测,一般超时的情况下基本上都是网络不通 + qCWarning(DSM) << "request network timeout"; break; } int httpCode = httpReply->httpCode(); - QString portalUrl = httpReply->portal(); if (httpCode == 0) { qCWarning(DSM) << "Nework is unreachabel:" << url << httpReply->errorMessage(); - detectedConnectivity = network::service::Connectivity::Limited; continue; } + + QString portalUrl = httpReply->portal(); networkIsOk = true; qCDebug(DSM) << "Http reply code:" << httpCode << ", portal url:" << portalUrl; - detectedPortalUrl = portalUrl; - detectedConnectivity = portalUrl.isEmpty() ? network::service::Connectivity::Full : network::service::Connectivity::Portal; + if (portalUrl.isEmpty()) { + // if the portal is empty, I think it ok + setConnectivity(network::service::Connectivity::Full); + } else { + setConnectivity(network::service::Connectivity::Portal); + } + setPortalUrl(portalUrl); break; } - if (m_isStop) - return; - - // 主连接在检查期间可能已被 InternetChecker 切换,此时检测结果基于旧路由,必须丢弃 - NetworkManager::ActiveConnection::Ptr currentPrimary = NetworkManager::primaryConnection(); - QString currentPrimaryId; - if (!currentPrimary.isNull()) - currentPrimaryId = currentPrimary->connection()->uuid(); - if (m_checkStartPrimaryId != currentPrimaryId) { - qCInfo(DSM) << "Primary connection changed during check, discarding stale result" - << "check:" << m_checkStartPrimaryId << "current:" << currentPrimaryId; - return; + // HTTP 检查完成后,再获取当前主连接,以检测检查期间是否发生了连接切换 + NetworkManager::ActiveConnection::Ptr pConnection = NetworkManager::primaryConnection(); + if (!pConnection.isNull()) { + m_primaryId = pConnection->connection()->uuid(); } - - if (networkIsOk) { - setConnectivity(detectedConnectivity); - setPortalUrl(detectedPortalUrl); - } else { + m_primaryConnectionChanged = (prePrimaryId != m_primaryId); + if (!m_isStop && !networkIsOk) { NetworkManager::Device::List devices = NetworkManager::networkInterfaces(); int disconnectCount = 0; for (NetworkManager::Device::Ptr device : devices) { @@ -324,13 +326,12 @@ void StatusChecker::realStartCheck() } } qCDebug(DSM) << "Network is unreachabel, disconnect count:" << disconnectCount; + setPortalUrl(QString()); if (disconnectCount == devices.size()) { - // 如果所有的网络设备都断开了,就默认让其变为断开的状态 setConnectivity(network::service::Connectivity::Noconnectivity); } else { setConnectivity(network::service::Connectivity::Limited); } - setPortalUrl(QString()); } } diff --git a/network-service-plugin/src/system/connectivitychecker.h b/network-service-plugin/src/system/connectivitychecker.h index 0e5b73e4..f00a1e13 100755 --- a/network-service-plugin/src/system/connectivitychecker.h +++ b/network-service-plugin/src/system/connectivitychecker.h @@ -83,6 +83,7 @@ class StatusChecker : public QObject QString portalUrl() const; void checkConnectivity(); QString detectionConnectionId() const; + bool primaryConnectionChanged() const; signals: void portalDetected(const QString &); @@ -113,7 +114,7 @@ private slots: QStringList m_checkUrls; bool m_isStop; QString m_primaryId; - QString m_checkStartPrimaryId; + bool m_primaryConnectionChanged; }; class NMConnectionvityChecker : public ConnectivityChecker diff --git a/network-service-plugin/src/system/internetchecker.cpp b/network-service-plugin/src/system/internetchecker.cpp index ed3fb4ec..442d50d2 100644 --- a/network-service-plugin/src/system/internetchecker.cpp +++ b/network-service-plugin/src/system/internetchecker.cpp @@ -10,8 +10,8 @@ #include #include -#include #include +#include #include #include @@ -79,8 +79,9 @@ bool InternetChecker::setConnectionNeverDefault(const NetworkManager::Connection } if (changed) { conn->updateUnsaved(settingsMap); - if (!device.isNull()) - device->reapplyConnection(conn->settings()->toMap(), 0, 0); + if (!device.isNull()) { + device->reapplyConnection(conn->settings()->toMap(), 0, SettingConfig::instance()->reapplyFlags()); + } qCInfo(DSM) << "Set connection" << conn->name() << "never-default to" << neverDefault; } return changed; @@ -132,7 +133,11 @@ bool InternetChecker::checkInternetAccessibleWithRetry(int maxRetry) const int httpTimeout = SettingConfig::instance()->httpRequestTimeout(); for (int i = 0; i < maxRetry; ++i) { // 每次检测前等待1秒,让NM路由表和DHCP先稳定 - QThread::sleep(1); + { + QEventLoop loop; + QTimer::singleShot(1000, &loop, &QEventLoop::quit); + loop.exec(); + } bool timedOut = false; // 首次使用20秒超时,避免在无网线路由器的环境中长时间阻塞 QElapsedTimer timer; @@ -216,7 +221,7 @@ void InternetChecker::onPrimaryConnectionTimeout() } } -void InternetChecker::switchInternetAccess() +void InternetChecker::switchInternetAccess(bool checkPrimaryConnection) { // 正在切换中,忽略新的切换请求 if (m_isSwitching) { @@ -224,6 +229,19 @@ void InternetChecker::switchInternetAccess() return; } + qCDebug(DSM) << "need check primary connection" << checkPrimaryConnection; + // 主连接在检测期间发生变化,需要重新检查当前主连接是否可上网 + NetworkManager::ActiveConnection::Ptr primaryConnection = NetworkManager::primaryConnection(); + if (checkPrimaryConnection && !primaryConnection.isNull()) { + int httpTimeout = SettingConfig::instance()->httpRequestTimeout(); + bool isTimeout = false; + if (checkInternetAccessible(httpTimeout, isTimeout)) { + qCInfo(DSM) << "Primary connection is accessible, skip switching"; + emit switchSuccess(); + return; + } + } + m_isSwitching = true; NetworkManager::Device::List availableDevice; NetworkManager::Device::List devices = NetworkManager::networkInterfaces(); @@ -244,7 +262,6 @@ void InternetChecker::switchInternetAccess() return; } - NetworkManager::ActiveConnection::Ptr primaryConnection = NetworkManager::primaryConnection(); if (primaryConnection.isNull()) { qCWarning(DSM) << "primary connection is null, it will reset all never default"; resetAllNeverDefault(); diff --git a/network-service-plugin/src/system/internetchecker.h b/network-service-plugin/src/system/internetchecker.h index 50b60a32..d47abd96 100644 --- a/network-service-plugin/src/system/internetchecker.h +++ b/network-service-plugin/src/system/internetchecker.h @@ -21,7 +21,7 @@ class InternetChecker : public QObject public: explicit InternetChecker(QObject *parent = nullptr); ~InternetChecker() override; - void switchInternetAccess(); + void switchInternetAccess(bool checkPrimaryConnection = false); signals: void switchSuccess(); diff --git a/network-service-plugin/src/utils/httpmanager.cpp b/network-service-plugin/src/utils/httpmanager.cpp index bd4d9d8a..13919316 100755 --- a/network-service-plugin/src/utils/httpmanager.cpp +++ b/network-service-plugin/src/utils/httpmanager.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -23,34 +24,40 @@ using namespace network::service; namespace { struct GetAddrInfoParams { - const char *node; - const char *service; + std::string node; + std::string service; const addrinfo *hints; addrinfo *result = nullptr; int ret = 0; + std::atomic delete_by_thread{false}; }; static void *getaddrinfo_thread(void *arg) { - auto *params = static_cast(arg); - params->ret = getaddrinfo(params->node, params->service, params->hints, ¶ms->result); + GetAddrInfoParams *params = static_cast(arg); + params->ret = getaddrinfo(params->node.c_str(), params->service.c_str(), params->hints, ¶ms->result); + // deleteByThread 为 true 说明主线程已 detach,由本线程释放 params + if (params->delete_by_thread.load()) + delete params; return nullptr; } // 在独立线程中执行 getaddrinfo,主线程等待 timeoutMs 毫秒 // 如果超时返回 ETIMEDOUT,成功返回 0 -static int getaddrinfo_with_timeout(const char *node, const char *service, +static int getaddrinfo_with_timeout(const std::string &node, const std::string &service, const addrinfo *hints, addrinfo **result, int timeoutMs) { - GetAddrInfoParams params = {node, service, hints, nullptr, 0}; + // 在堆上分配 params,防止 detach 的线程在超时后写入已释放的栈内存 + GetAddrInfoParams *params = new GetAddrInfoParams{node, service, hints, nullptr, 0}; pthread_t thread; pthread_attr_t attr; pthread_attr_init(&attr); pthread_attr_setdetachstate(&attr, PTHREAD_CREATE_JOINABLE); - if (pthread_create(&thread, &attr, getaddrinfo_thread, ¶ms) != 0) { + if (pthread_create(&thread, &attr, getaddrinfo_thread, params) != 0) { pthread_attr_destroy(&attr); + delete params; return EAI_SYSTEM; } pthread_attr_destroy(&attr); @@ -67,13 +74,16 @@ static int getaddrinfo_with_timeout(const char *node, const char *service, void *threadRet = nullptr; int joinRet = pthread_timedjoin_np(thread, &threadRet, &ts); if (joinRet == ETIMEDOUT) { - // 线程超时,detach 让其自行结束(getaddrinfo 最终会返回) + // 通知子线程由其负责释放 params + params->delete_by_thread.store(true); pthread_detach(thread); return ETIMEDOUT; } - *result = params.result; - return params.ret; + *result = params->result; + int ret = params->ret; + delete params; + return ret; } } // anonymous namespace @@ -196,11 +206,10 @@ HttpReply *HttpManager::get(const QString &url, int timeoutSec) // getaddrinfo 本身会调用系统 DNS,如果路由器不通,glibc 会等待很久 // 用独立线程包装,只等待 timeoutSec 秒 - // 注意:必须用局部变量持有 std::string,确保 getaddrinfo_with_timeout - // 返回前字符串生命周期有效,否则子线程会使用悬垂指针 + // std::string 存入 GetAddrInfoParams 后独立持有数据,不依赖栈生命周期 std::string hostStd = host.toStdString(); - int ret = getaddrinfo_with_timeout(hostStd.c_str(), - nullptr, + int ret = getaddrinfo_with_timeout(hostStd, + {}, &hints, &result, timeoutSec * 1000); diff --git a/network-service-plugin/src/utils/settingconfig.cpp b/network-service-plugin/src/utils/settingconfig.cpp index 3e7b01a0..39bb92e1 100644 --- a/network-service-plugin/src/utils/settingconfig.cpp +++ b/network-service-plugin/src/utils/settingconfig.cpp @@ -89,6 +89,11 @@ bool SettingConfig::needCheckNetwork() const return m_needCheckNetwork; } +int SettingConfig::reapplyFlags() const +{ + return m_reapplyFlags; +} + void SettingConfig::onValueChanged(const QString &key) { if (key == "reconnectIfIpConflicted") { @@ -120,6 +125,9 @@ void SettingConfig::onValueChanged(const QString &key) m_protalProcessMode = dConfig->value("portalProcessMode").toString(); } else if (key == QString("needCheckNetwork")) { m_needCheckNetwork = dConfig->value("needCheckNetwork").toBool(); + } else if (key == QString("reapplyFlags")) { + int val = dConfig->value("reapplyFlags").toInt(); + m_reapplyFlags = (val >= 0 && val <= 2) ? val : 2; } } @@ -134,6 +142,7 @@ SettingConfig::SettingConfig(QObject *parent) , m_disableFailureNotify(false) , m_resetWifiOSDEnableTimeout(300) , m_needCheckNetwork(true) + , m_reapplyFlags(2) { if (!dConfig) dConfig = Dtk::Core::DConfig::create("org.deepin.dde.network", "org.deepin.dde.network"); @@ -179,6 +188,14 @@ SettingConfig::SettingConfig(QObject *parent) m_needCheckNetwork = dConfig->value("needCheckNetwork").toBool(); m_disableFailureNotify = dConfig->value("disableFailureNotify", false).toBool(); + + if (keys.contains("reapplyFlags")) { + int val = dConfig->value("reapplyFlags").toInt(); + if (val >= 0 && val <= 2) + m_reapplyFlags = val; + else + m_reapplyFlags = 2; + } } if (m_networkUrls.isEmpty()) m_networkUrls = CheckUrls; diff --git a/network-service-plugin/src/utils/settingconfig.h b/network-service-plugin/src/utils/settingconfig.h index 4eda3b98..ca2e341f 100644 --- a/network-service-plugin/src/utils/settingconfig.h +++ b/network-service-plugin/src/utils/settingconfig.h @@ -29,6 +29,7 @@ class SettingConfig : public QObject bool supportAutoOpenPortal() const; // 是否支持自动打开网页 bool supportPortalPromp() const; // 是否支持在任务栏或控制中心给出提示 bool needCheckNetwork() const; // 是否需要检测网络 + int reapplyFlags() const; signals: void enableConnectivityChanged(bool); @@ -57,6 +58,7 @@ private slots: int m_httpRequestTimeout; int m_httpConnectTimeout; bool m_needCheckNetwork; + int m_reapplyFlags; }; #endif // SERVICE_H