Skip to content

Commit 3a36091

Browse files
committed
fix: add per-device internet connectivity check for network switching
Add InternetChecker module to detect and switch the primary network interface when the current one cannot access the internet. - Add InternetChecker class with switchInternetAccess() logic: traverse activated devices, check which ones can access internet, compare with current primary connection, and switch via never-default (updateUnsaved + reapplyConnection, not persisted to disk) - Subscribe to NetworkManager::primaryConnectionChanged signal, retry connectivity checks after switch to wait for DHCP/route settling - Reset all connections' never-default=false after successful switch or when all devices exhausted (via NetworkManager::Settings::listConnections + device active connections) - Add needCheckNetwork config option to enable/disable the feature - Integrate InternetChecker into LocalConnectionvityChecker, trigger on Limited/Noconnectivity/Unknownconnectivity states 增加检测单独网卡是否可以上网并自动切换网络的功能。 - 新增InternetChecker类,实现switchInternetAccess()逻辑:遍历所有已激活的网卡, 检测哪些网卡可以上网,与当前主连接网卡对比,若当前网卡不可上网则通过 never-default属性切换默认路由(使用updateUnsaved+reapplyConnection, 不持久化到磁盘) - 监听NetworkManager::primaryConnectionChanged信号,切换后重试检测网络连通性, 等待DHCP/路由生效 - 切换成功后或所有设备都尝试过以后,重置所有连接的never-default为false (通过NetworkManager::Settings::listConnections遍历所有已保存连接, 并补充设备当前激活的连接,确保不遗漏) - 新增needCheckNetwork配置项控制是否启用该功能 - 在LocalConnectionvityChecker中集成InternetChecker,在网络状态变为 Limited/Noconnectivity/Unknownconnectivity时触发检测和切换 PMS: BUG-351163
1 parent b02842b commit 3a36091

9 files changed

Lines changed: 579 additions & 2 deletions

File tree

config/org.deepin.dde.network.json

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -338,6 +338,17 @@
338338
"description":"the span to check connectivity(unit:second) when limit",
339339
"permissions":"readwrite",
340340
"visibility":"private"
341+
},
342+
"needCheckNetwork":{
343+
"value":true,
344+
"serial":0,
345+
"flags":["global"],
346+
"name":"needCheckNetwork",
347+
"name[zh_CN]":"是否检测网络可用性并切换网络",
348+
"description[zh_CN]":"是否检测网络可用性并切换网络",
349+
"description":"if check the network is unavailable and then switch to the available network",
350+
"permissions":"readwrite",
351+
"visibility":"private"
341352
}
342353
}
343354
}

network-service-plugin/src/system/connectivitychecker.cpp

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
// SPDX-License-Identifier: GPL-3.0-or-later
44

55
#include "connectivitychecker.h"
6+
#include "internetchecker.h"
67

78
#include "settingconfig.h"
89
#include "httpmanager.h"
@@ -27,12 +28,22 @@ LocalConnectionvityChecker::LocalConnectionvityChecker(QObject *parent)
2728
, m_statusChecker(new StatusChecker)
2829
, m_connectivity(network::service::Connectivity::Unknownconnectivity)
2930
, m_thread(new QThread)
31+
, m_internetCheckerThread(nullptr)
32+
, m_internetChecker(nullptr)
3033
{
3134
m_statusChecker->moveToThread(m_thread);
3235
connect(m_statusChecker, &StatusChecker::portalDetected, this, &LocalConnectionvityChecker::onPortalDetected);
3336
connect(m_statusChecker, &StatusChecker::connectivityChanged, this, &LocalConnectionvityChecker::onConnectivityChanged);
3437
m_thread->start();
3538
QMetaObject::invokeMethod(m_statusChecker, &StatusChecker::initConnectivityChecker, Qt::QueuedConnection);
39+
if (SettingConfig::instance()->needCheckNetwork()) {
40+
m_internetCheckerThread = new QThread(this);
41+
m_internetChecker = new InternetChecker;
42+
m_internetChecker->moveToThread(m_internetCheckerThread);
43+
m_internetCheckerThread->start();
44+
connect(m_internetChecker, &InternetChecker::switchSuccess, m_statusChecker, &StatusChecker::checkConnectivity);
45+
connect(m_internetCheckerThread, &QThread::finished, m_internetChecker, &QObject::deleteLater);
46+
}
3647
}
3748

