简 述: 在 DDE 桌面环境中开发 dde-dock 插件 项目实战: 网速插件 lfxSpeed

[TOC]


本文初发于 偕臧 https://xmuli.tech,同步转载于此。


背景

DDE 属实漂亮,使用美观舒适,作为 Deepin 爱好者,习惯于在其它系统上有一个任务栏网速插件,但此 Deepin v20 / UOS v20上没有,于是便有了这个项目。适用于 DDELinux 发行版。


预览

已实现功能:

  • 实时显示网速、CPU 、内存使用率
  • 自定义网速精确度
  • 自定义修改标签
  • 自定义刷新时间
  • 显示或隐藏某一信息栏
  • 网速单位显示模式三种
  • 浅色 / 暗色 主题模式

项目预览图:


编译

开发环境 💻: uos20 📎 Qt 5.11.3 📎 dde-dock = 5.2.0.24 📎 "1.2.2" 版本;安装开发包,

sudo apt-get install libdtkwidget-dev
sudo apt-get install dde-dock-dev

下载 源码 后,进入项目根目录,进行编译

mkdir build && cd build
cmake ..
make -j16

会生成一个 liblfxSpeed.so 共享库,在其路径下执行如下命令,若希望看到效果,需要执行 killall dde-dockdde-dock 后生效;

sudo cp liblfxSpeed.so /usr/lib/dde-dock/plugins     # 安装
sudo rm -rf /usr/lib/dde-dock/plugins/liblfxSpeed.so #  卸载

思路

插件原理

项目最后生成一个动态库,原理是 dde-dock 使用了 Qt5 的插件机制,在运行时候加载动态库。

有一篇 Deepin 官方出的教程,是隐藏 dde-dock 项目之中 plugins-developer-guide 感觉成了一个小彩蛋;这篇文章对插件接口写的很详细,也有一个自带的小例子,讲解的很清晰;

主要工作就是,继承如下两个类:

而自己新写的插件类,是必须同时继承两个类的:如 class SpeedPlugin : public QObject, public PluginsItemInterface 。实现一个最简单的插件类,重写下面前六个函数即可;若加更多功能,再重其它函数即可

// 插件必须重写函数
virtual const QString pluginName() const override;
virtual void init(PluginProxyInterface *proxyInter) override;
virtual QWidget *itemWidget(const QString &itemKey) override;

// 插件禁用和启用相关的接口
virtual bool pluginIsAllowDisable() override;
virtual bool pluginIsDisable() override;
virtual void pluginStateSwitched() override;

// 额外的功能:预览、右键菜单、悬浮显示
virtual const QString pluginDisplayName() const override;
virtual const QString itemContextMenu(const QString &itemKey) override;
virtual void invokedMenuItem(const QString &itemKey, const QString &menuId, const bool checked) override;
virtual QWidget *itemTipsWidget(const QString &itemKey) override;

// 更多其它...

lfxSpeed 原理

基本思路为 获取 /proc 文件,然后通过计算,转换单位等,只显示自己想要的结果;然后将其显示在控件布局之中,最后将整个控件显示在任务栏上(返回其对象的指针给 dock);下面讲解如何从 Linux 通过 /proc 获取系统网速、CPU、Memory和运行时间等信息。


获取系统参数

/proc 介绍

Linux系统上的/proc目录是一种文件系统,即proc文件系统。与其它常见的文件系统不同的是,/proc是一种伪文件系统(也即虚拟文件系统),存储的是当前内核运行状态的一系列特殊文件,用户可以通过这些文件查看有关系统硬件及当前正在运行进程的信息,甚至可以通过更改其中某些文件来改变内核的运行状态。

基于/proc文件系统如上所述的特殊性,其内的文件也常被称作虚拟文件,并具有一些独特的特点。例如,其中有些文件虽然使用查看命令查看时会返回大量信息,但文件本身的大小却会显示为0字节。此外,这些特殊文件中大多数文件的时间及日期属性通常为当前系统时间和日期,这跟它们随时会被刷新(存储于RAM中)有关。

