1067 lines
44 KiB
C++
1067 lines
44 KiB
C++
// SPDX-FileCopyrightText: 2026 Jokul
|
||
//
|
||
// SPDX-License-Identifier: LGPL-3.0-or-later
|
||
|
||
#include "networkmonitorapplet.h"
|
||
#include <pluginfactory.h>
|
||
|
||
#include <QFile>
|
||
#include <QTextStream>
|
||
#include <QDebug>
|
||
#include <QDir>
|
||
#include <QRegularExpression>
|
||
#include <QNetworkInterface>
|
||
#include <QColor>
|
||
#include <QSettings>
|
||
#include <QStandardPaths>
|
||
#include <QTranslator>
|
||
#include <QLocale>
|
||
#include <QCoreApplication>
|
||
#include <QProcess>
|
||
#include <QJsonDocument>
|
||
#include <QJsonObject>
|
||
#include <QFileInfo>
|
||
#include <QDate>
|
||
|
||
DS_BEGIN_NAMESPACE
|
||
|
||
NetworkMonitorApplet::NetworkMonitorApplet(QObject *parent)
|
||
: DApplet(parent)
|
||
, m_refreshTimer(nullptr)
|
||
, m_lastRxBytes(0)
|
||
, m_lastTxBytes(0)
|
||
, m_lastTimestampMs(0)
|
||
, m_downloadSpeed(0)
|
||
, m_uploadSpeed(0)
|
||
, m_totalDownload(0)
|
||
, m_totalUpload(0)
|
||
, m_rxPackets(0)
|
||
, m_txPackets(0)
|
||
, m_rxErrors(0)
|
||
, m_txErrors(0)
|
||
, m_rxDropped(0)
|
||
, m_txDropped(0)
|
||
, m_linkSpeed(0)
|
||
, m_refreshInterval(1000)
|
||
, m_tcpConnections(0)
|
||
, m_wifiSignal(0)
|
||
, m_lowFreqCounter(4)
|
||
, m_ipDetectCounter(4)
|
||
, m_ready(false)
|
||
, m_firstUpdate(true)
|
||
, m_textColor()
|
||
, m_historyDirty(true)
|
||
, m_trafficSaveCounter(0)
|
||
{
|
||
// 从独立配置文件读取持久化的字体颜色
|
||
// 设计原因:使用独立配置文件避免污染 dde-shell 的共享配置,
|
||
// 空串表示"跟随系统",由 QML 层回退到 primaryText 实现
|
||
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
|
||
+ QStringLiteral("/jnetapplet/settings.ini");
|
||
QSettings settings(configPath, QSettings::IniFormat);
|
||
if (settings.contains(QStringLiteral("textColor"))) {
|
||
const QString saved = settings.value(QStringLiteral("textColor")).toString();
|
||
// 空串合法(跟随系统),非空则校验是否为合法颜色值
|
||
if (saved.isEmpty() || QColor::isValidColorName(saved)) {
|
||
m_textColor = saved;
|
||
}
|
||
// 若校验失败则保持空串默认值(跟随系统),不抛出也不记录
|
||
}
|
||
|
||
// 从配置文件读取持久化的活动接口,启动时自动恢复用户上次选择
|
||
// 设计原因:用户手动选择的网卡应跨重启保持,而非每次启动重新自动检测;
|
||
// 若保存的接口已不存在(如 USB 网卡拔出),readNetworkStats 会清空并回退到自动选择
|
||
if (settings.contains(QStringLiteral("activeInterface"))) {
|
||
m_activeInterface = settings.value(QStringLiteral("activeInterface")).toString();
|
||
}
|
||
|
||
// 从配置文件读取持久化的刷新间隔(毫秒),仅接受 1/2/5 秒合法值
|
||
if (settings.contains(QStringLiteral("refreshInterval"))) {
|
||
const int iv = settings.value(QStringLiteral("refreshInterval")).toInt();
|
||
if (iv == 1000 || iv == 2000 || iv == 5000) {
|
||
m_refreshInterval = iv;
|
||
}
|
||
}
|
||
}
|
||
|
||
NetworkMonitorApplet::~NetworkMonitorApplet()
|
||
{
|
||
// 析构时兜底保存流量日志,确保进程退出前最后一段累加数据不丢失
|
||
// 设计原因:降频保存每 30 秒才写一次盘,若用户在两次保存之间退出,
|
||
// 这段增量会丢失;析构保存保证数据完整性
|
||
saveTrafficLog();
|
||
}
|
||
|
||
bool NetworkMonitorApplet::load()
|
||
{
|
||
return DApplet::load();
|
||
}
|
||
|
||
bool NetworkMonitorApplet::init()
|
||
{
|
||
// 加载翻译文件:根据系统语言加载对应的 .qm 文件
|
||
// 设计原因:QML 源串为英文,中文翻译通过 .qm 文件在运行时加载。
|
||
// 翻译上下文为 QML 文件名(如 "networkview"、"AboutWindow"),
|
||
// 安装到 QCoreApplication 后 qsTr() 自动查找匹配。
|
||
QTranslator *translator = new QTranslator(this);
|
||
const QString locale = QLocale::system().name();
|
||
if (translator->load(QStringLiteral("jnetapplet_%1.qm").arg(locale),
|
||
QStringLiteral(TRANSLATIONS_DIR))) {
|
||
qApp->installTranslator(translator);
|
||
}
|
||
|
||
detectInterfaces();
|
||
|
||
// 初始化流量日志持久化
|
||
// 设计原因:日志与设置共用 ~/.config/jnetapplet 目录;
|
||
// 启动时记录当前日期/月份用于跨日跨月检测,并从 JSON 文件加载历史累计数据
|
||
m_trafficLogPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
|
||
+ QStringLiteral("/jnetapplet/traffic_log.json");
|
||
m_currentDate = QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd"));
|
||
m_currentMonth = QDate::currentDate().toString(QStringLiteral("yyyy-MM"));
|
||
loadTrafficLog();
|
||
|
||
// 启动定时刷新
|
||
m_refreshTimer = new QTimer(this);
|
||
connect(m_refreshTimer, &QTimer::timeout, this, &NetworkMonitorApplet::refresh);
|
||
// 用持久化的刷新间隔启动(默认 1000ms = 1 秒)
|
||
m_refreshTimer->start(m_refreshInterval);
|
||
|
||
// 初始读取
|
||
readNetworkStats();
|
||
|
||
return DApplet::init();
|
||
}
|
||
|
||
double NetworkMonitorApplet::downloadSpeed() const
|
||
{
|
||
return m_downloadSpeed;
|
||
}
|
||
|
||
double NetworkMonitorApplet::uploadSpeed() const
|
||
{
|
||
return m_uploadSpeed;
|
||
}
|
||
|
||
double NetworkMonitorApplet::totalDownload() const
|
||
{
|
||
return static_cast<double>(m_totalDownload);
|
||
}
|
||
|
||
double NetworkMonitorApplet::totalUpload() const
|
||
{
|
||
return static_cast<double>(m_totalUpload);
|
||
}
|
||
|
||
QStringList NetworkMonitorApplet::networkInterfaces() const
|
||
{
|
||
return m_interfaceList;
|
||
}
|
||
|
||
bool NetworkMonitorApplet::ready() const
|
||
{
|
||
return m_ready;
|
||
}
|
||
|
||
QString NetworkMonitorApplet::activeInterface() const
|
||
{
|
||
return m_activeInterface;
|
||
}
|
||
|
||
// 返回活动接口的 IPv4 地址,供 QML 显示
|
||
QString NetworkMonitorApplet::ipAddress() const
|
||
{
|
||
return m_ipAddress;
|
||
}
|
||
|
||
// 返回活动接口的 IPv6 全球地址,供 QML 显示
|
||
QString NetworkMonitorApplet::ipv6Address() const
|
||
{
|
||
return m_ipv6Address;
|
||
}
|
||
|
||
// ---- 活动接口包统计 getter(累计值,自开机起)----
|
||
// 供弹窗接口信息区展示收发包/错误/丢包数
|
||
quint64 NetworkMonitorApplet::rxPackets() const { return m_rxPackets; }
|
||
quint64 NetworkMonitorApplet::txPackets() const { return m_txPackets; }
|
||
quint64 NetworkMonitorApplet::rxErrors() const { return m_rxErrors; }
|
||
quint64 NetworkMonitorApplet::txErrors() const { return m_txErrors; }
|
||
quint64 NetworkMonitorApplet::rxDropped() const { return m_rxDropped; }
|
||
quint64 NetworkMonitorApplet::txDropped() const { return m_txDropped; }
|
||
|
||
// 返回活动接口的链路协商速率(Mbps),0 表示不可用
|
||
int NetworkMonitorApplet::linkSpeed() const { return m_linkSpeed; }
|
||
|
||
// 返回刷新间隔(毫秒)
|
||
int NetworkMonitorApplet::refreshInterval() const { return m_refreshInterval; }
|
||
|
||
// 设置刷新间隔(毫秒),校验后持久化并即时生效
|
||
// 设计原因:仅接受 1/2/5 秒,避免非法值破坏刷新频率;
|
||
// 速度计算基于真实流逝时间(elapsedSec),改变间隔不影响计算正确性
|
||
void NetworkMonitorApplet::setRefreshInterval(int ms)
|
||
{
|
||
if (ms != 1000 && ms != 2000 && ms != 5000) {
|
||
return;
|
||
}
|
||
if (m_refreshInterval == ms) {
|
||
return;
|
||
}
|
||
m_refreshInterval = ms;
|
||
if (m_refreshTimer) {
|
||
m_refreshTimer->start(m_refreshInterval);
|
||
}
|
||
|
||
// 持久化到独立配置文件,重启后仍生效
|
||
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
|
||
+ QStringLiteral("/jnetapplet/settings.ini");
|
||
QSettings settings(configPath, QSettings::IniFormat);
|
||
settings.setValue(QStringLiteral("refreshInterval"), m_refreshInterval);
|
||
settings.sync();
|
||
|
||
emit refreshIntervalChanged();
|
||
}
|
||
|
||
// 返回当前活动接口的 TCP ESTABLISHED 连接数
|
||
int NetworkMonitorApplet::tcpConnections() const { return m_tcpConnections; }
|
||
|
||
// 返回活动接口的 WiFi 信号强度(dBm),0 表示不可用
|
||
int NetworkMonitorApplet::wifiSignal() const { return m_wifiSignal; }
|
||
|
||
// 返回插件版本号,从 dde-shell 插件元数据(metadata.json)读取
|
||
// 版本唯一源为 CMakeLists.txt 的 project(VERSION),经 configure_file 写入 metadata.json,
|
||
// dde-shell 加载时存入 DPluginMetaData,此处通过继承自 DApplet 的 pluginMetaData() 读取
|
||
QString NetworkMonitorApplet::version() const
|
||
{
|
||
// DPluginMetaData::value() 已在 "Plugin" 层级内部查找(源码:frame/pluginmetadata.cpp)
|
||
// metadata.json 的 { "Plugin": { "Version": "x.y.z" } } 被 DPluginMetaData 加载后,
|
||
// value("Version") 直接返回版本号,无需再 .value("Plugin").toMap() 展开
|
||
const QString ver = pluginMetaData().value("Version").toString();
|
||
// 兜底:元数据未加载或无 Version 字段时回退到编译时版本号
|
||
// 设计原因:QML 各处 version 属性兜底不一致(networkview.qml 有 "1.0",AboutWindow 无),
|
||
// 统一在后端兜底,避免 AboutWindow 版本行显示空白
|
||
return ver.isEmpty() ? QStringLiteral(PROJECT_VERSION) : ver;
|
||
}
|
||
|
||
// 返回任务栏网速字体颜色,空串表示跟随系统主题
|
||
QString NetworkMonitorApplet::textColor() const
|
||
{
|
||
return m_textColor;
|
||
}
|
||
|
||
// 设置任务栏网速字体颜色,空串表示跟随系统主题
|
||
// 设计原因:
|
||
// - 空串语义:空串作为"跟随系统"的语义值,QML 层用 textColor ? textColor : primaryText 做回退,
|
||
// 避免后端硬编码系统色
|
||
// - 校验逻辑:非空颜色值通过 QColor::isValidColor 校验,无效值直接忽略,不存储不发射信号
|
||
// - 独立配置文件:使用 ~/.config/jnetapplet/settings.ini 而非 dde-shell 共享配置,
|
||
// 避免污染其他组件的配置空间
|
||
void NetworkMonitorApplet::setTextColor(const QString &color)
|
||
{
|
||
// 空串视为合法(表示跟随系统),不校验
|
||
if (!color.isEmpty() && !QColor::isValidColorName(color)) {
|
||
return;
|
||
}
|
||
|
||
if (m_textColor == color) {
|
||
return;
|
||
}
|
||
|
||
m_textColor = color;
|
||
|
||
// 持久化到独立配置文件
|
||
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
|
||
+ QStringLiteral("/jnetapplet/settings.ini");
|
||
QSettings settings(configPath, QSettings::IniFormat);
|
||
settings.setValue(QStringLiteral("textColor"), m_textColor);
|
||
settings.sync();
|
||
|
||
emit textColorChanged();
|
||
}
|
||
|
||
// 返回流量统计日志(JSON 对象),结构见 trafficLog 属性注释
|
||
// QJsonObject 可直接暴露给 QML 作为 JS 对象读取,无需额外转换
|
||
QJsonObject NetworkMonitorApplet::trafficLog() const
|
||
{
|
||
return m_trafficLog;
|
||
}
|
||
|
||
// 返回 TCP 连接详情列表(QVariantList of QVariantMap),供 QML 连接清单窗口展示
|
||
QVariantList NetworkMonitorApplet::tcpConnectionList() const
|
||
{
|
||
return m_tcpConnectionList;
|
||
}
|
||
|
||
// 返回当前活动接口的下行速度历史,转换为 QVariantList of QPointF 供 QML 使用
|
||
// 设计原因:QPointF 仅携带 x/y 两个值,上传与下载分别对应两个列表;
|
||
// 结果缓存于成员变量,仅当 m_historyDirty 时重建,避免 QML 每次读取都重新构造 300 个点
|
||
QVariantList NetworkMonitorApplet::speedHistoryDownload() const
|
||
{
|
||
if (m_historyDirty) {
|
||
rebuildHistoryCache();
|
||
}
|
||
return m_cachedHistoryDl;
|
||
}
|
||
|
||
// 返回当前活动接口的上行速度历史,结构同 speedHistoryDownload
|
||
QVariantList NetworkMonitorApplet::speedHistoryUpload() const
|
||
{
|
||
if (m_historyDirty) {
|
||
rebuildHistoryCache();
|
||
}
|
||
return m_cachedHistoryUpload;
|
||
}
|
||
|
||
// 重建上下行历史缓存(一次遍历同时填充两个列表)
|
||
// 仅当 dirty 时由两个 getter 调用,避免重复构建;
|
||
// 单 dirty 标记 + 一次重建双缓存,保证先后读取两个列表时数据一致
|
||
void NetworkMonitorApplet::rebuildHistoryCache() const
|
||
{
|
||
m_cachedHistoryDl.clear();
|
||
m_cachedHistoryUpload.clear();
|
||
if (!m_activeInterface.isEmpty() && m_speedHistory.contains(m_activeInterface)) {
|
||
const QVector<SpeedSample> &samples = m_speedHistory[m_activeInterface];
|
||
m_cachedHistoryDl.reserve(samples.size());
|
||
m_cachedHistoryUpload.reserve(samples.size());
|
||
for (const SpeedSample &s : samples) {
|
||
m_cachedHistoryDl.append(QPointF(s.timestamp, s.downloadSpeed));
|
||
m_cachedHistoryUpload.append(QPointF(s.timestamp, s.uploadSpeed));
|
||
}
|
||
}
|
||
m_historyDirty = false;
|
||
}
|
||
|
||
void NetworkMonitorApplet::refresh()
|
||
{
|
||
readNetworkStats();
|
||
|
||
// 流量日志降频保存:每 30 秒写盘一次并通知 QML
|
||
// 设计原因:累加在 calculateSpeed 中每秒执行(纯内存操作开销低),
|
||
// 而写盘与 QML 重绘若每秒执行会造成不必要的磁盘 IO 与界面刷新;
|
||
// 计数器按刷新间隔折算(1s=30 次,2s=15 次,5s=6 次)保证实际每 30 秒保存一次
|
||
const int saveEveryMs = 30000;
|
||
const int saveThreshold = qMax(1, saveEveryMs / m_refreshInterval);
|
||
if (++m_trafficSaveCounter >= saveThreshold) {
|
||
m_trafficSaveCounter = 0;
|
||
saveTrafficLog();
|
||
emit trafficLogChanged();
|
||
}
|
||
}
|
||
|
||
void NetworkMonitorApplet::setActiveInterface(const QString &interface)
|
||
{
|
||
// 校验接口是否存在于当前接口列表中,无效接口名直接忽略
|
||
// 设计原因:传入不存在的接口名会被持久化到配置文件,且当前会话期间
|
||
// getActiveRxBytes() 返回 0 导致速度恒为 0,用户困惑;
|
||
// 虽然下次启动 readNetworkStats() 会清空回退,但当前会话不应接受无效值
|
||
if (!m_interfaceList.contains(interface)) {
|
||
return;
|
||
}
|
||
|
||
if (m_activeInterface != interface) {
|
||
m_activeInterface = interface;
|
||
m_firstUpdate = true;
|
||
|
||
// 接口切换后历史数据变化,标记脏缓存让趋势图立即重建
|
||
m_historyDirty = true;
|
||
|
||
// 接口切换后包统计指向新接口,旧值不再有效,重置并通知 QML
|
||
// 设计原因:包统计为各接口独立的累计值,切换后应显示新接口的数据
|
||
m_rxPackets = 0;
|
||
m_txPackets = 0;
|
||
m_rxErrors = 0;
|
||
m_txErrors = 0;
|
||
m_rxDropped = 0;
|
||
m_txDropped = 0;
|
||
emit packetStatsChanged();
|
||
|
||
// 持久化到配置文件,下次启动自动加载用户选择的网卡
|
||
// 设计原因:弹窗 chip 和设置窗口都调用此方法,统一持久化保证两处选择一致
|
||
const QString configPath = QStandardPaths::writableLocation(QStandardPaths::ConfigLocation)
|
||
+ QStringLiteral("/jnetapplet/settings.ini");
|
||
QSettings settings(configPath, QSettings::IniFormat);
|
||
settings.setValue(QStringLiteral("activeInterface"), m_activeInterface);
|
||
settings.sync();
|
||
|
||
emit activeInterfaceChanged();
|
||
// 接口切换后通知 QML 趋势图切换显示新接口的历史数据
|
||
emit speedHistoryChanged();
|
||
// 接口切换后立即检测新接口的 IP 地址
|
||
detectIpAddress();
|
||
// 接口切换后立即读取新接口的链路速率与 WiFi 信号
|
||
detectLowFreqStats();
|
||
}
|
||
}
|
||
|
||
void NetworkMonitorApplet::readNetworkStats()
|
||
{
|
||
QFile file("/proc/net/dev");
|
||
if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
qDebug() << "Failed to open /proc/net/dev";
|
||
return;
|
||
}
|
||
|
||
QTextStream stream(&file);
|
||
QStringList lines = stream.readAll().split('\n', Qt::SkipEmptyParts);
|
||
file.close();
|
||
|
||
QMap<QString, NetworkInterface> newInterfaces;
|
||
|
||
// 跳过前两行标题
|
||
for (int i = 2; i < lines.size(); ++i) {
|
||
QString line = lines[i].trimmed();
|
||
if (line.isEmpty()) continue;
|
||
|
||
// 格式: "interface: rx_bytes rx_packets rx_errors rx_dropped ... tx_bytes tx_packets tx_errors tx_dropped ..."
|
||
// static const 仅首次调用时编译正则一次,后续复用,避免每秒对每行重复编译
|
||
static const QRegularExpression re("^([^:]+):\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+\\d+\\s+\\d+\\s+\\d+\\s+\\d+\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)\\s+(\\d+)");
|
||
QRegularExpressionMatch match = re.match(line);
|
||
|
||
if (match.hasMatch()) {
|
||
NetworkInterface iface;
|
||
iface.name = match.captured(1);
|
||
iface.rxBytes = match.captured(2).toLongLong();
|
||
iface.rxPackets = match.captured(3).toLongLong();
|
||
iface.rxErrors = match.captured(4).toLongLong();
|
||
iface.rxDropped = match.captured(5).toLongLong();
|
||
iface.txBytes = match.captured(6).toLongLong();
|
||
iface.txPackets = match.captured(7).toLongLong();
|
||
iface.txErrors = match.captured(8).toLongLong();
|
||
iface.txDropped = match.captured(9).toLongLong();
|
||
|
||
// 过滤掉回环接口和虚拟接口
|
||
if (iface.name != "lo" && !iface.name.startsWith("veth") &&
|
||
!iface.name.startsWith("docker") && !iface.name.startsWith("br-")) {
|
||
newInterfaces[iface.name] = iface;
|
||
}
|
||
}
|
||
}
|
||
|
||
// 检测接口变化
|
||
if (newInterfaces.keys() != m_interfaces.keys()) {
|
||
m_interfaceList = newInterfaces.keys();
|
||
m_interfaces = newInterfaces;
|
||
|
||
// 清理已消失接口的历史缓冲和基线数据,避免内存泄漏
|
||
// 设计原因:USB 网卡、VPN 隧道等接口可能随时消失,其历史数据和基线不再需要
|
||
const QStringList oldIfaces = m_speedHistory.keys();
|
||
for (const QString &name : oldIfaces) {
|
||
if (!newInterfaces.contains(name)) {
|
||
m_speedHistory.remove(name);
|
||
m_lastRxBytesByIface.remove(name);
|
||
m_lastTxBytesByIface.remove(name);
|
||
}
|
||
}
|
||
|
||
// 保存的接口已不在当前列表中(如 USB 网卡拔出),清空让自动选择接管
|
||
if (!m_activeInterface.isEmpty() && !m_interfaceList.contains(m_activeInterface)) {
|
||
m_activeInterface.clear();
|
||
}
|
||
|
||
// 自动选择活动接口
|
||
if (m_activeInterface.isEmpty() && !m_interfaceList.isEmpty()) {
|
||
// 优先选择物理网卡(wlp/wlan/enp/eth)中有流量的接口,
|
||
// 避免选中 Meta/tun0 等虚拟代理接口作为默认显示
|
||
for (const QString &name : m_interfaceList) {
|
||
if (!isPhysicalInterface(name)) continue;
|
||
const NetworkInterface &iface = m_interfaces[name];
|
||
if (iface.rxBytes > 0 || iface.txBytes > 0) {
|
||
m_activeInterface = name;
|
||
break;
|
||
}
|
||
}
|
||
// 其次选择任意有流量的接口(物理网卡都无流量时的兜底)
|
||
if (m_activeInterface.isEmpty()) {
|
||
for (const QString &name : m_interfaceList) {
|
||
const NetworkInterface &iface = m_interfaces[name];
|
||
if (iface.rxBytes > 0 || iface.txBytes > 0) {
|
||
m_activeInterface = name;
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
// 最后选第一个(无任何流量时)
|
||
if (m_activeInterface.isEmpty()) {
|
||
m_activeInterface = m_interfaceList.first();
|
||
}
|
||
emit activeInterfaceChanged();
|
||
}
|
||
|
||
m_ready = !m_interfaceList.isEmpty();
|
||
emit interfacesChanged();
|
||
emit readyChanged();
|
||
} else {
|
||
m_interfaces = newInterfaces;
|
||
}
|
||
|
||
calculateSpeed();
|
||
// IP 地址变化频率极低(DHCP 续约/网络切换等),降频至每 5 秒检测一次,
|
||
// 减少每秒 QNetworkInterface::interfaceFromName 的系统查询开销
|
||
// 计数器每次 refresh 递增,达到 5 时归零并执行检测
|
||
if (++m_ipDetectCounter >= 5) {
|
||
m_ipDetectCounter = 0;
|
||
detectIpAddress();
|
||
}
|
||
// 链路速率与 WiFi 信号同样变化频率低,每 5 秒检测一次
|
||
if (++m_lowFreqCounter >= 5) {
|
||
m_lowFreqCounter = 0;
|
||
detectLowFreqStats();
|
||
}
|
||
// TCP 连接清单同样每 5 秒更新一次(进程名反查需遍历 /proc 开销大,不能每秒执行)
|
||
if (++m_tcpListCounter >= 5) {
|
||
m_tcpListCounter = 0;
|
||
buildTcpConnectionList();
|
||
}
|
||
}
|
||
|
||
void NetworkMonitorApplet::calculateSpeed()
|
||
{
|
||
// 当前采样时间戳(毫秒),用于按真实流逝时间计算速度
|
||
const qint64 nowMs = QDateTime::currentMSecsSinceEpoch();
|
||
const qint64 nowSec = QDateTime::currentSecsSinceEpoch();
|
||
const double elapsedSec = (nowMs - m_lastTimestampMs) / 1000.0;
|
||
m_lastTimestampMs = nowMs;
|
||
|
||
// 记录旧值用于 dirty 比较:网络空闲时速度/总量可能不变,
|
||
// 仅在真正变化时才发射信号,避免 QML 每秒无效重绘
|
||
const double oldDownload = m_downloadSpeed;
|
||
const double oldUpload = m_uploadSpeed;
|
||
const qint64 oldTotalDownload = m_totalDownload;
|
||
const qint64 oldTotalUpload = m_totalUpload;
|
||
|
||
// ---- 活动接口速度计算(用于任务栏显示和总量累加)----
|
||
if (m_firstUpdate) {
|
||
// 首次更新或接口切换后:仅记录基线,不计算速度
|
||
m_firstUpdate = false;
|
||
m_lastRxBytes = getActiveRxBytes();
|
||
m_lastTxBytes = getActiveTxBytes();
|
||
} else if (elapsedSec > 0 && !m_activeInterface.isEmpty()) {
|
||
// 按真实流逝时间计算速度(字节/秒),避免定时器抖动失真
|
||
const qint64 currentRxBytes = getActiveRxBytes();
|
||
const qint64 currentTxBytes = getActiveTxBytes();
|
||
// 计数器回绕/接口重置时差值可能为负,钳制为 0 避免显示负速度
|
||
const qint64 rxDelta = currentRxBytes - m_lastRxBytes;
|
||
const qint64 txDelta = currentTxBytes - m_lastTxBytes;
|
||
const qint64 rxDeltaClamped = rxDelta > 0 ? rxDelta : 0;
|
||
const qint64 txDeltaClamped = txDelta > 0 ? txDelta : 0;
|
||
m_downloadSpeed = rxDeltaClamped / elapsedSec;
|
||
m_uploadSpeed = txDeltaClamped / elapsedSec;
|
||
// 会话总量:直接读取活动接口累计字节数(开机至今),而非累加差值
|
||
// 设计原因:用户期望"本次会话"显示当前网卡从开机起的累计流量,
|
||
// 切换网卡后立即显示新网卡的累计值,无流量网卡显示 0
|
||
m_totalDownload = currentRxBytes;
|
||
m_totalUpload = currentTxBytes;
|
||
// 累加日/月流量日志(仅活动接口增量,与速度计算口径一致)
|
||
appendToTrafficLog(rxDeltaClamped, txDeltaClamped);
|
||
m_lastRxBytes = currentRxBytes;
|
||
m_lastTxBytes = currentTxBytes;
|
||
}
|
||
|
||
// ---- 活动接口包统计(累计值,自开机起)----
|
||
// 直接从当前活动接口读取收发包/错误/丢包计数,任一值变化时发射 packetStatsChanged
|
||
if (!m_activeInterface.isEmpty() && m_interfaces.contains(m_activeInterface)) {
|
||
const NetworkInterface &iface = m_interfaces[m_activeInterface];
|
||
if (m_rxPackets != static_cast<quint64>(iface.rxPackets)
|
||
|| m_txPackets != static_cast<quint64>(iface.txPackets)
|
||
|| m_rxErrors != static_cast<quint64>(iface.rxErrors)
|
||
|| m_txErrors != static_cast<quint64>(iface.txErrors)
|
||
|| m_rxDropped != static_cast<quint64>(iface.rxDropped)
|
||
|| m_txDropped != static_cast<quint64>(iface.txDropped)) {
|
||
m_rxPackets = static_cast<quint64>(iface.rxPackets);
|
||
m_txPackets = static_cast<quint64>(iface.txPackets);
|
||
m_rxErrors = static_cast<quint64>(iface.rxErrors);
|
||
m_txErrors = static_cast<quint64>(iface.txErrors);
|
||
m_rxDropped = static_cast<quint64>(iface.rxDropped);
|
||
m_txDropped = static_cast<quint64>(iface.txDropped);
|
||
emit packetStatsChanged();
|
||
|
||
// 接口切换后立即更新会话总量为新网卡的累计字节数(开机至今)
|
||
// 设计原因:会话总量改为读取活动接口累计值,切换后应立即显示新网卡数据,
|
||
// 无需等下一次 calculateSpeed(1 秒后)才更新
|
||
if (m_interfaces.contains(m_activeInterface)) {
|
||
m_totalDownload = m_interfaces[m_activeInterface].rxBytes;
|
||
m_totalUpload = m_interfaces[m_activeInterface].txBytes;
|
||
emit totalChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
// ---- TCP 连接数(每秒统计)----
|
||
// /proc/net/tcp 与 tcp6 文件很小,遍历开销低;仅在连接数变化时发射信号
|
||
const int conns = countTcpConnections();
|
||
if (m_tcpConnections != conns) {
|
||
m_tcpConnections = conns;
|
||
emit tcpConnectionsChanged();
|
||
}
|
||
|
||
// ---- 为所有接口采集速度历史(非仅活动接口)----
|
||
// 设计原因:原仅采集活动接口,切换到非活动接口时趋势图为空需等待数分钟。
|
||
// 改为遍历所有接口计算各自速度并追加历史,切换接口时趋势图立即有数据。
|
||
if (elapsedSec > 0) {
|
||
for (const QString &name : m_interfaceList) {
|
||
if (!m_interfaces.contains(name)) continue;
|
||
const NetworkInterface &iface = m_interfaces[name];
|
||
|
||
// 接口尚未初始化基线(首次采集或运行中新增的接口),记录基线跳过本次
|
||
if (!m_lastRxBytesByIface.contains(name)) {
|
||
m_lastRxBytesByIface[name] = iface.rxBytes;
|
||
m_lastTxBytesByIface[name] = iface.txBytes;
|
||
continue;
|
||
}
|
||
|
||
const qint64 iRxDelta = iface.rxBytes - m_lastRxBytesByIface[name];
|
||
const qint64 iTxDelta = iface.txBytes - m_lastTxBytesByIface[name];
|
||
|
||
SpeedSample sample;
|
||
sample.timestamp = nowSec;
|
||
sample.downloadSpeed = (iRxDelta > 0 ? iRxDelta : 0) / elapsedSec;
|
||
sample.uploadSpeed = (iTxDelta > 0 ? iTxDelta : 0) / elapsedSec;
|
||
m_speedHistory[name].append(sample);
|
||
|
||
// 滑动窗口裁剪:超过 300 点时丢弃最旧的
|
||
QVector<SpeedSample> &samples = m_speedHistory[name];
|
||
while (samples.size() > MAX_HISTORY_SAMPLES) {
|
||
samples.removeFirst();
|
||
}
|
||
|
||
m_lastRxBytesByIface[name] = iface.rxBytes;
|
||
m_lastTxBytesByIface[name] = iface.txBytes;
|
||
}
|
||
// 历史采样点已追加,标记脏缓存,QML 下次读取时重建
|
||
m_historyDirty = true;
|
||
emit speedHistoryChanged();
|
||
}
|
||
|
||
// 仅在速度/总量实际变化时发射信号,避免空闲时每秒触发 QML 无效重绘
|
||
if (m_downloadSpeed != oldDownload || m_uploadSpeed != oldUpload) {
|
||
emit speedChanged();
|
||
}
|
||
if (m_totalDownload != oldTotalDownload || m_totalUpload != oldTotalUpload) {
|
||
emit totalChanged();
|
||
}
|
||
}
|
||
|
||
void NetworkMonitorApplet::detectInterfaces()
|
||
{
|
||
QDir sysClass("/sys/class/net");
|
||
if (!sysClass.exists()) return;
|
||
|
||
QStringList entries = sysClass.entryList(QDir::Dirs | QDir::NoDotAndDotDot);
|
||
m_interfaceList.clear();
|
||
|
||
for (const QString &entry : entries) {
|
||
// 过滤回环接口和虚拟接口,与 readNetworkStats() 保持一致
|
||
// 设计原因:原仅过滤 lo,docker/veth/br- 接口会短暂出现在列表中,
|
||
// 随后被 readNetworkStats() 覆盖,造成 UI 闪烁
|
||
if (entry != "lo" && !entry.startsWith("veth") &&
|
||
!entry.startsWith("docker") && !entry.startsWith("br-")) {
|
||
m_interfaceList << entry;
|
||
}
|
||
}
|
||
|
||
m_ready = !m_interfaceList.isEmpty();
|
||
emit interfacesChanged();
|
||
emit readyChanged();
|
||
}
|
||
|
||
qint64 NetworkMonitorApplet::getActiveRxBytes() const
|
||
{
|
||
if (m_activeInterface.isEmpty() || !m_interfaces.contains(m_activeInterface)) {
|
||
return 0;
|
||
}
|
||
return m_interfaces[m_activeInterface].rxBytes;
|
||
}
|
||
|
||
qint64 NetworkMonitorApplet::getActiveTxBytes() const
|
||
{
|
||
if (m_activeInterface.isEmpty() || !m_interfaces.contains(m_activeInterface)) {
|
||
return 0;
|
||
}
|
||
return m_interfaces[m_activeInterface].txBytes;
|
||
}
|
||
|
||
// 检测活动接口的 IPv4 与 IPv6 地址
|
||
// IPv4:跳过 loopback,取第一个
|
||
// IPv6:跳过 loopback 和 link-local (fe80::),取第一个全球地址
|
||
// 任一地址变化时分别 emit 对应信号通知 QML 更新
|
||
// 设计原因:DHCP 续约、网络切换、IPv6 SLAAC 等场景下地址可能变化,需持续检测
|
||
void NetworkMonitorApplet::detectIpAddress()
|
||
{
|
||
QString newIp;
|
||
QString newIpv6;
|
||
if (!m_activeInterface.isEmpty()) {
|
||
QNetworkInterface iface = QNetworkInterface::interfaceFromName(m_activeInterface);
|
||
for (const QNetworkAddressEntry &entry : iface.addressEntries()) {
|
||
// IPv4:跳过 loopback,取第一个
|
||
if (entry.ip().protocol() == QAbstractSocket::IPv4Protocol
|
||
&& !entry.ip().isLoopback()
|
||
&& newIp.isEmpty()) {
|
||
newIp = entry.ip().toString();
|
||
}
|
||
// IPv6:跳过 loopback 和 link-local (fe80::),取第一个全球地址
|
||
if (entry.ip().protocol() == QAbstractSocket::IPv6Protocol
|
||
&& !entry.ip().isLoopback()
|
||
&& !entry.ip().isLinkLocal()
|
||
&& newIpv6.isEmpty()) {
|
||
newIpv6 = entry.ip().toString();
|
||
}
|
||
}
|
||
}
|
||
if (m_ipAddress != newIp) {
|
||
m_ipAddress = newIp;
|
||
emit ipAddressChanged();
|
||
}
|
||
if (m_ipv6Address != newIpv6) {
|
||
m_ipv6Address = newIpv6;
|
||
emit ipv6AddressChanged();
|
||
}
|
||
}
|
||
|
||
// 低频统计检测:链路速率与 WiFi 信号(每 5 秒或接口切换时),变化时发射对应信号
|
||
// 设计原因:这两项数据变化频率极低(协商速率/信号强度基本稳定),
|
||
// 无需每秒读取,降频降低系统调用与文件读取开销
|
||
void NetworkMonitorApplet::detectLowFreqStats()
|
||
{
|
||
if (!m_activeInterface.isEmpty()) {
|
||
const int speed = detectLinkSpeed(m_activeInterface);
|
||
if (m_linkSpeed != speed) {
|
||
m_linkSpeed = speed;
|
||
emit linkSpeedChanged();
|
||
}
|
||
const int sig = detectWifiSignal(m_activeInterface);
|
||
if (m_wifiSignal != sig) {
|
||
m_wifiSignal = sig;
|
||
emit wifiSignalChanged();
|
||
}
|
||
} else {
|
||
// 无活动接口时归零,避免显示上一接口的残留数据
|
||
if (m_linkSpeed != 0) {
|
||
m_linkSpeed = 0;
|
||
emit linkSpeedChanged();
|
||
}
|
||
if (m_wifiSignal != 0) {
|
||
m_wifiSignal = 0;
|
||
emit wifiSignalChanged();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 读取指定接口的链路协商速率(Mbps)
|
||
// 有线:直接读 /sys/class/net/<iface>/speed(Mbps)
|
||
// 无线:speed 文件常返回错误(EINVAL),降级执行 `iw dev <iface> link` 解析 bitrate
|
||
// 任何一步失败都返回 0(表示不可用),不抛出不记录
|
||
int NetworkMonitorApplet::detectLinkSpeed(const QString &iface)
|
||
{
|
||
// 尝试 /sys/class/net/<iface>/speed
|
||
QFile f(QStringLiteral("/sys/class/net/%1/speed").arg(iface));
|
||
if (f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
const QString val = QString::fromLatin1(f.readAll()).trimmed();
|
||
f.close();
|
||
bool ok = false;
|
||
const int speed = val.toInt(&ok);
|
||
if (ok && speed >= 0) {
|
||
return speed;
|
||
}
|
||
}
|
||
|
||
// 无线网卡降级:解析 iw dev link 的 bitrate
|
||
if (iface.startsWith("wlp") || iface.startsWith("wlan")) {
|
||
QProcess proc;
|
||
proc.start(QStringLiteral("iw"), {QStringLiteral("dev"), iface, QStringLiteral("link")});
|
||
if (proc.waitForFinished(1000)) {
|
||
const int br = parseIwBitrate(QString::fromUtf8(proc.readAllStandardOutput()));
|
||
if (br > 0) {
|
||
return br;
|
||
}
|
||
}
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
// 解析 `iw dev <iface> link` 输出中的 bitrate(Mbps),失败返回 0
|
||
// 输出形如 " bitrate: 866.7 MBit/s",也兼容 "Gbit/s"(换算为 Mbps)
|
||
int NetworkMonitorApplet::parseIwBitrate(const QString &output) const
|
||
{
|
||
static const QRegularExpression re("bitrate:\\s*([\\d.]+)\\s*(M|G)bit/s");
|
||
QRegularExpressionMatch m = re.match(output);
|
||
if (!m.hasMatch()) {
|
||
return 0;
|
||
}
|
||
const double val = m.captured(1).toDouble();
|
||
const bool isGigabit = m.captured(2) == QLatin1String("G");
|
||
return isGigabit ? qRound(val * 1000.0) : qRound(val);
|
||
}
|
||
|
||
// 统计 /proc/net/tcp 与 /proc/net/tcp6 中 ESTABLISHED(状态 01)的连接数
|
||
// 文件第 4 列(st)为连接状态码,01 表示 ESTABLISHED;
|
||
// 任一文件读取失败时跳过,不因单个文件缺失而失败
|
||
int NetworkMonitorApplet::countTcpConnections() const
|
||
{
|
||
int count = 0;
|
||
const QStringList paths = {QStringLiteral("/proc/net/tcp"), QStringLiteral("/proc/net/tcp6")};
|
||
for (const QString &path : paths) {
|
||
QFile f(path);
|
||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
continue;
|
||
}
|
||
QTextStream stream(&f);
|
||
// 跳过标题行
|
||
stream.readLine();
|
||
while (!stream.atEnd()) {
|
||
const QString line = stream.readLine().trimmed();
|
||
if (line.isEmpty()) {
|
||
continue;
|
||
}
|
||
const QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||
if (parts.size() > 3 && parts[3] == QLatin1String("01")) {
|
||
++count;
|
||
}
|
||
}
|
||
f.close();
|
||
}
|
||
return count;
|
||
}
|
||
|
||
// 构建 TCP 连接详情列表(调用 ss 命令获取 ESTABLISHED 连接,含进程名反查)
|
||
// 结果存入 m_tcpConnectionList(QVariantList of QVariantMap)并发射 tcpConnectionListChanged;
|
||
// 仅保留 ESTABLISHED 状态连接。优雅降级:ss 命令不存在/执行失败时返回空列表,不崩溃
|
||
// 设计原因:原先通过遍历 /proc/<pid>/fd 反查进程名,但 dde-shell 以普通用户运行,
|
||
// 无权 readlink 其他进程的 fd(lr-x------ 权限),导致进程名全空;改用 ss 命令一次
|
||
// 获取全部 ESTABLISHED 连接的地址/端口/进程名,ss 通过 netlink 获取信息不受 /proc 权限限制,
|
||
// 且仍由 readNetworkStats 降频至每 5 秒调用一次(ss 启动子进程仍有开销)
|
||
void NetworkMonitorApplet::buildTcpConnectionList()
|
||
{
|
||
QVariantList list;
|
||
|
||
// 用 ss 命令获取 ESTABLISHED 连接(含进程名),通过 netlink 不受 /proc 权限限制
|
||
// -t TCP, -n 不解析服务名(显示端口), -p 显示进程信息, state established 仅已建立连接
|
||
QProcess process;
|
||
process.start(QStringLiteral("ss"), {QStringLiteral("-tnp"), QStringLiteral("state"), QStringLiteral("established")});
|
||
// ss 命令不存在或启动失败(如精简系统未安装 iproute2)时优雅降级:返回空列表,不崩溃
|
||
if (!process.waitForStarted(1000)) {
|
||
m_tcpConnectionList = list;
|
||
emit tcpConnectionListChanged();
|
||
return;
|
||
}
|
||
process.waitForFinished(3000); // 3 秒超时,ss 卡住时不阻塞主线程过久
|
||
const QString output = QString::fromUtf8(process.readAllStandardOutput());
|
||
|
||
const QStringList lines = output.split(QLatin1Char('\n'), Qt::SkipEmptyParts);
|
||
for (int i = 0; i < lines.size(); ++i) {
|
||
if (i == 0) continue; // 跳过表头(Recv-Q Send-Q ...)
|
||
|
||
const QString line = lines[i].trimmed();
|
||
if (line.isEmpty()) continue;
|
||
|
||
// ss 输出按多空格分割:Recv-Q Send-Q Local:Port Peer:Port [Process]
|
||
// 注意 Process 列可能为空,且 IPv6 地址形如 [::1]:port(方括号内无空格),
|
||
// 用正则提取更可靠,可容忍列之间任意空白
|
||
QRegularExpression re(QStringLiteral("(\\S+)\\s+(\\S+)\\s+(\\S+):(\\d+)\\s+(\\S+):(\\d+)\\s*(.*)"));
|
||
QRegularExpressionMatch match = re.match(line);
|
||
if (!match.hasMatch()) continue;
|
||
|
||
QString localAddress = match.captured(3);
|
||
const QString localPort = match.captured(4);
|
||
QString remoteAddress = match.captured(5);
|
||
const QString remotePort = match.captured(6);
|
||
const QString processField = match.captured(7).trimmed();
|
||
|
||
// 去掉地址中的接口后缀(如 192.168.3.83%wlp3s0 -> 192.168.3.83)
|
||
localAddress = localAddress.split(QLatin1Char('%')).first();
|
||
remoteAddress = remoteAddress.split(QLatin1Char('%')).first();
|
||
// 去掉 IPv6 地址的方括号([::1] -> ::1)
|
||
if (localAddress.startsWith(QLatin1Char('['))) localAddress = localAddress.mid(1, localAddress.size() - 2);
|
||
if (remoteAddress.startsWith(QLatin1Char('['))) remoteAddress = remoteAddress.mid(1, remoteAddress.size() - 2);
|
||
|
||
// 提取进程名:users:(("进程名",pid=xxx,fd=xxx)),取第一个双引号内的内容
|
||
QString processName;
|
||
if (processField.startsWith(QStringLiteral("users:"))) {
|
||
QRegularExpression nameRe(QStringLiteral("\\(\"([^\"]+)\""));
|
||
QRegularExpressionMatch nameMatch = nameRe.match(processField);
|
||
if (nameMatch.hasMatch()) {
|
||
processName = nameMatch.captured(1);
|
||
}
|
||
}
|
||
|
||
QVariantMap item;
|
||
item[QStringLiteral("localAddress")] = localAddress;
|
||
item[QStringLiteral("localPort")] = localPort.toInt();
|
||
item[QStringLiteral("remoteAddress")] = remoteAddress;
|
||
item[QStringLiteral("remotePort")] = remotePort.toInt();
|
||
item[QStringLiteral("state")] = QStringLiteral("ESTABLISHED");
|
||
item[QStringLiteral("processName")] = processName;
|
||
list.append(item);
|
||
}
|
||
|
||
m_tcpConnectionList = list;
|
||
emit tcpConnectionListChanged();
|
||
}
|
||
|
||
// 读取指定接口的 WiFi 信号强度(dBm)
|
||
// 仅无线接口(wlp/wlan)有效;/proc/net/wireless 桌面环境可能不存在(返回 0)
|
||
// 该文件每行形如 "wlp3s0: 60. -65. -256 0 ...",level(dBm)为第 3 个数值字段
|
||
int NetworkMonitorApplet::detectWifiSignal(const QString &iface)
|
||
{
|
||
if (!iface.startsWith("wlp") && !iface.startsWith("wlan")) {
|
||
return 0;
|
||
}
|
||
QFile f(QStringLiteral("/proc/net/wireless"));
|
||
if (!f.open(QIODevice::ReadOnly | QIODevice::Text)) {
|
||
return 0;
|
||
}
|
||
QTextStream stream(&f);
|
||
// 跳过前两行标题
|
||
stream.readLine();
|
||
stream.readLine();
|
||
int sig = 0;
|
||
while (!stream.atEnd()) {
|
||
const QString line = stream.readLine().trimmed();
|
||
if (line.isEmpty()) {
|
||
continue;
|
||
}
|
||
const QStringList parts = line.split(QRegularExpression("\\s+"), Qt::SkipEmptyParts);
|
||
if (parts.size() > 3 && parts[0].startsWith(iface)) {
|
||
bool ok = false;
|
||
const double level = parts[3].toDouble(&ok);
|
||
if (ok) {
|
||
sig = qRound(level);
|
||
}
|
||
break;
|
||
}
|
||
}
|
||
f.close();
|
||
return sig;
|
||
}
|
||
|
||
// 判断是否为物理网卡
|
||
// 物理网卡前缀:无线 wlp/wlan/wifi,有线 enp/eth
|
||
// 虚拟接口(Meta/tun/tap/docker/veth/br-)返回 false,
|
||
// 用于自动选择时优先真实网卡而非代理 TUN 接口
|
||
bool NetworkMonitorApplet::isPhysicalInterface(const QString &name) const
|
||
{
|
||
return name.startsWith("wlp") || name.startsWith("wlan")
|
||
|| name.startsWith("enp") || name.startsWith("eth");
|
||
}
|
||
|
||
// ---- 流量日志(日/月持久化)----
|
||
|
||
// 启动时从 traffic_log.json 加载流量日志
|
||
// 优雅降级:文件不存在、读取失败或 JSON 解析失败时均使用空结构,不崩溃不抛异常;
|
||
// 首次运行(文件不存在)时先创建空的 byDay/byMonth 结构,由后续保存写盘创建文件
|
||
void NetworkMonitorApplet::loadTrafficLog()
|
||
{
|
||
QFile file(m_trafficLogPath);
|
||
if (!file.exists()) {
|
||
m_trafficLog = QJsonObject();
|
||
m_trafficLog[QStringLiteral("byDay")] = QJsonObject();
|
||
m_trafficLog[QStringLiteral("byMonth")] = QJsonObject();
|
||
return;
|
||
}
|
||
if (file.open(QIODevice::ReadOnly)) {
|
||
QJsonParseError err;
|
||
const QJsonDocument doc = QJsonDocument::fromJson(file.readAll(), &err);
|
||
file.close();
|
||
if (err.error == QJsonParseError::NoError && doc.isObject()) {
|
||
m_trafficLog = doc.object();
|
||
} else {
|
||
// 解析失败(如文件被外部写坏):降级为空结构,避免后续累加异常
|
||
m_trafficLog = QJsonObject();
|
||
m_trafficLog[QStringLiteral("byDay")] = QJsonObject();
|
||
m_trafficLog[QStringLiteral("byMonth")] = QJsonObject();
|
||
}
|
||
}
|
||
}
|
||
|
||
// 将流量日志写入 traffic_log.json
|
||
// 先裁剪超期记录再写盘,避免日志无限膨胀;
|
||
// 写前 mkpath 确保目录存在(首次运行时目录尚未创建)
|
||
void NetworkMonitorApplet::saveTrafficLog()
|
||
{
|
||
// 测试等场景未初始化日志路径时直接跳过,避免对空路径 mkpath/写文件产生告警
|
||
if (m_trafficLogPath.isEmpty()) {
|
||
return;
|
||
}
|
||
pruneTrafficLog();
|
||
QDir dir;
|
||
dir.mkpath(QFileInfo(m_trafficLogPath).absolutePath());
|
||
QFile file(m_trafficLogPath);
|
||
if (file.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
|
||
// 用 Indented 缩进格式便于用户手动查看/调试日志文件
|
||
file.write(QJsonDocument(m_trafficLog).toJson(QJsonDocument::Indented));
|
||
file.close();
|
||
}
|
||
}
|
||
|
||
// 将本次流量增量累加到当日/当月/当前活动接口的日志记录中(每秒调用,纯内存操作)
|
||
// 跨日/跨月检测:日期或月份与上次记录不同时切换当前记录,不累加到昨天的记录;
|
||
// 只累加活动接口(与 m_totalDownload 口径一致),避免多接口重复计数
|
||
void NetworkMonitorApplet::appendToTrafficLog(qint64 rxDelta, qint64 txDelta)
|
||
{
|
||
if (rxDelta <= 0 && txDelta <= 0) {
|
||
return;
|
||
}
|
||
if (m_activeInterface.isEmpty()) {
|
||
return;
|
||
}
|
||
|
||
// 跨日/跨月检测:更新当前日期/月份,切换记录(旧记录保留不再累加)
|
||
const QString date = QDate::currentDate().toString(QStringLiteral("yyyy-MM-dd"));
|
||
const QString month = QDate::currentDate().toString(QStringLiteral("yyyy-MM"));
|
||
m_currentDate = date;
|
||
m_currentMonth = month;
|
||
|
||
// 累加到按日记录:byDay[date][iface] = {rx, tx}
|
||
// 用 toVariant().toLongLong() 读取并写回 qint64,保留整数精度
|
||
//(直接赋值 double 会导致超过 2^53 的字节数精度丢失)
|
||
QJsonObject byDay = m_trafficLog.value(QStringLiteral("byDay")).toObject();
|
||
QJsonObject dayEntry = byDay.value(date).toObject();
|
||
QJsonObject ifaceDay = dayEntry.value(m_activeInterface).toObject();
|
||
ifaceDay[QStringLiteral("rx")] =
|
||
ifaceDay.value(QStringLiteral("rx")).toVariant().toLongLong() + rxDelta;
|
||
ifaceDay[QStringLiteral("tx")] =
|
||
ifaceDay.value(QStringLiteral("tx")).toVariant().toLongLong() + txDelta;
|
||
dayEntry[m_activeInterface] = ifaceDay;
|
||
byDay[date] = dayEntry;
|
||
m_trafficLog[QStringLiteral("byDay")] = byDay;
|
||
|
||
// 累加到按月记录:byMonth[month][iface] = {rx, tx}
|
||
QJsonObject byMonth = m_trafficLog.value(QStringLiteral("byMonth")).toObject();
|
||
QJsonObject monthEntry = byMonth.value(month).toObject();
|
||
QJsonObject ifaceMonth = monthEntry.value(m_activeInterface).toObject();
|
||
ifaceMonth[QStringLiteral("rx")] =
|
||
ifaceMonth.value(QStringLiteral("rx")).toVariant().toLongLong() + rxDelta;
|
||
ifaceMonth[QStringLiteral("tx")] =
|
||
ifaceMonth.value(QStringLiteral("tx")).toVariant().toLongLong() + txDelta;
|
||
monthEntry[m_activeInterface] = ifaceMonth;
|
||
byMonth[month] = monthEntry;
|
||
m_trafficLog[QStringLiteral("byMonth")] = byMonth;
|
||
}
|
||
|
||
// 裁剪超期记录:按日最多 90 天、按月最多 24 个月,超出删除最旧记录
|
||
// 日期/月份均为 ISO 格式("yyyy-MM-dd"/"yyyy-MM"),字典序即时间序,
|
||
// 排序后取最小的超量个删除,保证始终保留最近的记录
|
||
void NetworkMonitorApplet::pruneTrafficLog()
|
||
{
|
||
QJsonObject byDay = m_trafficLog.value(QStringLiteral("byDay")).toObject();
|
||
QStringList dayKeys = byDay.keys();
|
||
dayKeys.sort();
|
||
while (dayKeys.size() > MAX_DAY_ENTRIES) {
|
||
byDay.remove(dayKeys.first());
|
||
dayKeys.removeFirst();
|
||
}
|
||
m_trafficLog[QStringLiteral("byDay")] = byDay;
|
||
|
||
QJsonObject byMonth = m_trafficLog.value(QStringLiteral("byMonth")).toObject();
|
||
QStringList monthKeys = byMonth.keys();
|
||
monthKeys.sort();
|
||
while (monthKeys.size() > MAX_MONTH_ENTRIES) {
|
||
byMonth.remove(monthKeys.first());
|
||
monthKeys.removeFirst();
|
||
}
|
||
m_trafficLog[QStringLiteral("byMonth")] = byMonth;
|
||
}
|
||
|
||
D_APPLET_CLASS(NetworkMonitorApplet)
|
||
|
||
DS_END_NAMESPACE
|
||
|
||
#include "networkmonitorapplet.moc" |