QT中服务使用

无论是 Windows 还是 Linux,管理服务(启动、停止、重启)的核心思路都是一致的:调用系统提供的命令行工具,或使用专门的 API

下面是不同平台的主流命令及其在 Qt 中的调用方式。

Windows 系统

在 Windows 上,最常用的是 scnet 这两个命令。

1. sc 命令

功能强大、速度较快,推荐使用。基本用法如下:

操作 命令格式 示例 (以 wuauserv 服务为例)
启动服务 sc start <ServiceName> sc start wuauserv
停止服务 sc stop <ServiceName> sc stop wuauserv
查询状态 sc query <ServiceName> sc query wuauserv
配置启动类型 sc config <ServiceName> start= <type> sc config wuauserv start= auto
  • 名称说明 :命令中的 <ServiceName> 是服务的内部名称 (Service Name),而非在服务管理器中看到的"显示名称"。可以通过 sc query 命令查看所有服务的内部名称。

  • 权限提示 :大多数服务的控制操作需要管理员权限

2. net 命令

这是一个更简单的命令,功能不如 sc 强大,但足以满足基本的启停需求。

操作 命令格式 示例
启动服务 net start <ServiceName> net start wuauserv
停止服务 net stop <ServiceName> net stop wuauserv
  • 名称注意:同样需要使用服务的内部名称。

在 Qt 中调用 Windows 命令

在 Qt 中,你可以使用 QProcess 来执行上述系统命令,实现对服务的控制。

核心代码示例:

cpp

复制代码
#include <QProcess>

// 启动服务(以使用 sc 命令为例)
void startService(const QString &serviceName)
{
    // 方法1: 使用 sc 命令
    QProcess::startDetached("sc", {"start", serviceName});
    
    // 方法2: 使用 net 命令
    // QProcess::startDetached("net", {"start", serviceName});
}

// 停止服务
void stopService(const QString &serviceName)
{
    QProcess::startDetached("sc", {"stop", serviceName});
}

// 使用示例
int main()
{
    const QString serviceName = "wuauserv"; // Windows Update 服务的内部名称
    startService(serviceName);
    // stopService(serviceName);
    return 0;
}
  • 注意startDetached 会使启动的进程独立于你的 Qt 应用,适合后台执行。你也可以使用 QProcess::execute 进行同步执行。

更原生的方法:QtServiceController

如果你的 Qt 应用本身是一个服务,且使用了 QtService 来开发,那么你还可以获得更高级、更原生的控制方式。

核心代码示例:

cpp

复制代码
#include <QCoreApplication>
#include "qtservice.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);
    
    // 创建服务控制器,参数为服务内部名称
    QtServiceController controller("MyQtService");
    
    // 启动服务
    if (controller.start())
        qDebug() << "Service started successfully.";
    
    // 停止服务
    if (controller.stop())
        qDebug() << "Service stopped successfully.";
    
    return 0;
}

这种方式将控制逻辑封装在库中,使得代码更加简洁和模块化。

Linux 系统

服务控制命令

systemctl命令

在现代 Linux 发行版中,systemctl 是管理服务的主力工具。

操作 命令格式 示例 (以 nginx 服务为例)
启动服务 sudo systemctl start <service_name> sudo systemctl start nginx
停止服务 sudo systemctl stop <service_name> sudo systemctl stop nginx
重启服务 sudo systemctl restart <service_name> sudo systemctl restart nginx
查看状态 sudo systemctl status <service_name> sudo systemctl status nginx
重载配置 sudo systemctl reload <service_name> sudo systemctl reload nginx
启用开机自启 sudo systemctl enable <service_name> sudo systemctl enable nginx
禁用开机自启 sudo systemctl disable <service_name> sudo systemctl disable nginx

systemctl--user 和不带 --user 的主要区别