// 更多文件的获取,可以参考 https://zh.m.wikipedia.org/zh-cn/Procfs

#define PROC_PATH_UPTIME    "/proc/uptime"      // "系统启动" 和 "系统空闲" 的时间
#define PROC_PATH_CPU       "/proc/stat"        // "CPU" 使用率 的状态
#define PROC_PATH_MEM       "/proc/meminfo"     // "内存" 和 "交换空间" 的状态
#define PROC_PATH_NET       "/proc/net/dev"     // "网速" 下载和上传 的状态
#define PROC_PATH_DISK      "/proc/diskstats"   // "磁盘" 读取和写入 的状态

获取网速

读取文件

网络相关的数据,从 /proc/net/dev 文件获取:读取此文件进行计算即可

这个文件每一行的详细含义如下:face: 接口

​ ————————————-【接收】————————————-

  1. bytes: 接口接收的数据的总字节数

  2. packets: 接口接收的数据包总数

  3. errs: 由设备驱动程序检测到接收错误的总数

  4. drop: 设备驱动程序丢弃的数据包总数

  5. fifo: FIFO缓冲区错误的数量

  6. frame: 分组帧错误的数量

  7. compressed: 设备驱动程序接收的压缩数据包数

  8. multicast: 设备驱动程序发送或接收的多播帧数

    ————————————-【传送】————————————-

  9. bytes: 接口发送的数据的总字节数

  10. packets: 接口发送的数据包总数

  11. errs: 由设备驱动程序检测到的发送错误的总数

  12. drop: 同上

  13. fifo: 同上

  14. colls: 接口上检测到的冲突数

  15. carrier: 由设备驱动程序检测到的载波损耗的数量

  16. compressed: 设备驱动程序发送的压缩数据包数

需要注意的,,此处只是获取的为接口接收的数据的总字节数 ,要获取实时速率,还得用起差值除以单位时间,才是网速。 其单位默认是字节


思路

此文件是某一时刻的,本机所有接收和发送的数据包总量 ,其分别对应每一行的第 1 列、第 9 列 (byte 一列);(注:第 0 列是前面的英文字符串)

将每一行的的第 2 列累加,为此时可总的接收数据包(下载);将每一行的的第 10 列加,为此时总的发送数据包(上传);然后将两次时刻之差 除以时间间隔,就得到单位时间网速;


代码

这里使用 QRegExp("\\s{1,}") 来分割,莫名会得到第一个切割为 “” 的字符串,所以对应的第 1和 9 列都要顺延加 1。 此问题已经由 #5 修复

/*!
 * \brief SpeedInfo::netRate 获取网某一时刻的网络总的数据包量
 * \param[out] netUpload 网络上传数据量
 * \param[out] netUpload 网络下载数据量
 */
void SpeedInfo::netRate(long &netDown, long &netUpload)
{
    QFile file(PROC_PATH_NET);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {  // 在读取时,把行尾结束符修改为 '\n'; 在写入时,把行尾结束符修改为本地系统换行风格,比如Windows文本换行是 "\r\n"
        qDebug()<<"\"/proc/net/dev\" don't open!";
        return;
    }

    long down = 0;
    long upload = 0;
    QTextStream stream(&file);
    QString line = stream.readLine();
    line  = stream.readLine();
    line  = stream.readLine();
    while (!line.isNull()) {
        line = line.trimmed();
        QStringList list = line.split(QRegExp("\\s{1,}"));   // 匹配任意 大于等于1个的 空白字符

        if (!list.isEmpty()) {
            down = list.at(1).toLong();
            upload = list.at(9).toLong();
        }

        netDown += down;
        netUpload += upload;
        line  = stream.readLine();
    }

    file.close();
}

还需要将字节可以智能转换为对应的单位和对应的数值 的函数:

/*!
 * \brief SpeedInfo::autoRateUnits 自动显示单位
 * \param[in] speed 传入的网速(无单位)
 * \param[out] unit 智能调节后的网速的单位
 * \param sensitive 速率单位的大小写模式
 * \return 自能调节单位后的速率
 */