3849
LocalConnectionvityChecker::~LocalConnectionvityChecker()
@@ -42,6 +53,11 @@ LocalConnectionvityChecker::~LocalConnectionvityChecker()
4253
m_thread->wait();
4354
m_statusChecker->deleteLater();
4455
m_thread->deleteLater();
56+
if (m_internetCheckerThread) {
57+
m_internetCheckerThread->quit();
58+
m_internetCheckerThread->wait();
59+
m_internetCheckerThread->deleteLater();
60+
}
4561
}
4662

4763
network::service::Connectivity LocalConnectionvityChecker::connectivity() const
@@ -75,6 +91,12 @@ void LocalConnectionvityChecker::onPortalDetected(const QString &portalUrl)
7591

7692
void LocalConnectionvityChecker::onConnectivityChanged(network::service::Connectivity connectivity)
7793
{
94+
if (m_internetChecker && (connectivity == network::service::Connectivity::Limited
95+
|| connectivity == network::service::Connectivity::Noconnectivity
96+
|| connectivity == network::service::Connectivity::Unknownconnectivity)) {
97+
// 在开启网络检测的情况下,如果当前网络不可用,则启动切换主连接的工作流程,尝试恢复网络连通性。
98+
QMetaObject::invokeMethod(m_internetChecker, &InternetChecker::switchInternetAccess, Qt::QueuedConnection);
99+
}
78100
qCInfo(DSM) << "Connectivity changed, incomming: " << static_cast<int>(connectivity) << ", current: " << static_cast<int>(m_connectivity);
79101
if (m_connectivity == connectivity)
80102
return;

network-service-plugin/src/system/connectivitychecker.h

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ namespace network {
2020
namespace systemservice {
2121

2222
class StatusChecker;
23+
class InternetChecker;
2324

2425
// 网络连通性的检测
2526
class ConnectivityChecker : public QObject
@@ -64,6 +65,8 @@ private slots:
6465
QString m_portalUrl;
6566
network::service::Connectivity m_connectivity;
6667
QThread *m_thread;
68+
QThread *m_internetCheckerThread;
69+
InternetChecker *m_internetChecker;
6770
};
6871

6972
class StatusChecker : public QObject
Lines changed: 298 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,298 @@
1+
// SPDX-FileCopyrightText: 2026 UnionTech Software Technology Co., Ltd.
2+
//
3+
// SPDX-License-Identifier: LGPL-3.0-or-later
4+
5+
#include "internetchecker.h"
6+
7+
#include "httpmanager.h"
8+
#include "settingconfig.h"
9+
#include "constants.h"
10+
11+
#include <QDebug>
12+
#include <QTimer>
13+
#include <QThread>
14+
#include <QElapsedTimer>
15+
16+
#include <NetworkManagerQt/Manager>
17+
#include <NetworkManagerQt/ActiveConnection>
18+
#include <NetworkManagerQt/Ipv4Setting>
19+
#include <NetworkManagerQt/Ipv6Setting>
20+
#include <NetworkManagerQt/Settings>
21+
22+
using namespace network::systemservice;
23+
24+
InternetChecker::InternetChecker(QObject *parent)
25+
: QObject(parent)
26+
, m_tryIndex(0)
27+
, m_switchTimer(nullptr)
28+
, m_isSwitching(false)
29+
{
30+
connect(NetworkManager::notifier(), &NetworkManager::Notifier::primaryConnectionChanged,
31+
this, &InternetChecker::onPrimaryConnectionChanged);
32+
}
33+
34+
InternetChecker::~InternetChecker()
35+
{
36+
if (m_switchTimer) {
37+
m_switchTimer->stop();
38+
m_switchTimer->deleteLater();
39+
}
40+
}
41+
42+
void InternetChecker::resetAllNeverDefault() const
43+
{
44+
const NetworkManager::Device::List devices = NetworkManager::networkInterfaces();
45+
for (const NetworkManager::Device::Ptr &device : devices) {
46+
NetworkManager::ActiveConnection::Ptr activeConn = device->activeConnection();
47+
if (!activeConn.isNull())
48+
setConnectionNeverDefault(activeConn->connection(), device, false);
49+
}
50+
}
51+
52+
bool InternetChecker::setConnectionNeverDefault(const NetworkManager::Connection::Ptr &conn,
53+
const NetworkManager::Device::Ptr &device,
54+
bool neverDefault) const
55+
{
56+
if (conn.isNull())
57+
return false;
58+
59+
NMVariantMapMap settingsMap;
60+
bool changed = false;
61+
62+
NetworkManager::Setting::Ptr ipv4Setting = conn->settings()->setting(NetworkManager::Setting::Ipv4);
63+
if (ipv4Setting) {
64+
NetworkManager::Ipv4Setting::Ptr ipv4 = ipv4Setting.dynamicCast<NetworkManager::Ipv4Setting>();
65+
if (ipv4->neverDefault() != neverDefault) {
66+
ipv4->setNeverDefault(neverDefault);
67+
settingsMap.insert(ipv4->name(), ipv4->toMap());
68+
changed = true;
69+
}
70+
}
71+
NetworkManager::Setting::Ptr ipv6Setting = conn->settings()->setting(NetworkManager::Setting::Ipv6);
72+
if (ipv6Setting) {
73+
NetworkManager::Ipv6Setting::Ptr ipv6 = ipv6Setting.dynamicCast<NetworkManager::Ipv6Setting>();
74+
if (ipv6->neverDefault() != neverDefault) {
75+
ipv6->setNeverDefault(neverDefault);
76+
settingsMap.insert(ipv6->name(), ipv6->toMap());
77+
changed = true;
78+
}
79+
}
80+
if (changed) {
81+
conn->updateUnsaved(settingsMap);
82+
if (!device.isNull())
83+
device->reapplyConnection(conn->settings()->toMap(), 0, 0);
84+
qCInfo(DSM) << "Set connection" << conn->name() << "never-default to" << neverDefault;
85+
}
86+
return changed;
87+
}
88+
89+
void InternetChecker::setPrimaryDeviceNeverDefault(bool neverDefault) const
90+
{
91+
NetworkManager::ActiveConnection::Ptr primaryConn = NetworkManager::primaryConnection();
92+
if (primaryConn.isNull())
93+
return;
94+
95+
const QStringList deviceUnis = primaryConn->devices();
96+
for (const QString &uni : deviceUnis) {
97+
NetworkManager::Device::Ptr device = NetworkManager::findNetworkInterface(uni);
98+
if (!device.isNull())
99+
changeDeviceNeverDefault(device, neverDefault);
100+
}
101+
}
102+
103+
bool InternetChecker::checkInternetAccessible() const
104+
{
105+
bool timedOut = false;
106+
return checkInternetAccessible(0, timedOut);
107+
}
108+
109+
bool InternetChecker::checkInternetAccessible(int timeoutSec, bool &timedOut) const
110+
{
111+
timedOut = false;
112+
113+
const QStringList checkUrls = SettingConfig::instance()->networkCheckerUrls();
114+
for (const QString &url : checkUrls) {
115+
network::service::HttpManager http;
116+
network::service::HttpReply *httpReply = (timeoutSec > 0)
117+
? http.get(url, timeoutSec)
118+
: http.get(url);
119+
if (httpReply->httpCode() != 0) {
120+
return true;
121+
}
122+
if (httpReply->isTimeout()) {
123+
timedOut = true;
124+
break;
125+
}
126+
}
127+
return false;
128+
}
129+
130+
bool InternetChecker::checkInternetAccessibleWithRetry(int maxRetry) const
131+
{
132+
for (int i = 0; i < maxRetry; ++i) {
133+
// 每次检测前等待1秒,让NM路由表和DHCP先稳定
134+
QThread::sleep(1);
135+
bool timedOut = false;
136+
// 首次使用20秒超时,避免在无网线路由器的环境中长时间阻塞
137+
QElapsedTimer timer;
138+
timer.start();
139+
if (checkInternetAccessible((i == 0) ? 20 : 0, timedOut)) {
140+
return true;
141+
}
142+
if (timedOut) {
143+
qCDebug(DSM) << "Request timed out after" << timer.elapsed() << "ms, skipping retries";
144+
return false;
145+
}
146+
}
147+
return false;
148+
}
149+
150+
void InternetChecker::changeDeviceNeverDefault(const NetworkManager::Device::Ptr &device, bool neverDefault) const
151+
{
152+
if (device.isNull())
153+
return;
154+
155+
NetworkManager::ActiveConnection::Ptr activeConn = device->activeConnection();
156+
if (activeConn.isNull())
157+
return;
158+
159+
NetworkManager::Connection::Ptr conn = activeConn->connection();
160+
setConnectionNeverDefault(conn, device, neverDefault);
161+
}
162+
163+
void InternetChecker::onPrimaryConnectionChanged(const QString &uni)
164+
{
165+
Q_UNUSED(uni)
166+
167+
// 停止超时定时器
168+
if (m_switchTimer) {
169+
m_switchTimer->stop();
170+
}
171+
172+
qCInfo(DSM) << "Primary connection changed:" << uni;
173+
// NM已切换主连接,通过默认路由检测整体是否可以上网(重试3次,等待DHCP/路由生效)
174+
if (checkInternetAccessibleWithRetry(3)) {
175+
qCInfo(DSM) << "Found accessible device, switching done";
176+
// 当前主设备已经是能上网的,它的never-default本来就是false,无需额外操作
177+
// 此处不能恢复其他设备never default, 因为如果恢复后,NM 内部会根据每个设备的metric值又将主链接设置回去了,导致无法上网
178+
m_tryIndex = 0;
179+
m_isSwitching = false;
180+
emit switchSuccess();
181+
} else if (m_tryIndex < m_tryDevices.size()) {
182+
// 当前设备不能上网,把当前主设备的never-default设为true,让下一个设备接住默认路由
183+
qCDebug(DSM) << "Current device still cannot access internet, trying next device, index:" << m_tryIndex;
184+
setPrimaryDeviceNeverDefault(true);
185+
NetworkManager::Device::Ptr nextDevice = m_tryDevices.at(m_tryIndex);
186+
changeDeviceNeverDefault(nextDevice, false);
187+
m_tryIndex++;
188+
} else {
189+
// 所有设备都尝试过了,全部恢复
190+
qCInfo(DSM) << "All devices tried, none can access internet, resetting all never-default";
191+
resetAllNeverDefault();
192+
m_tryIndex = 0;
193+
m_isSwitching = false;
194+
emit switchFailed();
195+
}
196+
}
197+
198+
void InternetChecker::onPrimaryConnectionTimeout()
199+
{
200+
// NM在超时时间内没有切换主连接,可能是只有一个可切换的设备
201+
qCWarning(DSM) << "PrimaryConnection change timeout, trying next device manually, index:" << m_tryIndex;
202+
if (m_tryIndex < m_tryDevices.size()) {
203+
// 把当前主设备的never-default设为true,让下一个设备接住默认路由
204+
setPrimaryDeviceNeverDefault(true);
205+
NetworkManager::Device::Ptr nextDevice = m_tryDevices.at(m_tryIndex);
206+
changeDeviceNeverDefault(nextDevice, false);
207+
m_tryIndex++;
208+
// 重置超时定时器
209+
m_switchTimer->start(5000);
210+
} else {
211+
resetAllNeverDefault();
212+
m_tryIndex = 0;
213+
m_isSwitching = false;
214+
emit switchFailed();
215+
}
216+
}
217+
218+
void InternetChecker::switchInternetAccess()
219+
{
220+
// 正在切换中,忽略新的切换请求
221+
if (m_isSwitching) {
222+
qCDebug(DSM) << "current is switching";
223+
return;
224+
}
225+
226+
m_isSwitching = true;
227+
NetworkManager::Device::List availableDevice;
228+
NetworkManager::Device::List devices = NetworkManager::networkInterfaces();
229+
for (const NetworkManager::Device::Ptr &device : devices) {
230+
if (device->type() != NetworkManager::Device::Type::Ethernet
231+
&& device->type() != NetworkManager::Device::Type::Wifi)
232+
continue;
233+
if (device->state() != NetworkManager::Device::State::Activated)
234+
continue;
235+
236+
availableDevice << device;
237+
}
238+
239+
if (availableDevice.size() <= 1) {
240+
qCDebug(DSM) << "only one available device,it will reset all never default";
241+
resetAllNeverDefault();
242+
m_isSwitching = false;
243+
return;
244+
}
245+
246+
NetworkManager::ActiveConnection::Ptr primaryConnection = NetworkManager::primaryConnection();
247+
if (primaryConnection.isNull()) {
248+
qCWarning(DSM) << "primary connection is null, it will reset all never default";
249+
resetAllNeverDefault();
250+
m_isSwitching = false;
251+
return;
252+
}
253+
254+
// 找到当前主连接使用的网卡
255+
QStringList primaryDeviceUnis = primaryConnection->devices();
256+
NetworkManager::Device::Ptr primaryDevice;
257+
for (const NetworkManager::Device::Ptr &device : availableDevice) {
258+
if (primaryDeviceUnis.contains(device->uni())) {
259+
primaryDevice = device;
260+
qWarning() << "get primary device: " << primaryDevice->interfaceName();
261+
break;
262+
}
263+
}
264+
265+
if (primaryDevice.isNull()) {
266+
qWarning() << "primary device is null";
267+
resetAllNeverDefault();
268+
m_isSwitching = false;
269+
return;
270+
}
271+
272+
// 保存需要遍历的设备列表(排除当前主设备)
273+
m_tryDevices.clear();
274+
int primaryIdx = availableDevice.indexOf(primaryDevice);
275+
for (int i = 1; i < availableDevice.size(); ++i) {
276+
m_tryDevices << availableDevice.at((primaryIdx + i) % availableDevice.size());
277+
}
278+
279+
if (m_tryDevices.isEmpty()) {
280+
qCWarning(DSM) << "try device is empty";
281+
resetAllNeverDefault();
282+
m_isSwitching = false;
283+
return;
284+
}
285+
286+
// 先将第一个候选设备的never-default恢复为false,确保它能接住默认路由
287+
changeDeviceNeverDefault(m_tryDevices.first(), false);
288+
changeDeviceNeverDefault(primaryDevice, true);
289+
m_tryIndex = 1;
290+
291+
// 设置超时定时器,防止NM没有触发信号时卡住
292+
if (!m_switchTimer) {
293+
m_switchTimer = new QTimer(this);
294+
m_switchTimer->setSingleShot(true);
295+
connect(m_switchTimer, &QTimer::timeout, this, &InternetChecker::onPrimaryConnectionTimeout);
296+
}
297+
m_switchTimer->start(5000);
298+
}

0 commit comments

Comments
 (0)