特性 不带 --user(系统模式) --user(用户模式)
服务范围 系统全局服务,所有用户共享 仅当前登录用户自己的服务
运行权限 需要 root 权限(大部分操作需 sudo 普通用户权限(无需 sudo
服务配置文件位置 /etc/systemd/system/ /usr/lib/systemd/system/ ~/.config/systemd/user/ /etc/systemd/user/ /usr/lib/systemd/user/
进程管理 由 PID 1(systemd 进程)直接管理 由当前用户下的 systemd --user 进程管理(该进程是用户登录后由 user@<uid>.service 启动的)
服务生命周期 随系统启动/关机而启动/停止 随用户登录/登出而启动/停止(可通过 lingering 让服务在用户未登录时也运行)
环境变量 继承系统环境(如 PATH 由系统配置决定) 继承用户登录时的环境(如 .bashrc.profile 中设置的环境变量)
默认目标 multi-user.targetgraphical.target 等系统目标 default.target(用户会话默认目标)
常用命令示例 sudo systemctl start sshd sudo systemctl enable docker systemctl --user start mpd systemctl --user enable syncthing

**journalctl**查看服务输入的日志

使用 journalctl 查看日志 :这是最直接的方法,尤其是 journalctl -u <service-name>journalctl -u <service-name> -f 命令。

复制代码
# 查看全部日志(可能非常多,建议加过滤)
journalctl

# 实时跟踪新日志(同 tail -f)
journalctl -f

# 查看本次启动后的所有日志
journalctl -b

# 查看指定 systemd 服务的日志(如 ssh、docker、nginx)
journalctl -u ssh.service
journalctl -u docker

# 查看特定优先级的日志(err=错误, warning=警告, info=信息等)
journalctl -p err

# 查看最近 10 分钟内的日志
journalctl --since "10 minutes ago"

# 查看两个时间点之间的日志
journalctl --since "2026-05-21 10:00:00" --until "2026-05-21 12:00:00"

# 查看内核日志(等同于 dmesg 但更丰富)
journalctl -k

# 查看某个 PID 产生的日志
journalctl _PID=1234

# 显示并跟随特定服务的日志
journalctl -u myapp.service -f

# 查看上一次启动的日志(当本次启动有问题时非常有用)
journalctl -b -1

# 按 JSON 格式输出(便于脚本处理)
journalctl -o json-pretty

//查看服务日志并且过滤与usb有关系信息
journalctl -f | grep  usb
  • 注意 :大多数操作需要 sudo 提升权限。

.service 文件格式

.service 文件是 systemd 中最常用的单元类型,用于定义和管理一个守护进程或应用程序。本文介绍其基本结构、主要指令及示例。

文件位置与优先级

作用域 路径 说明
系统服务 /etc/systemd/system/ 管理员自定义,优先级最高
系统服务 /run/systemd/system/ 运行时临时文件
系统服务 /usr/lib/systemd/system/ 软件包安装的默认服务
用户服务 ~/.config/systemd/user/ 用户自定义,优先级最高
用户服务 /etc/systemd/user/ 全系统用户服务覆盖
用户服务 /usr/lib/systemd/user/ 软件包安装的默认用户服务

文件基本结构

一个 .service 文件由多个节(section)组成,每节包含若干键值对。

ini

复制代码
[Unit]
描述和依赖关系

[Service]
服务启动、运行和停止方式

[Install]
如何安装(开机自启等)

1、[Unit] 节 -- 元信息与依赖

指令 说明 示例
Description= 服务的简短描述 Description=My Web Server
Documentation= 文档链接(man、http 等) Documentation=man:sshd(8)
After= 在此所列单元之后启动(不强制依赖) After=network.target
Before= 在此所列单元之前启动 Before=shutdown.target
Requires= 强依赖,本单元启动时这些单元必须启动成功,否则本单元失败 Requires=docker.service
Wants= 弱依赖,仅尝试启动这些单元,失败不影响本单元 Wants=network-online.target
Conflicts= 冲突,本单元启动时会停止所列单元 Conflicts=shutdown.target
Condition... / Assert... 条件检查(文件存在、内核参数等) ConditionFileIsExecutable=/usr/bin/myapp

2、[Service] 节 -- 核心运行参数

1). 服务类型 (Type=)
类型 说明
simple(默认) 主进程由 ExecStart 启动,立即认为启动成功(不 fork)
forking 主进程会 fork 一次后退出,systemd 等待原进程退出后认为启动成功(常用于传统 daemon)
oneshot 一次性任务,启动后立即退出,常与 RemainAfterExit=yes 配合
dbus 进程会在 D-Bus 上注册名称,需要 BusName=
notify 进程会在就绪后通过 sd_notify() 发送通知,需要 NotifyAccess=
idle 在无其他活动任务时才运行(避免占用 CPU)
2). 启动与停止命令
指令 说明 示例
ExecStart= 启动服务时执行的命令(必须绝对路径) ExecStart=/usr/bin/python3 /opt/app.py
ExecStartPre= 启动前执行的准备命令 ExecStartPre=/bin/mkdir -p /var/log/myapp
ExecStartPost= 启动后执行的命令 --
ExecReload= 重载服务配置的命令 ExecReload=/bin/kill -HUP $MAINPID
ExecStop= 停止服务的命令 ExecStop=/bin/kill -TERM $MAINPID
ExecStopPost= 停止后执行的命令 --
RestartSec= 自动重启前的等待秒数 RestartSec=5s
TimeoutStartSec= 启动超时时间 TimeoutStartSec=30s
TimeoutStopSec= 停止超时时间 TimeoutStopSec=10s
3). 重启策略 (Restart=)
含义
no(默认) 不自动重启
on-success 仅当退出码为 0 时重启
on-failure 非零退出码、超时、或被信号终止时重启
on-abnormal 信号终止、超时等异常时重启
on-abort 因未捕获信号崩溃时重启
always 总是重启(包括正常退出)
4). 运行环境与权限
指令 说明
User= 运行服务的用户名(推荐非 root)
Group= 运行服务的用户组
WorkingDirectory= 工作目录
Environment= 设置环境变量,如 Environment=FOO=bar
EnvironmentFile= 从文件加载环境变量(每行 KEY=value
UMask= 文件创建掩码,如 UMask=0022
Nice= 调度优先级(-20 最高,19 最低)
LimitNOFILE= 最大打开文件数
LimitMEMLOCK= 锁定的内存上限
PrivateTmp= 使用私有 /tmp/var/tmp(默认 false)
NoNewPrivileges= 禁止进程获得新权限(推荐 true)
ProtectSystem= 保护系统目录(/usr, /boot),可选 full/strict
ReadOnlyPaths= 设置只读路径
ReadWritePaths= 设置可写路径(与只读冲突时优先)
5). 进程管理
指令 说明
PIDFile= 用于 Type=forking 时指定 PID 文件路径
RemainAfterExit= Type=oneshot 时使用,退出后仍视为 active
KillMode= 如何杀死进程:control-group(默认,杀死全部)、process(仅主进程)、mixednone
KillSignal= 发送的终止信号,默认 SIGTERM
Restart= 见上文重启策略
RestartPreventExitStatus= 遇到这些退出码时禁止重启
SuccessExitStatus= 视为正常退出的额外退出码/信号