double SpeedInfo::autoRateUnits(long speed, SpeedInfo::RateUnit &unit)
{
    /* 自动判断合适的速率单位,默认传进来的是 Byte
     * bit    0 ~ 7 位 (不到 1 字节)
     * Byte   1    ~ 2^10  Byte
     * KB     2^10 ~ 2^20  Byte
     * MB     2^20 ~ 2^30  Byte
     * GB     2^30 ~ 2^40  Byte
     * TB     2^40 ~ 2^50  Byte
     */

    if (unit != SpeedInfo::RateByte) {
        qDebug()<<"请先将单位转为字节(byte)后再传参";
        return -1;
    }

    double sp = 0;
    if (0 <= speed && speed < qPow(2, 10)) {
        unit = SpeedInfo::RateByte;
        sp = speed;
    } else if (qPow(2, 10) <= speed && speed < qPow(2, 20)) {
        unit = SpeedInfo::RateKb;
        sp = static_cast<double>(speed / qPow(2, 10) * 1.0);
    } else if (qPow(2, 20) <= speed && speed < qPow(2, 30)) {
        unit = SpeedInfo::RateMb;
        sp = static_cast<double>(speed / qPow(2, 20) * 1.0);
    } else if (qPow(2, 30) <= speed && speed < qPow(2, 40)) {
        unit = SpeedInfo::RateGb;
        sp = static_cast<double>(speed / qPow(2, 30) * 1.0);
    } else if (qPow(2, 40) <= speed && speed < qPow(2, 50)) {
        unit = SpeedInfo::RateTb;
        sp = static_cast<double>(speed / qPow(2, 40) * 1.0);
    } else {
        unit = SpeedInfo::RateUnknow;
        qDebug()<<"本设备网络速率单位传输超过 TB, 或者低于 0 Byte.";
        sp = -1;
    }

    return sp;
}

获取CPU

读取文件

CPU 相关的数据,从 /proc/stat 文件获取:读取此文件进行计算即可

这个文件上面每一列表的详细含义如下:

  1. user: 用户态时间(一般/高优先级,仅统计nice<=0)
  2. nice: 用户态时间(低优先级,nice>0)
  3. system: 内核态时间
  4. idle: 空闲时间( 不包含IO等待时间)
  5. iowait: I/O等待时间 ( 硬盘IO等待时间)
  6. irq: 硬中断
  7. softirq: 软中断
  8. steal: 被盗时间(虚拟化环境中运行其他操作系统上花费的时间(since Linux 2.6.11))
  9. guest: 来宾时间(操作系统运行虚拟CPU花费的时间(since Linux 2.6.24))
  10. guest_nice: nice 来宾时间( 运行一个带nice值的guest花费的时间(since Linux 2.6.33))

最后的几个数值含义:

  1. intr: 系统启动以来的所有interrupts的次数情况(有冗余信息);这行给出中断的信息,第一个为自系统启动以来,发生的所有的中断的次数。然后每个数对应一个特定的中断自系统启动以来所发生的次数。
  2. ctxt: 自系统启动以来CPU发生的上下文交换的次数
  3. btime: 启动时长(单位:秒),从Epoch(即1970零时)开始到系统启动所经过的时长,每次启动会改变。
  4. processes:自系统启动以来所创建的任务的个数目。当短时间该值特别大,系统可能出现异常
  5. procs_running: 当前运行队列的任务的数目
  6. procs_blocked: 当前被阻塞的任务的数目
  7. softirq: 此行显示所有CPU的softirq总数, 第一列是所有软件和每个软件的总数, 后面的列是特定softirq的总数

其单位为 jiffies;其中 1 jiffies = 0.01s = 10ms


思路

和我们相关暂时只关心第一行,每次读取,依旧是某一时刻状态 ,CPU 总量 cpuAll 为第一行所有列之和,空闲量 cpuFree 为第一行的第 4 列。将 (cpuAll - cpuFree) / cpuAll 就是此时刻的 CPU 使用率。