3、[Install] 节 -- 安装/启用配置

指令 说明 示例
WantedBy= 指定由哪个目标(target)所需(通常是 multi-user.targetdefault.target WantedBy=multi-user.target
RequiredBy= 强依赖目标 --
Also= 启用/禁用本服务时,同时启用/禁用的其他单元 --
Alias= 为单元设置别名 Alias=ssh.service

注意[Install] 节只在执行 systemctl enable/disable 时使用,不会影响运行时状态。

4、完整示例

示例1:简单系统服务(Type=simple)

ini

复制代码
[Unit]
Description=My Custom App
After=network.target

[Service]
ExecStart=/usr/local/bin/myapp --config /etc/myapp/config.ini
Restart=on-failure
RestartSec=5s
User=nobody
Group=nogroup
WorkingDirectory=/var/lib/myapp
Environment=ENV=production
PrivateTmp=yes
NoNewPrivileges=yes

[Install]
WantedBy=multi-user.target
示例2:一次性任务(Type=oneshot)

ini

复制代码
[Unit]
Description=Cleanup Temp Files

[Service]
Type=oneshot
ExecStart=/usr/local/bin/cleanup.sh
RemainAfterExit=no

[Install]
WantedBy=multi-user.target
示例3:传统 daemon(Type=forking)

ini

复制代码
[Unit]
Description=Legacy Daemon
After=network.target

[Service]
Type=forking
ExecStart=/usr/sbin/legacyd -d
PIDFile=/run/legacyd.pid
User=legacy
Restart=on-failure

[Install]
WantedBy=multi-user.target
示例4:用户服务(~/.config/systemd/user/myapp.service)

ini

复制代码
[Unit]
Description=User-level Syncthing

[Service]
ExecStart=/usr/bin/syncthing serve --no-browser
Restart=on-failure
RestartSec=10

[Install]
WantedBy=default.target

用户服务启用:systemctl --user enable myapp.service

并可通过 loginctl enable-linger $USER 使其在未登录时也运行。

5、常用调试命令

命令 用途
systemctl cat service_name 查看合并后的完整单元文件
systemctl show service_name 显示当前加载的配置(含默认值)
systemctl edit --full service_name 编辑或覆盖服务文件
systemctl edit service_name 创建 drop-in 片段,不覆盖原文件
systemctl daemon-reload 重新加载所有单元文件(修改后必须执行)

6、注意事项

  1. 绝对路径Exec* 中的命令必须使用绝对路径,除非指定 $PATH 环境(但建议绝对路径)。

  2. 命令行语法 :空格和特殊字符需适当转义(可用 \ 或引号),更推荐使用多参数形式:ExecStart=/bin/bash -c "echo hello"

  3. 环境变量继承 :服务默认继承 systemd 的干净环境,不会自动继承用户 shell 环境;需要显式设置 EnvironmentEnvironmentFile

  4. 服务依赖循环 :避免 AfterBefore 形成循环,systemd 会检测并报错。

  5. 修改后生效 :任何 .service 文件改动后都需执行 systemctl daemon-reload,然后 systemctl restart name.service

在 Qt 中调用 Linux 命令

同样可以使用 QProcess 来执行 systemctl 命令。

核心代码示例:

cpp

复制代码
#include <QProcess>

void startService(const QString &serviceName)
{
    // 使用 systemctl 启动服务
    QProcess::startDetached("sudo", {"systemctl", "start", serviceName});
}

void stopService(const QString &serviceName)
{
    QProcess::startDetached("sudo", {"systemctl", "stop", serviceName});
}

// 使用示例
int main()
{
    const QString serviceName = "nginx";
    startService(serviceName);
    // stopService(serviceName);
    return 0;
}
  • 权限处理 :由于 systemctl 通常需要 sudo,这可能会要求用户输入密码。你可以考虑通过配置 sudoers 文件来允许特定命令免密执行,以避免交互。

示例(实现软件升级功能)

实现软件升级功能。升级功能部署流程:

一、升级守护服务配置

1、在 /etc/systemd/system/ 目录下创建一个服务文件update-daemon.service

复制代码
[Unit]
Description=Software Upgrade Daemon
After=multi-user.target

[Service]
# 可执行文件的完整路径(建议用绝对路径)

WorkingDirectory=/home/dq
Environment="LD_LIBRARY_PATH=/home/dq:/home/dq/Qt-5.15.2-arm/lib:/usr/local/cuda/lib64:/usr/lib/aarch64-linux-gnu"
# 将 sleep 5 改为 sleep 1
ExecStart=/bin/bash -c "sleep 10 && /home/dq/upgrade-daemon"
# 重启策略
Restart=always
#减少重启等待时间
RestartSec=5

# 环境变量(如果需要,比如 DISPLAY 或 QT_QPA_PLATFORM)
# Environment=DISPLAY=:0
# Environment=QT_QPA_PLATFORM=wayland  # 或 xcb,根据你的系统

# 日志输出(可选,方便调试)
StandardOutput=journal
StandardError=journal

[Install]
WantedBy=default.target

2、将upgrade-daemon可执行程序拷贝到/home/dq/目录

服务中对应的程序(升级守护进程)

updatedaemon.h

复制代码
#ifndef UPDATEDAEMON_H
#define UPDATEDAEMON_H

#include <QObject>
#include <QLocalServer>
#include <QString>

class UpdateDaemon : public QObject
{
    Q_OBJECT
public:
    explicit UpdateDaemon(QObject *parent = nullptr);
    ~UpdateDaemon();

private slots:
    void onNewConnection();

private:
    void performUpdate(bool success,  QString ouputZipFilePath, QString storeFilePath);

    QLocalServer *server;
    QString socketPath;

    bool m_success;
    QString m_ouputZipFilePath;
    QString m_storeFilePath;
};

#endif // UPDATEDAEMON_H

updatedaemon.cpp

bash 复制代码
#include "updatedaemon.h"
#include <QLocalSocket>
#include <QProcess>
#include <QThread>
#include <QDebug>
#include <QDateTime>
#include <QDir>
#include <QFileInfo>
#include <QTimer>
#include <QCoreApplication>
#include <QJsonDocument>
#include <QJsonObject>

UpdateDaemon::UpdateDaemon(QObject *parent)
    : QObject(parent), socketPath("/tmp/update_trigger.sock")
{
    m_success = false;
    m_ouputZipFilePath = "";
    m_storeFilePath = "";

    QLocalServer::removeServer(socketPath);
    server = new QLocalServer(this);
    // 只允许 root 用户连接(守护进程以 root 运行)
    //server->setSocketOptions(QLocalServer::UserAccessOption);
    // 允许任何用户连接
    server->setSocketOptions(QLocalServer::WorldAccessOption);
    if (!server->listen(socketPath)) {
        qCritical() << "无法监听 socket:" << server->errorString();
        QCoreApplication::exit(1);
    }
    connect(server, &QLocalServer::newConnection, this, &UpdateDaemon::onNewConnection);
    qDebug() << "升级守护进程已启动,监听:" << socketPath;
}

UpdateDaemon::~UpdateDaemon()
{
    QLocalServer::removeServer(socketPath);
}

void UpdateDaemon::onNewConnection()
{
    QLocalSocket *client = server->nextPendingConnection();
    if (!client) return;

    m_success = false;
    m_ouputZipFilePath = "";
    m_storeFilePath = "";

    // 读取客户端数据
    connect(client, &QLocalSocket::readyRead, [this, client]() {
        QByteArray jsonData =  client->readAll();
        try{
            QJsonParseError error;
            QJsonDocument doc = QJsonDocument::fromJson(jsonData, &error);
            if (error.error == QJsonParseError::NoError) {
                if (doc.isObject()) {
                    QJsonObject obj = doc.object();
                    if("upgrade" == obj["action"].toString()){
                        m_ouputZipFilePath = obj["zipFilePath"].toString();
                        m_storeFilePath = obj["unzipStorePath"].toString();
                        m_success = true;
                    }
                    else{
                        qDebug() << "action:" << obj["action"].toString();
                    }
                }
                else {
                    qDebug() << "JSON 不是一个对象.";
                }
            }
            else {
                qDebug() << "解析失败:" << error.errorString();
            }
        }
        catch(...){
            qDebug() << "解析异常";
        }

        qDebug() << "收到升级触发信号,开始执行升级流程...";

        QJsonObject jsonResponse;
        jsonResponse["action"] = "upgrade response";
        jsonResponse["errorCode"] = 0;
        // 转换为 QJsonDocument
        QJsonDocument doc(jsonResponse);
        // 转换为字节数组(默认紧凑格式)
        QByteArray jsonResponseData = doc.toJson();
        // 转为 QString
        QString jsonResponseString = QString::fromUtf8(jsonResponseData);
        client->write(jsonResponseString.toUtf8());
        client->flush();
        QTimer::singleShot(100, client, &QLocalSocket::disconnectFromServer);

        performUpdate(m_success, m_ouputZipFilePath, m_storeFilePath);  // 执行升级(最终会重启系统)
    });
    connect(client, &QLocalSocket::disconnected, client, &QLocalSocket::deleteLater);
}

void UpdateDaemon::performUpdate(bool success,  QString ouputZipFilePath, QString storeFilePath)
{
    if(!success || ouputZipFilePath.isEmpty() || storeFilePath.isEmpty()){
        return;
    }

    // 停止进程
    {
        QString stopFilePath = QDir(storeFilePath).filePath("stop.sh");
        if(QFile::exists(stopFilePath )){
        // 获取当前权限
        QFile::Permissions perms = QFile::permissions(stopFilePath );
        // 添加所有者执行权限(必要时可加 Group/Other)
        perms |= QFile::ExeUser;      // 用户执行
        // perms |= QFile::ExeGroup;  // 组执行(可选)
        // perms |= QFile::ExeOther;  // 其他用户执行(可选)

            if (!QFile::setPermissions(stopFilePath , perms)) {
                qWarning() << "Failed to set executable permission on" << stopFilePath ;
            }

            QProcess processStop;
            processStop.start("/bin/bash", QStringList() << stopFilePath);
            // 等待脚本执行完成(最多等待 300 秒)
            if (processStop.waitForFinished(300000)) {
                QByteArray output = processStop.readAllStandardOutput();
                QByteArray error = processStop.readAllStandardError();
                qDebug() << "Output:" << output;
                qDebug() << "Error:" << error;
                qDebug() << "Exit code:" << processStop.exitCode();
            } 
            else {
            qDebug() << "stop Process timeout or error:" << processStop.errorString();
            }
        }
        else{
            qDebug() << "停止进程 dataCollect...";
            QProcess::execute("pkill", {"-f", "dataCollect"});
            QThread::sleep(2);
            QProcess check;
            check.start("pgrep", {"-f", "dataCollect"});
            check.waitForFinished();
            if (check.exitCode() == 0) {
                qDebug() << "进程未响应,强制终止";
                QProcess::execute("pkill", {"-9", "-f", "dataCollect"});
                QThread::sleep(1);
            }
        }
    }


    // 备份指定文件和目录
    qDebug() << "备份指定文件和目录...";
    QString backupFilePath = QDir(storeFilePath).filePath("backup.sh");
    if(QFile::exists(backupFilePath)){
        // 获取当前权限
        QFile::Permissions perms = QFile::permissions(backupFilePath);
        // 添加所有者执行权限(必要时可加 Group/Other)
        perms |= QFile::ExeUser;      // 用户执行
        // perms |= QFile::ExeGroup;  // 组执行(可选)
        // perms |= QFile::ExeOther;  // 其他用户执行(可选)

        if (!QFile::setPermissions(backupFilePath, perms)) {
            qWarning() << "Failed to set executable permission on" << backupFilePath;
        }
    }
    QProcess processBackup;
    processBackup.start("/bin/bash", QStringList() << backupFilePath);
    // 等待脚本执行完成(最多等待 300 秒)
    if (processBackup.waitForFinished(300000)) {
        QByteArray output = processBackup.readAllStandardOutput();
        QByteArray error = processBackup.readAllStandardError();
        qDebug() << "Output:" << output;
        qDebug() << "Error:" << error;
        qDebug() << "Exit code:" << processBackup.exitCode();
    } else {
        qDebug() << "Backup Process timeout or error:" << processBackup.errorString();
    }

    //可选命令(支持多个)
    {
        //optional-commands
        QString path = QDir(storeFilePath).filePath("optional-commands");
        QDir dir(path);
        dir.setFilter(QDir::Files);                // 只显示文件
        dir.setNameFilters(QStringList() << "*.sh"); // 可选:只筛选 .sh 文件
        dir.setSorting(QDir::Name);                // 按文件名升序排序
        QStringList shFiles = dir.entryList();     // 返回已排序的文件名列表
        for (const QString &file : shFiles) {
            QString cmdFullPath = dir.absoluteFilePath(file);
            qDebug() << cmdFullPath;
            if(QFile::exists(cmdFullPath)){
                // 获取当前权限
                QFile::Permissions perms = QFile::permissions(cmdFullPath);
                // 添加所有者执行权限(必要时可加 Group/Other)
                perms |= QFile::ExeUser;      // 用户执行
                // perms |= QFile::ExeGroup;  // 组执行(可选)
                // perms |= QFile::ExeOther;  // 其他用户执行(可选)

                if (!QFile::setPermissions(cmdFullPath, perms)) {
                    qWarning() << "Failed to set executable permission on" << cmdFullPath;
                }

                qInfo() <<"start execute " << cmdFullPath;

                QProcess processBackup;
                processBackup.start("/bin/bash", QStringList() << cmdFullPath);
                // 等待脚本执行完成(最多等待 300 秒)
                if (processBackup.waitForFinished(300000)) {
                    QByteArray output = processBackup.readAllStandardOutput();
                    QByteArray error = processBackup.readAllStandardError();
                    qDebug() << "Output:" << output;
                    qDebug() << "Error:" << error;
                    qDebug() << "Exit code:" << processBackup.exitCode();
                } else {
                    qDebug() << "Backup Process timeout or error:" << processBackup.errorString();
                }
            }
        }
    }

    // 替换文件
    // copy文件,添加所有者执行权限
    QString cpFilePath = QDir(storeFilePath).filePath("upgrade-copy.sh");
    if(!QFile::exists(cpFilePath)){
        return;
    }
    // 获取当前权限
    QFile::Permissions perms = QFile::permissions(cpFilePath);
    // 添加所有者执行权限(必要时可加 Group/Other)
    perms |= QFile::ExeUser;      // 用户执行
    // perms |= QFile::ExeGroup;  // 组执行(可选)
    // perms |= QFile::ExeOther;  // 其他用户执行(可选)

    if (!QFile::setPermissions(cpFilePath, perms)) {
        qWarning() << "Failed to set executable permission on" << cpFilePath;
    }
    qDebug() << "替换新文件...";
    QProcess processCopy;
    processCopy.start("/bin/bash", QStringList() << cpFilePath);
    // 等待脚本执行完成(最多等待 300 秒)
    if (processCopy.waitForFinished(300000)) {
        QByteArray output = processCopy.readAllStandardOutput();
        QByteArray error = processCopy.readAllStandardError();
        qDebug() << "Output:" << output;
        qDebug() << "Error:" << error;
        qDebug() << "Exit code:" << processCopy.exitCode();
    } else {
        qDebug() << "Copy Process timeout or error:" << processCopy.errorString();
    }

    // 重启系统
    qDebug() << "升级完成,3秒后重启...";
    QThread::sleep(3);
    QProcess::execute("reboot");
}

main.cpp

复制代码
#include <QCoreApplication>
#include "updatedaemon.h"

int main(int argc, char *argv[])
{
    QCoreApplication a(argc, argv);

    // Set up code that uses the Qt event loop here.
    // Call a.quit() or a.exit() to quit the application.
    // A not very useful example would be including
    // #include <QTimer>
    // near the top of the file and calling
    // QTimer::singleShot(5000, &a, &QCoreApplication::quit);
    // which quits the application after 5 seconds.

    // If you do not need a running Qt event loop, remove the call
    // to a.exec() or use the Non-Qt Plain C++ Application template.

    UpdateDaemon daemon;
    return a.exec();
}

客户端(触发升级动作,发送到守护)

cpp 复制代码
QString cpFilePath = QDir(storeFilePath).filePath("upgrade-copy.sh");
if(QFile::exists(cpFilePath)){
          //triger upgrade
          QLocalSocket socket;
          socket.connectToServer("/tmp/update_trigger.sock");
          if (!socket.waitForConnected(3000)) {
             qCritical() << "无法连接守护进程:" << socket.errorString();
                                            QFile::remove(ouputZipFilePath);
             return ;
         }
         QJsonObject json;
         json["action"] = "upgrade";
         json["zipFilePath"] = ouputZipFilePath;
         json["unzipStorePath"] = storeFilePath;
         // 转换为 QJsonDocument
          QJsonDocument doc(json);
         // 转换为字节数组(默认紧凑格式)
         QByteArray jsonData = doc.toJson();
         // 转为 QString
         QString jsonString = QString::fromUtf8(jsonData);
         socket.write(jsonString.toUtf8());
         socket.flush();

         qDebug()<<"send data:" << json;

         if (socket.waitForReadyRead(2000)) {
                                    qDebug() << "响应:" << socket.readAll().trimmed();
          } else {
              qDebug() << "未收到响应,但触发已发送";
         }
          socket.disconnectFromServer();
          qDebug()<<"triger upgrade notify";
         }
         else{
              qDebug() << "没有复制脚本文件.";
              QFile::remove(ouputZipFilePath);
         }

扫描加载u盘中固定目录(比如:upgrade-files)中是否存在升级文件包。存在并解析升级包信息。

3、将stop_and_backup.sh文件放到升级包upgrade-files目录下。

bash 复制代码
#!/bin/bash
# ============================================
# stop_and_backup.sh
# 功能: 1) 停止主进程 dataCollect
#       2) 备份指定的文件和文件夹到备份目录
# ============================================

set -e

# ---------- 配置区域 ----------
PROCESS_NAME="dataCollect"   # 主进程名称

# 备份源列表:可以写文件或目录的绝对路径
BACKUP_SOURCES=(
    "/home/dq/dataCollect"
    "/home/dq/Config.ini"
    "/home/dq/libHCUSBSDK.so"
    "/home/dq/hikvision_thermal/"               # 整个目录
)

# 备份目标根目录(会在下面创建按时间戳的子目录)
BACKUP_BASE="/mnt/dq_root/asmq_backup"
# -----------------------------

if [ "$EUID" -ne 0 ]; then
    echo "请使用 root 权限运行此脚本"
    exit 1
fi

# 1. 停止主进程
echo "[1/2] 停止进程: $PROCESS_NAME"
# 尝试优雅终止 (SIGTERM)
pkill -f "$PROCESS_NAME" && echo "已发送 SIGTERM" || echo "未找到进程 $PROCESS_NAME"
sleep 3

# 检查是否还在运行,若有则强制杀死 (SIGKILL)
if pgrep -f "$PROCESS_NAME" > /dev/null; then
    echo "进程未响应,强制终止"
    pkill -9 -f "$PROCESS_NAME" || true
    sleep 1
fi

# 最终确认
if pgrep -f "$PROCESS_NAME" > /dev/null; then
    echo "错误: 无法停止进程 $PROCESS_NAME"
    exit 1
fi
echo "进程已停止"

# 2. 备份指定文件和文件夹
echo "[2/2] 开始备份指定内容到 $BACKUP_BASE"

# 创建带时间戳的备份目录
#TIMESTAMP=$(date +%Y%m%d_%H%M%S)
#BACKUP_DIR="$BACKUP_BASE/$TIMESTAMP"
BACKUP_DIR="$BACKUP_BASE/asmq_stable"
[ -d "$BACKUP_DIR" ] && rm -rf "$BACKUP_DIR"
mkdir -p "$BACKUP_DIR"

# 遍历备份源列表,逐个复制
for src in "${BACKUP_SOURCES[@]}"; do
    if [ -e "$src" ]; then
        # 目标路径:保留源路径的最后一级名称(如果是目录则复制整个目录)
        # 例如 /opt/myapp/config/settings.ini -> $BACKUP_DIR/settings.ini
        # 例如 /opt/myapp/models/ -> $BACKUP_DIR/models/
        base_name=$(basename "$src")
        dest="$BACKUP_DIR/$base_name"
        
        echo "  复制 $src -> $dest"
        # 使用 cp -a 保留权限、时间戳等
        cp -a "$src" "$dest"
    else
        echo "  警告: 源文件/目录不存在,跳过: $src"
    fi
done

echo "备份完成,所有内容已保存到: $BACKUP_DIR"
ls -lh "$BACKUP_DIR"

4、配置服务为开机自启

sudo systemctl enable update-daemon

二、配置udev规则,以便实时检测u盘插入/卸载

将99-usb-automount.rules文件拷贝到/etc/udev/rules.d目录中。

bash 复制代码
ACTION=="add", SUBSYSTEM=="block", ENV{ID_BUS}=="usb", ENV{ID_FS_USAGE}=="filesystem", \
    RUN+="/usr/bin/systemd-mount --no-block --collect --options=uid=1000,gid=1000,umask=000 /dev/%k /mnt/usb"
ACTION=="remove", SUBSYSTEM=="block", ENV{ID_BUS}=="usb", ENV{ID_FS_USAGE}=="filesystem", \
    RUN+="/usr/bin/systemd-mount --no-block --umount /mnt/usb"

三、制作升级包

利用UpgradeTool工具制作升级包(提前存放文件和文件夹,以及upgrade-copy.sh脚本)

bash 复制代码
#!/bin/bash
workdir=/mnt/dq_root/upgrade_temp/upgradefiles/

# 复制文件 netmanager.cpp 到 /home/dq/ 目录
cp -f "$workdir"/netmanager.cpp /home/dq/

# 替换文件 netmanager.h(目标已存在则覆盖)
cp -f "$workdir"/netmanager.h /home/dq/netmanager.h

cp -f "$workdir"/dataCollect/home/dq/dataCollect

# 复制 subdir 文件夹及其所有内容到 /home/dq/ 下
cp -rf "$workdir"/subdir/ /home/dq/

四、升级包存放到u盘固定目录中

将做好的升级包文件(比如:upgrade-20260421161205.bin)拷贝到u盘根目录下upgrade-files目录中。

相关推荐
mqiqe2 小时前
AgentScope Java Harness:4. 双层记忆系统 让 Agent 拥有真正的“长期大脑“
java·开发语言
gugucoding2 小时前
46. 【Java】JUC并发工具:让并发更简单
java·开发语言
Nebula_g2 小时前
JavaSE基础语法:面向对象高级(代码中的成分)
java·开发语言·编程·javase·技术栈·高级语法
for_ever_love__3 小时前
python基础语法学习: 异常的传递性
开发语言·python·学习
依然鸣3 小时前
PTA团体程序设计天梯赛L1真题讲解L1-077-080
开发语言·c++·算法·深度优先·pat考试·图论
库玛西5 小时前
现代 C++ 智能指针全景指南:从 RAII 思想到工业级实践
c语言·开发语言·c++·笔记·面试
liulilittle5 小时前
无锁并发容器的设计与实现原理
开发语言·c++·set·map·并发·无锁·lock-free
朋克洛德的码农6 小时前
Go并发-sync包四剑客:Mutex、RWMutex、WaitGroup、Once-从入门到原理
开发语言·后端·golang
淼澄研学7 小时前
Kimi API黑产倒卖技术解析与Python合规接入指南
开发语言·网络·python