(((cpuAll - old_cpuAll) - (cpuFree - old_cpuFree)) * 100.0 / (cpuAll - old_cpuAll) 则是CPU 在某一单位时间段 的 CPU 使用率。


代码

获取某一时刻 CPU 的总量和空闲量

/*!
 * \brief SpeedInfo::cpuRate 获取某一次 CPU 的使用情况
 * \param[out] cpuAll 总 cpu 使用量
 * \param[out] cpuFree 空闲 cpu 的使用量
 */
void SpeedInfo::cpuRate(long &cpuAll, long &cpuFree)
{
    cpuAll = cpuFree = 0;
    bool ok = false;
    QFile file(PROC_PATH_CPU);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
        return;

    QTextStream stream(&file);
    QString line = stream.readLine();
    if (!line.isNull()) {
        QStringList list = line.split(QRegExp("\\s{1,}"));
        for (auto v = list.begin() + 1; v != list.end(); ++v)
            cpuAll += (*v).toLong(&ok);

        cpuFree = list.at(4).toLong(&ok);
    }

    file.close();
}

获取内存

读取文件

Memory 相关的数据,从 /proc/meminfo 文件获取:读取此文件进行计算即可

此文件的详细解释:

  1. MemTotal: 所有内存(RAM)大小,减去一些预留空间和内核的大小。

  2. MemFree: 完全没有用到的物理内存,lowFree+highFree

  3. MemAvailable: 在不使用交换空间的情况下,启动一个新的应用最大可用内存的大小,计算方式:MemFree+Active(file)+Inactive(file)-(watermark+min(watermark,Active(file)+Inactive(file)/2))

  4. Buffers: 块设备所占用的缓存页,包括:直接读写块设备以及文件系统元数据(metadata),比如superblock使用的缓存页。

  5. Cached: 表示普通文件数据所占用的缓存页。

  6. SwapCached: swap cache中包含的是被确定要swapping换页,但是尚未写入物理交换区的匿名内存页。那些匿名内存页,比如用户进程malloc申请的内存页是没有关联任何文件的,如果发生swapping换页,这类内存会被写入到交换区。

  7. Active: active包含active anon和active file

  8. Inactive: inactive包含inactive anon和inactive file

  9. Active(anon): anonymous pages(匿名页),用户进程的内存页分为两种:与文件关联的内存页(比如程序文件,数据文件对应的内存页)和与内存无关的内存页(比如进程的堆栈,用malloc申请的内存),前者称为file pages或mapped pages,后者称为匿名页。

  10. Inactive(anon): 见上

  11. Active(file): 见上

  12. Inactive(file): 见上

  1. SwapTotal: 可用的swap空间的总的大小(swap分区在物理内存不够的情况下,把硬盘空间的一部分释放出来,以供当前程序使用)

  2. SwapFree: 当前剩余的swap的大小

  3. Dirty: 需要写入磁盘的内存去的大小Writeback: 正在被写回的内存区的大小AnonPages: 未映射页的内存的大小Mapped: 设备和文件等映射的大小

  1. Slab: 内核数据结构slab的大小

  2. SReclaimable: 可回收的slab的大小

  3. SUnreclaim: 不可回收的slab的大小

  4. PageTables: 管理内存页页面的大小

  5. NFS_Unstable: 不稳定页表的大小

  1. VmallocTotal: Vmalloc内存区的大小

  2. VmallocUsed: 已用Vmalloc内存区的大小

  3. VmallocChunk: vmalloc区可用的连续最大快的大小

思路

此为某一时刻系统的内存和交换空间使用情况的截图;

对于内存: 内存总量 memoryAll 为 MemTotal 的数值,空闲内存 memoryFree 为 MemAvailable 的数值,使用中的内存为 memoryUse 为 (MemTotal - MemAvailable);

对于交换空间: 交换空间总量 swapAll 为 SwapTotal 数值,空闲交换空间 swapFree 为 SwapFree 数值,使用中的交换控件量为 swapUse 为 (SwapTotal - SwapFree) 数值;

代码

获取某一时刻内存核交换空间的使用情况,但是这里所有单位都是 字节;

/*!
 * \brief SpeedInfo::memoryRate 获取 “内存” 和 “交换空间” 的某一时刻的使用情况
 * \param memory 内存使用量
 * \param memoryAll 内存总量
 * \param swap 交换空间使用量
 * \param swapAll 交换空间总量
 */
void SpeedInfo::memoryRate(long &memory, long &memoryAll, long &swap, long &swapAll)
{
    memory = memoryAll = 0;
    swap = swapAll = 0;
    bool ok = false;
    QFile file(PROC_PATH_MEM);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text))
        return;

    QTextStream stream(&file);
    long buff[16] = {0};
    for (int i = 0; i <= 15; ++i) {
        QString line = stream.readLine();
        QStringList list = line.split(QRegExp("\\s{1,}"));
        buff[i] = list.at(1).toLong(&ok);

    }

    memoryAll = buff[0];
    memory = buff[0] - buff[2];
    swapAll = buff[14];
    swap = buff[14] - buff[15];

    file.close();
}

获取系统运行时间

读取文件

想在预览里面显示系统开机到现在的运行时间。读取文件为 /proc/uptime

文件一共的两列表含义为:

  1. 系统启动到现在的时间(以秒为单位)
  2. 系统空闲的时间(以秒为单位)

思路

将第一个参数获取获取,然后将这个数值转换为 “ x 天, hh:MM:ss” 的格式,这是我想显示的格式。

代码

获取系统开始到现在的运行时间,单位为 秒;

void SpeedInfo::uptime(double &run, double &idle)
{
    run = 0;
    idle = 0;

    QFile file(PROC_PATH_UPTIME);
    if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) {
        qDebug()<<"\"/proc/uptime\" don't open!";
        return;
    }

    QTextStream stream(&file);
    QString line = stream.readLine();
    QStringList list = line.split(QRegExp("\\s{1,}"));
    if (!list.isEmpty()) {
        run = list.at(0).toDouble();
        idle = list.at(1).toDouble();
    }

    file.close();
}

这里本以为个很容易有现成的 QDateTime 之类的,直接将 秒 转换为想要的时间格式,试了一圈,发现没有,有一个很接近,但是以 1970 零时为开始的,也不符合预期。娜娜的无奈,还是自己动手写吧~

QString SpeedInfo::autoTimeUnits(double s)
{
    int time = qFloor(s);
    int ss = time % 60;
    int MM = (time % 3600) / 60;
    int hh = (time % 86400) / 3600;
    int dd = time / 86400;

    QString runTime = QString(tr("系统已运行: %1天, %2:%3:%4"))
            .arg(dd, 0, 'f', 0, QLatin1Char(' '))
            .arg(hh, 2, 'f', 0, QLatin1Char('0'))
            .arg(MM, 2, 'f', 0, QLatin1Char('0'))
            .arg(ss, 2, 'f', 0, QLatin1Char('0'));

    return runTime;
}

任务栏 1.2.2 的缺陷

开发过程中,又遇到一些如下 dde-dock 的一些 bug:

  1. 插件真实大小只有 图标范围

    这里指的是,无论插件在 dock 布局上,显示有多大,但实际大小都只有如下中间的一个图标大小,右键非红色区域没有响应;

  2. 叫非 datetime 的插件显示都会被压缩

    如果返回的插件名称为非 datetime, 且任务栏高度过低的时候,会出现内容上下被遮盖,显示不全。原因分析参见 #321 (基于 1.2.2);另外在 9188fff1 已经修复(基于 1.2.3 )。补丁暂只提交到 uos 分支,当前开发分支暂时未升级。

  3. 修改布局不会自动刷新

    当我调整的插件布局时候,好像不会立刻 通知到 dock 重新布局,也没有信号重新通知任务栏。也有可能室友,但是我没有找到???或如果有更好的解决方案,可以分享一波???临时的解决方案是,①将布局大小写死 ②手动刷新(卸载此插件后立刻重新加载此插件)。


更新 2020-12-28

此项目 不再维护;但基于其思路和新的实现目标,源码将会重新设计实现,一个功能更加强大的网速插件,也会提供对应的库便于其它开发者二次开发,新的项目在 lfxNet ,芜湖~


下载地址:

lfxspeed


参考: