# UOS 系统急救卫士 **Repository Path**: deepin20/uos-guardian ## Basic Information - **Project Name**: UOS 系统急救卫士 - **Description**: UOS 系统急救卫士是一款专为统信 UOS 专业版打造的轻量级系统运维与安全防护工具。它能有效预防因误删核心系统组件导致的桌面崩溃,并提供了在极端的 Live 环境下“一键急救恢复”桌面的能力。 - **Primary Language**: Python - **License**: MIT - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-07-27 - **Last Updated**: 2026-10-02 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # UOS 系统急救卫士 (UOSGuardian) [![UOS 兼容](https://img.shields.io/badge/UOS-20%20Professional-blue)](https://www.chinauos.com/) [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](LICENSE) **UOS 系统急救卫士**是一款专为统信 UOS 专业版打造的轻量级系统运维与安全防护工具。它能有效预防因误删核心系统组件导致的桌面崩溃,并提供了在极端的 Live 环境下“一键急救恢复”桌面的能力。 > 🛡️ **核心理念**:防范于未然,救助于危难。 ## ✨ 核心功能 - **🩺 系统体检(首页)**:12 项只读检查一次跑完,逐项给出「是否需要修复」与「如何修复」。自动覆盖核心桌面包完整性与锁定状态、`/dev/null` 设备节点、dpkg 数据库一致性、APT 源配置、根分区剩余空间、桌面快捷方式权限、系统语言环境、后台监控服务、当前内核关键文件、玲珑运行环境、输入法残留。低风险项支持**一键修复并在修复后自动复检**,高风险项(内核、dpkg、换源)只给手动命令建议,绝不擅自执行。体检结果可**一键导出为 TXT / JSON 报告**。 - **🆘 Live 救援体检**:系统损坏无法开机时,用 Live U 盘启动电脑并运行本工具,在「Live 救援体检」页即可**扫描发现硬盘上的已安装系统**(只读探测,绝不改数据)→ **挂载目标分区** → **对损坏系统做完整 12 项体检**(命令自动 chroot 进目标系统执行)→ **一键修复低风险项**(锁定核心包、重建 `/dev/null`、修复快捷方式权限、重置语言环境等,全部作用于目标系统)→ **安全卸载**后拔 U 盘重启。自动检测 Live 环境并给出操作引导;非 Live 环境下也可手动使用,但会明确提示。 - **🛡️ 安全卸载预演**:在卸载软件前,通过后台模拟分析,精准探测是否会连带删除 `dde-shell`/`dde-dock`、`lightdm` 等核心桌面包。如有危险,即刻红字警告,防止“连坐崩盘”。 - **🚑 一键急救修复**:内置了完备的救援脚本。在 Deepin/UOS Live 环境下,支持自动挂载虚拟目录(幂等,可重复执行)、注入网络 DNS、`chroot` 进入损坏系统、一键换清华源(先自动备份原源)并重装核心桌面组件。修复脚本经 stdin 直喂 chroot,并自动识别目标系统代号(deepin 23=eagle / 25=crane)与新旧架构包名,逐包安装、旧包名自动跳过。目标系统**由扫描硬盘得出**(不再预置猜测路径),选中后会校验它确实是系统根目录,并在开修前二次确认;修完自动解除 bind mount 并 `sync` 落盘,避免把目标文件系统留在半挂状态。 - **🧹 桌面扫尾工具**:解决系统修复后遗症。支持一键恢复 `.desktop` 文件权限(解决快捷方式变白文件问题),以及一键重置系统语言环境(解决右键菜单变为英文的问题)。另提供玲珑应用恢复、输入法急救、内核救援信息、`/opt` 本地软件缺失扫描。 - **📦 终端静默安装器**:无需依赖图形化应用商店,在终端输入包名即可静默安装软件。**特别采用 `shlex.quote` 安全转义机制**,彻底杜绝通过输入框执行 `rm -rf` 等命令注入风险。 - **⚙️ 主动防御后台监控**:基于 `systemd` 和 `psutil` 的后台守护进程。监测到用户在交互终端执行针对核心包的 `apt remove / purge` 时,**立即强制杀死高危进程**并弹出桌面通知(带 60 秒通知冷却)。模拟预演(`--dry-run`)、`autoremove` 与无终端的系统自动更新任务一律放行,不会误伤正常升级。 - **🎨 UOS 原生视觉**:遵循 DDE 设计规范,配备极简蓝白渐变图标与纯净的无干扰界面。 ## 🩺 系统体检说明 打开软件即自动体检一次(也可随时手动重跑)。每项都会明确输出「是否需要修复」和「如何修复」。 | 体检项 | 判定依据 | 需要修复时如何处理 | |---|---|---| | 核心桌面包安装完整性 | 按「角色 → 候选包」判定,自动适配 deepin 25 组件更名(`dde-dock`→`dde-shell`、`dde-launcher`→`dde-launchpad`;`dde-core` 元包不再要求) | 缺失且软件源有替代包:手动重装(**只建议真实存在的包名**);候选包全部被淘汰:提示人工确认,不判定为异常 | | 核心桌面包锁定状态 | `apt-mark showhold` 是否覆盖全部**实际在位**的核心包(已安装 ∪ 源中可用) | **一键修复**:自动 `apt-mark hold`(只锁在位的包) | | `/dev/null` 设备节点 | 是否为字符设备 1:3 且权限 666 | **一键修复**:重建节点并校验 | | dpkg 数据库一致性 | `dpkg --audit` 是否有输出 | 手动:`dpkg --configure -a` 等 | | APT 软件源配置 | `sources.list` + `sources.list.d` 是否有有效条目 | 手动:恢复备份或重配官方源 | | 根分区剩余空间 | 剩余 < 2 GB 注意(< 512 MB 异常) | 手动:清理 apt 缓存 / 日志 / 回收站 | | 桌面快捷方式权限 | 桌面 `.desktop` 是否缺少可执行位 | **一键修复**:批量 `chmod +x` | | 系统语言环境 | `/etc/default/locale` 与当前会话是否均为中文 | **一键修复**:写入配置并重建 locale(需注销生效) | | 后台防御监控服务 | `systemctl is-active uos-guardian.service`(指本工具自身的守护服务,与 senseshield 等第三方服务无关) | **一键修复**:自动启动服务 | | 当前内核关键文件 | `/boot` 下 `vmlinuz/config/initrd` 是否齐全 | 手动:仅给命令建议(高风险,程序不代跑) | | 玲珑运行环境 | `linglong-bin` 与 `ll-cli` 是否就绪 | 手动:跳转「桌面扫尾工具」的一键恢复 | | 输入法残留 | 搜狗包/残留目录是否存在 | 手动:跳转「桌面扫尾工具」的输入法急救 | **安全边界**:体检过程**只读**,不会改动系统;只有上表中标注「一键修复」的低风险项可自动执行,且必须由用户点击按钮触发。修复完成后程序会自动复检一次,让结果可验证。 ### 两个已修复的误报坑(报告数字"跳变"时先看这里) 1. **`apt-mark hold` 之后,核心包体检曾误报"未知"**。`apt-mark hold` 会把 dpkg 里该包的期望状态(want)改写成 `hold`,`dpkg-query -f='${Package} ${Status}'` 的输出随之从 `install ok installed` 变成 **`hold ok installed`**;旧解析器只认 `install ok installed`,于是把已锁定的核心包全当成"没装"——1/12 变成未知、2/12 变成"未发现任何在位的核心组件"。**本工具自己的「一键锁定核心包」就会触发这个坑**。现在只要实际状态是 `ok installed` 就算在位,与期望状态无关(`hold` / `deinstall` 都认)。 2. **中文系统上 `apt-cache policy` 的标签是中文**。apt 的 zh_CN 翻译包里收录了 `' Candidate: ' → ' 候选:'`,逐字匹配英文标签的解析器会一条候选都拿不到,把"包在源中"误判成"已更名淘汰"。现在所有需要**解析输出**的命令(dpkg / apt-cache / apt-mark / dpkg -l …)一律经 C 语言环境执行(`utils/cmd.py`),解析器同时兼容中英标签。 体检结果的文案也会自我说明:桌面快捷方式一项会写明**扫描了哪个目录**(提权后落到 `/root` 会看到"桌面快捷方式从 1 个变成 7 个"这类跳变),核心包一项在"一个在位包都读不到"时会直接提示**运行环境受限**,而不是含糊地说"组件可能已更名"。 ### 导出体检报告 体检完成后点击「**导出体检报告**」,可选择保存为两种格式。报告默认落在**当前登录用户的用户级主目录**(如 `/home/deepin`,不再往 `~/Documents` 等子目录里塞),文件名为 `uos-guardian-体检报告-年月日-时分秒`。 - **提权也不落错地方**:以 `sudo` / `pkexec` 运行时自动回溯真实用户(`SUDO_USER` / `SUDO_UID` / `PKEXEC_UID`,其中 pkexec 给的是**数值 UID**,按 UID 查询),绝不会把报告写进 `/root`;root 写完后文件属主会自动**交还真实用户**,用户自己就能改、能删、能拖走。 - **导出界面全中文**:文件对话框使用 Qt 自带实现(不依赖 dbus/portal,提权会话中也能正常弹出),标题、`查找范围 / 文件名 / 文件类型` 等标签与 `保存 / 取消` 按钮均显式设为中文;程序启动时还会加载 Qt 自带的简体中文翻译(`qt_zh_CN` / `qtbase_zh_CN`),使全局标准对话框一并中文化。AppImage 打包会把这两个 `.qm` 一并打进包内,deb 通过 `Recommends: qt5-translations` 补齐。 - **取消也有兜底**:若对话框被取消或不可用,程序会询问是否**直接保存到主目录**,保证导出总能完成。 两种格式: - **TXT 文本报告**:含生成时间、主机名、系统版本、内核与程序版本,逐项列出「检查结果 / 是否需要修复 / 修复方式」,适合贴进工单或直接发给运维同事; - **JSON 数据**:在文本报告的基础上附带可机读的 `summary` 与 `results` 结构化字段(含每项 `id`、`status`、`auto_fixable`、`manual_hint`),便于脚本二次处理或做批量巡检归档。 导出为**纯本地写文件**,不联网、不上传、不修改系统;报告生成与落盘逻辑全部是可直接单测的纯函数。 ## 🆘 Live 救援体检使用说明 笔记本电脑系统损坏开不了机时的标准流程: 1. **制作 Live U 盘**(Deepin/UOS 官方镜像),从 U 盘启动电脑; 2. 在 Live 桌面打开本工具,进入「**Live 救援体检**」页(检测到 Live 环境时本程序会**默认停在这一页**); 3. **第 1 步:扫描硬盘上的系统** —— 程序用 `lsblk` 列出分区并逐个探测 `os-release`(分区已被桌面挂载就直接读,未挂载才只读挂载,全程不改硬盘数据);发现系统后填入下拉框,已被挂载的会标注「将直接复用」; 4. **第 2 步:挂载所选系统** —— 若该分区已被桌面自动挂载(通常在 `/media/` 下),**直接复用那个挂载点**而不是重复挂载;只有尚未挂载时才挂到 `/mnt/uos-guardian-target`。随后准备 chroot 环境,并按目标系统自身的 `/etc/fstab` **补挂独立的 `/boot`、`/boot/efi`**; 5. **第 3 步:对目标系统体检** —— 12 项体检与首页完全一致,但所有命令经 `chroot` 在目标系统内执行、文件检查指向目标系统路径(报告会标注「目标系统 · Live 救援模式」); 6. **一键修复目标系统** —— 低风险项(锁定核心包、重建 `/dev/null`、快捷方式权限、语言环境)直接修复在目标系统上,修复后自动复检;内核、dpkg、换源等高危项给出 chroot 内的手动命令; 7. **卸载目标分区** —— 先卸下我们补挂的子分区与 chroot bind;如果根分区是桌面自己挂的,则不替你卸载那处挂载(只提示你手动卸载或直接关机),然后拔掉 U 盘重启即可进入修复后的系统。 与「一键急救修复」页的分工:那一页执行**重量级 chroot 大修**(换清华源 + 重装核心桌面组件),本页执行**精准体检 + 低风险修复**,两者可配合使用(先体检定位问题,再决定是否需要大修)。 ### 在 Live 系统里启动本程序 Live 系统的根文件系统是只读镜像,**不能**像硬盘系统那样"缺依赖就 `apt install`",因此提供了专用启动脚本: ```bash bash run-live.sh # 推荐:自动挑运行载体 + 自动提权 + 环境自检 bash run-live.sh --check # 只做环境自检,不启动(排查"点了没反应"时先跑这个) bash run-live.sh --source # 忽略 AppImage,强制用源码运行 ``` 脚本会依次处理:Live 特征自检 → 必需命令(`lsblk`/`mount`/`umount`/`chroot`)检查 → **优先用 AppImage 运行**(免安装、自带 Python 依赖;无 FUSE 时自动解包运行)→ **确认 AppImage 真的执行得了**(U 盘常被挂成 `noexec`,或 FAT32 没有执行位,这时自动复制一份到本机可执行目录再跑)→ 转发图形会话环境变量(`DISPLAY` / `XAUTHORITY` / Wayland 相关)→ 依次尝试 `sudo -n`(Live 常免密)/ `sudo` / `pkexec` 提权。原有的 `start.sh` 在检测到 Live 环境时会自动转交给本脚本。 不启动界面也能排查环境(**这两个开关不需要 root,也不需要显示器**): ```bash # 打印 Live 判定原因、已发现的硬盘系统、报告落点——"为什么扫不到硬盘"先看这个 ubs@live:~$ ./UOSGuardian-x86_64.AppImage --live-check UOS 系统急救卫士 v1.1.0 · Live 环境自检 ---------------------------------------------------- [Live 判定] ✅ 是 Live 环境 —— 内核启动参数含 Live 标识(boot=live) [Live 介质] /run/live/medium [硬盘系统] 发现 1 个: - /dev/sda3 Deepin 25,已挂载于 /media/ubs/Roota [报告落点] /media/ubs/UDISK ./UOSGuardian-x86_64.AppImage --version # 确认手头这个包是哪个版本 ``` > **双击启动 / 从菜单启动也可以**:程序检测到非 root 时会弹出**图形授权框**(经 `pkexec`)自动重新以管理员身份启动,因此不再依赖 `lxterminal` 之类的具体终端。若系统没有 `pkexec`,会给出中文说明并在终端里可用 `sudo uos-guardian`(Live 下为 `bash run-live.sh`)。 > **⚠️ `/tmp` 太小时启动会失败**:AppImage / 单文件程序启动时要把内置的 Python + Qt 解包到临时目录(约需 300MB)。部分 Live 系统的 `/tmp` 是小容量 tmpfs,空间不足会直接报 > `Failed to extract ...: decompression resulted in return code -1!`——看起来就像"点了没反应"。`run-live.sh` 会自动检测并把 `TMPDIR` 切到 `/var/tmp` 或程序所在目录;手动运行 AppImage 时如遇此报错,请自行指定: > ```bash > TMPDIR=/var/tmp ./UOSGuardian-x86_64.AppImage > ``` > **⚠️ U 盘上的 AppImage"执行不了"**:U 盘常用 FAT32(没有 Unix 权限位),或为安全起见被挂载成 `noexec`。这种情况下文件明明在、权限看着也对(`-rwxr-xr-x`),**双击却毫无反应**——而 Live 的只读根又装不了 PyQt5、退不回源码运行。`run-live.sh` 会自动识别,并把这一个文件**复制到本机可执行目录**(优先 `~/.cache`、`/var/tmp`)再运行(AppImage 自包含,复制后照常工作)。 > 手动运行遇到 `Permission denied`、`无法执行二进制文件` 之类提示时,照同样思路处理即可: > ```bash > cp UOSGuardian-x86_64.AppImage ~/ && chmod +x ~/UOSGuardian-x86_64.AppImage > ~/UOSGuardian-x86_64.AppImage > ``` > **制作救援 U 盘只需两个文件**:把 `UOSGuardian-x86_64.AppImage` 与 `run-live.sh` 一起放到 U 盘(同一目录)即可,插入 Live 系统后在该目录执行 `bash run-live.sh`。若 AppImage 比 `src/` 里的源码旧,脚本会明确警告(避免拿旧包去修系统)。U 盘上"文件在但执行不了"属正常现象,脚本会自动转到本机可执行目录运行,无需你手动处理。 ### Live 环境下的行为差异 | 场景 | 本程序的处理 | |---|---| | 分区已被桌面自动挂载 | 复用现有挂载点,不重复挂载;卸载时不替你卸掉桌面那处挂载 | | 目标是独立 `/boot` 分区 | 读取目标系统 `/etc/fstab` 后自动补挂 `/boot`、`/boot/efi`,避免"缺内核"误报 | | 扫描时遇到 Live U 盘自身 / 正在运行的系统 | 直接从候选列表剔除,不提供挂载(绝不"把自己挂起来修") | | 「系统体检」页(本机体检) | 顶部显示 Live 警示条,**停用一键修复**(修的是内存系统,重启即还原),并引导至 Live 救援页 | | 导出体检报告 | 默认落在**可移动介质**(U 盘/移动硬盘,通过 `/sys/block/*/removable` 判定真外接盘);找不到则落到家目录并提示"报告在内存中,重启即失" | | 「系统设置」页的后台监控 | 在 Live 中停用并说明原因(systemd 服务活不过一次重启) | | 报告头部元信息 | 标注「Live 运行环境 · 本机为内存系统」,避免把 Live 的版本误认成硬盘系统的版本 | > ⚠️ **Wayland 会话**:提权运行图形程序可能被合成器拒绝。若窗口起不来,请在登录界面切换为 X11 会话,或改用 `pkexec` 启动。另:Live 的根文件系统通常不可写,若确实需要装依赖,请优先使用 AppImage。 > 说明:判定 Live 的依据是内核启动参数(`boot=live` / `casper` 等)、根文件系统类型(squashfs / erofs)、`/run/live` 是否存在、以及 **Live 介质是否真的挂载在 `/run/live`、`/lib/live/mount` 之下**。刻意**不**把 `/lib/live/mount` 目录本身当特征——已安装的 deepin 25(ostree 不可变层)也自带这个目录,用它判会把硬盘系统误判成 Live。 > 启动脚本(`start.sh` / `run-live.sh`)里另有一份**同口径的 shell 版**判据(启动参数 / 根文件系统类型 / 运行期目录)。它俩曾各自写了一份只查 `cmdline` 和 `/run/live` 的简版逻辑,漏掉了"根是 squashfs/erofs"这条硬特征——对 `run-live.sh` 只是少打一行提示,对 `start.sh` 却意味着判成非 Live 后去 `sudo apt install python3-pyqt5`,而 Live 的根是只读镜像,**必然失败**。现在两处共用同一份实现,并有单元测试锁死它们不许漂移。 > **chroot 的"进不去"和"进去以后报错"是两回事,判定只看退出码。** coreutils 的 `chroot` > 用退出码区分,本仓库用 coreutils 9.4 在真环境(userns + tmpfs 假根)逐项实测过: > > | 退出码 | 含义 | 实测的触发条件 | > |---|---|---| > | `125` | chroot 自身失败(**没进去**) | 目标根不存在 / 是普通文件 / 当前进程没有 root 权限 | > | `126` | 命令在,但执行不了 | 文件丢了执行位;**目标分区挂在 `noexec` 上**(此时里面所有命令都跑不了) | > | `127` | 目标系统里**没有**这个命令 | 例如未装 `locales` 包 → 没有 `locale-gen` | > > 不能靠报错文本判断:中文 UOS 上 `125` 打的是「无法将根目录更改为…」,而救援日志走的是 > 实时管道(未强制 C 语言)。更要紧的是 **`126` 与 `127` 的文本几乎一模一样,都含 > `failed to run command`**,只差结尾的 `Permission denied` / `No such file or directory` > ——若拿这个短语当兜底,就会把"noexec 导致执行不了"误报成"没装 `locales`",让用户去装一个 > 本来就存在的包,还把这次真失败记成已处理。 > > 另外,一键修复**无论成败都会解除**为 chroot 建立的 `/dev` `/proc` `/sys` `/run` bind 挂载 > 并 `sync` 落盘——失败路径(chroot 被拒、Live 没联网)恰恰是最常走的那条,不能只在成功时 > 收拾。清理本身是幂等的,重复执行不会误报"未能解除"。 ## 📸 界面概览 *(此处可插入演示截图或录屏 GIF)* ## 🚀 安装方式 ### 1. 通过 `.deb` 包安装(推荐) 如果你是普通 UOS 用户,请在[发行版页面]下载 `uos-guardian_1.1.0_all.deb` 安装包,双击即可通过 UOS 应用商店自动安装。 **安装时,脚本会自动执行以下操作:** - 将实际在位的核心桌面组件(deepin 25:`dde-shell`/`dde-launchpad` 等;旧架构:`dde-core`/`dde-dock` 等)**锁定**,防止被意外卸载。 - 注册并启动后台监控服务(`uos-guardian.service`)。 > **从启动器/桌面启动即可,不需要先开终端**:程序需要 root,检测到非 root 且处于图形会话时会弹出**图形授权框**(`pkexec`)自动重新以管理员身份启动。因此本包**不依赖** `lxterminal` 之类的具体终端;`policykit-1` 列为 `Recommends`(缺失时只能从终端 `sudo uos-guardian` 运行)。 ### 2. 从源码构建 `.deb` 安装包(开发者) 如果您想参与开发或自行打包,请确保您的系统安装了打包依赖: ```bash sudo apt install debhelper python3-pyqt5 python3-psutil -y ``` 克隆项目后,在项目根目录执行: ```bash # 生成 .deb 安装包(文件将生成在上一级目录) dpkg-buildpackage -us -uc -b ``` 打包完成后,deb 与 `.buildinfo` / `.changes` 都生成在**上一级目录**,双击 `.deb` 即可安装。本仓已实测跑通全流程,产出 `uos-guardian_1.1.0_all.deb`(约 89 KB,安装后约 300 KB,构建过程零警告,`lintian` **零错误零警告**)。 > ⚠️ **`/tmp` 只有 10MB**(tmpfs)时 deb 打包会中途失败。构建前先把临时目录指到大分区: > ```bash > TMPDIR="$PWD/.build-tmp" dpkg-buildpackage -us -uc -b > ``` > ⚠️ **别把 deb 放进 `dist/` 归档**:`dist/` 是 PyInstaller 的输出目录,`build_appimage.sh` 开头就执行 `rm -rf build/ dist/ AppDir/`,放进去的 deb 会在下次打 AppImage 时被**静默删掉**。要归档就放在上一级目录,或另建目录。 > 包内布局:程序主体安装到 `/usr/share/uos-guardian/`,入口脚本 `/usr/bin/uos-guardian`(GUI)与 `/usr/bin/uos-guardian-monitor`(后台监控)由 `bin/` 下的包装脚本提供,systemd 服务单元由 `debian/uos-guardian.service` 自动注册(注册/启动由 `dh_installsystemd` 注入,`debian/postinst` 里**不**手写 `systemctl`)。 > **打包约定(改 `debian/` 之前请先看)**:下面三条都曾让 deb 构建链彻底不可用,现已由 `tests/test_packaging_consistency.py` 看住,注入对应缺陷即会变红。 > > 1. **debhelper 兼容级别只能声明一次。** 本包在 `debian/control` 里用 `Build-Depends: debhelper-compat (= 12)`,就**不能**再留 `debian/compat` 文件——两者并存时 `dh` 直接报错(`debhelper compat level specified both in debian/compat and in debian/control`),连 `dh clean` 都跑不过,一个 deb 都产不出来。 > 2. **`debian/rules` 里的 `dh_auto_*` 必须保持为空规则。** 根目录存在 `setup.py`,debhelper 会据此自动判定构建系统为 `python-distutils`,而该系统在 compat 12 已被移除,否则报 `dh_auto_clean: error: This feature was removed in compat 12.`。本包是纯文件安装型,包内容全部由 `debian/install` 决定。 > 3. **图标源文件名必须等于 `.desktop` 的 `Icon=` 值,且 `debian/install` 的目的路径要以 `/` 结尾。** `dh_install` 不支持重命名:目的路径不以 `/` 结尾时它会把该串当作**目录**创建,最终得到 `.../apps/uos-guardian.png/app_icon.png`,而 freedesktop 规范要求 `uos-guardian.png` 是一个**文件**——装完 deb 启动器只能显示缺省图标。(玲珑包同理:图标按应用 ID 命名为 `uos.guardian.app.png`,所以 `linglong.yaml` 必须把 `Icon=` 一并改写。) > > **字节码不进包**:`debian/install` 是整目录复制 `src/uos_guardian/`,会把开发机上的 `__pycache__` 一起带走。debhelper 的 `dh_clean` 确实会清源码树里的 `__pycache__`,但 `dpkg-buildpackage -nc`(迭代时最常用的「不清理直接构建」)会跳过这一步,所以 `debian/rules` 在安装阶段又剪了一遍。实测对照:带剪枝 → 包内 0 个 `.pyc`;去掉剪枝 → 包内 28 个 `.pyc`。 #### 静态检查:lintian 本包的 deb 经 `lintian` 检查为**零错误零警告**。修掉的 6 项(2 E + 4 W)分别是: | 级别 | 标签 | 处理 | |---|---|---| | E | `bogus-mail-host` | `dev@uos-guardian.local` 的 `.local` 不是全球可路由域 → 改为 `dev@tuxnav.cn`(`debian/control` 与 `debian/changelog` 两处) | | E | `description-synopsis-is-duplicated` | **中文误报**(见下) → 改写首行避开 | | W | `possible-unindented-list-in-extended-description` | 描述里 `^ [\-\*]` 视为未缩进列表 → 列表项改为两个空格缩进 | | W | `depends-on-obsolete-package policykit-1` | 改为 `pkexec \| policykit-1`(`pkexec` 在前;老系统由 `policykit-1` 兜底。它是 `Recommends`,不会阻断安装) | | W | `no-manual-page` ×2 | 新增 `resources/man/uos-guardian.1` 与 `uos-guardian-monitor.1`,由 `debian/uos-guardian.manpages` 安装 | > **`description-synopsis-is-duplicated` 是 lintian 对中文的误报**,不是本包的缺陷。它的"模糊匹配"会把 `[^a-zA-Z0-9]+` **全部删掉**再比对(见 `Lintian/Check/Fields/Description.pm`):中文全被删光,简介和描述首行都只剩 `UOS`,于是判定重复。把描述首行的 `UOS` 去掉即可避开。 > **本机(Deepin/UOS)装不上 `lintian`**:源的 `libhtml-form-perl` 停留在 `6.07-1`,而系统已装的 `libhttp-message-perl` 声明 `Breaks: libhtml-form-perl (< 6.11-2~)`,`libwww-mechanize-perl`(lintian 硬依赖)因此无法安装。这是**源版本冲突**,装更多包解决不了。**不必动系统包**——把 lintian 与其缺失依赖下载到私有前缀里跑即可: > ```bash > mkdir -p /tmp/lintian-dl /tmp/lintian-root && cd /tmp/lintian-dl > apt-get download lintian libdigest-sha-perl libencode-perl libversion-perl \ > libwww-mechanize-perl libhtml-form-perl > for f in *.deb; do dpkg -x "$f" /tmp/lintian-root; done > PERL5LIB=/tmp/lintian-root/usr/share/perl5 \ > perl /tmp/lintian-root/usr/bin/lintian ../uos-guardian_1.1.0_all.deb > ``` > dpkg 的 `Breaks` 只在**安装**时检查,解包到私有目录用 `PERL5LIB` 加载完全绕开它,系统包状态零改动(`lintian-explain-tags <标签>` 可查任一标签的确切含义)。 #### 手册页 ```bash man uos-guardian # 装包后 groff -Kutf8 -man -Tutf8 -ww resources/man/uos-guardian.1 # 改动后自查 ``` 中文手册页有个容易漏的坑:roff 默认**两端对齐**、按**空格**断行,而中文没有词间空格,整段会被当成一个不可断的长"词",于是 groff 报 `cannot adjust line` / `cannot break line`,排版还被拉出难看的间隙。两份手册页都在 `.TH` 之后加了 `.ad l` 关掉两端对齐,并把超长的 CJK 串在标点处断成两行——`tests/test_packaging_consistency.py::test_man_pages_render_without_warnings` 会用 `-ww`(打开全部告警)实跑一遍,**任何告警都算失败**。 > 本包有**两个**可执行文件,所以手册页用显式的 `debian/uos-guardian.manpages` 指定,而不是靠 `debian/*.1` 自动发现——后者只会把 `debian/<包名>.<章节>` 归给主包,第二个文件会被误判成另一个包的。 ### 3. 构建 AppImage 便携版(可选) ```bash ./build_appimage.sh ``` 产物为 `UOSGuardian-x86_64.AppImage`。注意:AppImage 无法注册 systemd 服务,后台监控需按下文说明手动启用。 > ⚠️ **关键:必须用"装了本项目依赖的那个 Python"来构建。** `python3` 未必是它——PyQt5 装在 `/usr/lib/python3/dist-packages`,只有系统 Python 或用 `--system-site-packages` 建的虚拟环境读得到。用错了解释器的后果非常隐蔽:**PyInstaller 照样成功、产物也照样生成,但那个包双击起不来**(`ModuleNotFoundError: No module named 'PyQt5'`)。本项目已实测踩过一次,因此脚本现在会在打包**之前**就检查这一点并明确报错。若你的 `python3` 不满足条件,用项目虚拟环境构建: > ```bash > PATH="$PWD/.build-venv/bin:$PATH" ./build_appimage.sh > ``` > **改完代码务必重新打包**:AppImage 是**打包那一刻的快照**,Live 系统上它又是主要运行载体,拿旧包去修系统是最坏的情况。`run-live.sh` 会比对 AppImage 与 `src/` 的时间戳,发现包比源码旧会明确警告并建议 `--source` 运行。构建脚本还会:自动把 Qt 中文翻译(`qt_zh_CN.qm` / `qtbase_zh_CN.qm`)打进 `translations/`;复用已下载的 `appimagetool`;`/tmp` 空间不足时自动改用项目内临时目录(`TMPDIR`)。 **构建脚本的四道关**(任何一道不过都会把产物改名隔离成 `*.STALE-*` / `*.BROKEN-*` 并让构建失败——坏的包绝不能留在原地,`run-live.sh` 会优先挑它、用户也可能直接双击): | 顺序 | 检查 | 拦住什么 | |---|---|---| | 打包前 | 构建解释器能 `import PyQt5` | 缺依赖的解释器 → 包起不来(**打包本身不会失败**,所以必须前置拦) | | 打包后 | `scripts/verify_appimage.py` 比对产物字节码与当前源码 | 产物里的代码是旧的("重建过了"不等于"包里是新代码") | | 打包后 | **实跑**产物 `--version` | 前一道关证明不了"这个包能起来":它用的是**同一个解释器**,缺依赖时自己跟自己比、天然报"一致" | | 打包后 | **实跑界面**(offscreen,判据为"退出码 = 124") | Qt 平台插件 / 翻译 / 图标资源缺失,或窗口构造期就抛异常 —— `--version` 全拦不住,但用户看到的就是"双击没反应" | **第四道关的判据是反的,这点很容易写错。** GUI 正常运行时**不会自己退出**,它会一直待在事件循环里;所以脚本用 `timeout 25` 给它设上限,**被超时杀掉反而代表成功**(退出码 124),"提前退出"才是失败。两个配套细节: - 必须套 `unshare -Ur`。非 root 运行时程序会走 pkexec 提权分支弹授权框,而构建环境没有显示器,弹框会**一直挂住**。`unshare -Ur` 让它以为自己是 root,直接走正常启动路径。脚本会先探测 `unshare -Ur true` 是否可用,探测失败才跳过(不靠猜 stderr 关键字)。 - `--appimage-extract-and-run` 每次都会往 `/tmp` 解包,**不能并发运行**:两个实例同时跑会互踩归档,报出 `Failed to extract pyi_rth_pkgutil: failed to open archive file!`(退出码 255)——看着像"产物坏了",其实是测试方法错了。本项目实测踩过。 **产物一致性自检**:构建脚本在打包完成后会调用 `scripts/verify_appimage.py`,把产物里的**字节码**与当前源码逐个模块比对。这一步不是多余的——"源码比包新"只能说明该重建,"重建过了"也说明不了包里就是新代码。本项目曾出现 AppImage 停在两个月前、而 Live 上优先跑 AppImage 的情况(等于新代码从未在 Live 上跑过)。校验器能精确点名哪个模块不一致: ```bash # 由 build_appimage.sh 自动调用;也可手动查 python3 scripts/verify_appimage.py # 自动找项目内的 AppImage python3 scripts/verify_appimage.py --inner dist/UOSGuardian # 直接查单文件产物 ``` 它比对的是 `co_names/co_varnames/co_consts`(含被折叠进元组常量的长文案),所以任何逻辑改动都逃不掉;纯注释或空白改动不会误报。 覆盖范围包括**入口模块 `main.py`**(`--live-check` 等启动开关与提权逻辑都在里面)。入口脚本在归档里的位置和名字与普通模块不同(位于归档顶层、名为 `main`),校验器已按此对应——`uos_guardian.main` 会被**真正比对**,而不是简单豁免。正常情况下自检会报告"与源码不符 0、产物中缺失 1",那唯一缺失的是 `services/monitor.py`:它是供 systemd 拉起的独立后台服务,GUI 只通过 `systemctl` 查它的状态,本就不随 AppImage 打包。 **自检不通过时脚本会 `exit 1`,并把那份产物改名隔离**为 `UOSGuardian-x86_64.AppImage.STALE-<时间戳>`,不让它被 `run-live.sh` 或双击误用(改名后不再匹配 `UOSGuardian-*.AppImage`)。最常见的触发原因是"边改代码边打包"——打包器读源码与自检读源码之间源码变了。见到这个提示,确认代码不再变动后重新执行本脚本即可,隔离件可删。 ## 💻 开发与本地测试 如果您希望在不打包的前提下快速运行本项目,请在项目根目录使用启动脚本: ```bash ./start.sh ``` 该脚本会自动检测并安装 `python3-pyqt5` 依赖,并通过环境变量(`PYTHONPATH`)无缝兼容 UOS 内置的 Python 3.7 环境,无需复杂的配置。 **注意:** 由于 UOS 系统存在 Python 3.7 版本,本工具未使用 `importlib.metadata` 这种只在 Python 3.8+ 有效的特性,从而保证了极致兼容性。 ### 运行测试 三套测试互不干扰。`tests/conftest.py` 已把 `src/` 加入 `sys.path`,因此不必再手写 `PYTHONPATH=src`(漏写会让 pytest 在**收集阶段**就报 `No module named 'uos_guardian'`,看着像"测试坏了")。 > ⚠️ **跑测试同样要用"装了 PyQt5 的那个 Python"。** 本项目已踩过两次同一个坑:一次是打包时选错解释器,一次就是跑测试时选错——后者表现为**9 个 GUI 测试一起报 `ModuleNotFoundError: No module named 'PyQt5'`**,看着像界面测试坏了,其实只是解释器没依赖。现在 `tests/conftest.py` 会提前拦住这种情况并直接告诉你该用哪个解释器(**不会静默跳过**:静默跳过会让"依赖缺失"伪装成"全部通过",比报错更危险)。确实只想跑不依赖 PyQt5 的用例时: > ```bash > UOS_GUARDIAN_SKIP_PYQT5_CHECK=1 python3 -m pytest tests/test_packaging_consistency.py -q > ``` ```bash python3 -m pytest tests/ -q # 全部 # 1) 核心逻辑单测(纯函数,不碰 Qt,也不读写真实系统) python3 -m pytest tests/test_core.py -q # 2) 界面级回归(离屏运行,无需真实显示器) QT_QPA_PLATFORM=offscreen python3 -m pytest tests/test_gui_offscreen.py -q # 3) Live 启动脚本的分支回归(run-live.sh) python3 -m pytest tests/test_run_live_sh.py -q ``` 第 2 套主要锁死「导出体检报告」的界面契约:对话框文案必须是中文、默认落盘位置必须是**当前登录用户的用户级主目录**(提权时也不落 `/root`)。本机未安装 Qt 中文翻译(`qt5-translations`)时,相关用例自动跳过——界面本身不依赖该包,标签是显式写死中文的。 第 3 套把 `run-live.sh` 的**函数定义区**截出来单独 `source`,用打桩覆盖"挂载选项 / 剩余空间"这类沙箱里造不出的环境,逐条验证"AppImage 能不能跑、要不要复制到别处"。其中一条会用 `unshare -Urm` + `mount -t tmpfs -o noexec` **真挂一个 noexec 分区**做端到端验证(内核不允许时自动跳过);另有一条**防复发断言**,确保主流程不回到 `[ -x "$APPIMAGE" ]` 那种会把 noexec 的 U 盘推进死路的写法。 ## 🔧 后台监控服务说明 本工具包含一套**实时进程拦截系统**,用于在终端执行高危 `apt remove` 操作时立即阻断并弹窗警告。其运行方式根据安装包格式有所不同,请您务必留意: ### 📦 1. 如果你安装了 `.deb` 包(通过应用商店或双击安装) - **自动运行**:安装脚本会自动注册 `systemd` 服务 `uos-guardian.service`,并设置为**开机自启**。系统启动后,防御机制会默默在后台运行,无需您任何额外操作。 - **手动控制**:您可以通过工具界面中的 **“系统设置” -> “启用/停用后台监控”** 按钮,随时开启或关闭防御。 ### 🟢 2. 如果你使用的是 `AppImage` 单文件便携版(即 `UOSGuardian-x86_64.AppImage`) - **AppImage 的特性**:因为 AppImage 是独立的单文件运行包,**无法**像 `.deb` 包那样向系统注册常驻服务(没有权限写入 `/etc/systemd/system/`)。因此,**AppImage 版本默认不会随系统开机自动启动防御功能**。 - **如何启用防御**:每次您启动软件后,只需打开软件界面,点击左侧的 **“系统设置”**,然后点击 **“启用后台监控”** 按钮即可。只要软件窗口保持打开(或最小化到托盘),防御机制就会生效。 - **💡 进阶方案:设置开机手动触发自启动** 如果您希望每次开机时自动启用防御,可以通过以下方式创建一个“自启动脚本”来帮您自动勾选(这需要您具备一定的 Linux 基础): 1. 将您的 `UOSGuardian-x86_64.AppImage` 文件放置到固定路径,例如 `/opt/UOSGuardian.AppImage`,并赋予可执行权限。 2. 在 `~/.config/autostart/` 目录下创建桌面自启动文件:`nano ~/.config/autostart/uos-guardian-autostart.desktop`。 3. 填入以下内容并保存: ```ini [Desktop Entry] Type=Application Name=UOS 急救卫士 (防御后台) Exec=/opt/UOSGuardian.AppImage X-GNOME-Autostart-enabled=true ``` 4. 下次您登录桌面时,软件将会自动启动,您只需在界面里点击一次“启用后台监控”,即可实现“登录即防御”。 > **⚠️ 重要提示**:无论您使用哪种版本,如果在“系统设置”中看到状态为“监控服务已停止”,均代表防御功能未生效,请务必按照上述指引手动启用。 ## ❓ 常见问题排查 (Q&A) **Q:运行 `bash start.sh` 后提示 `SyntaxError: (unicode error) 'utf-8' codec can't decode...`** **A**:您使用的编辑器在保存 `pages.py` 等文件时保存为了 GBK 编码。请使用 `cat > 文件名 << 'EOF' ... EOF` 的纯终端方式重新写入,确保文件为纯正的 UTF-8 格式。 **Q:双击 `.deb` 安装后,启动器找不到这个软件,或者图标点了没反应。** **A**:请确认 `setup.py` 里正确配置了 `data_files`,并成功把 `.desktop` 安装到了 `/usr/share/applications`。桌面项里的 `Exec=uos-guardian` 是**不带终端**的:程序启动后发现不是 root 会自己弹一个图形确认框、用 `pkexec` 一键提权重启(旧版本用 `lxterminal -e sudo …`,依赖终端程序存在,已废弃)。如果您用的是 AppImage 版,请通过右键文件 →“添加到启动器”手动固定到菜单。 **Q:弹了「需要管理员权限」框、点了「以管理员身份重新启动」,之后就没动静了。** **A**:v1.1.0 已修复旧版“提权结果静默”的缺陷。现在授权被取消、密码不对、polkit 代理不在、重启后崩溃,都会**弹窗说明原因**;pkexec 与重启后程序的完整输出写在 `~/.cache/uos-guardian/elevation.log`(每次提权轮换保存为 `.old`),弹窗里会带日志摘录与路径。若仍异常,优先看这个日志。授权框反复不出现时,检查 polkit 代理是否在运行(`pgrep -af polkit`,UOS/DDE 下应为 `dde-polkit-agent`)。 **Q:一键修复时报「无法进入目标系统(chroot 被拒绝)」。** **A**:这说明 `chroot` 没能进入目标系统——通常是**当前程序没有 root 权限**,或系统启用了限制 chroot 的安全策略。这与"目标系统里的命令跑失败了"是两回事:后者才是目标系统自身的问题。请关闭程序,用「右键 → 以管理员身份运行」重新打开后重试。注意体检报告里**语言项**在遇到这种情况时会显示为“配置已写入、待进入系统后执行 `sudo locale-gen`”,因为配置文件确实已经写进目标系统了,只是没能在里面预生成 locale——这不是修复失败。 **Q:Live 救援页点「体检」时提示无法进入目标系统,没有出报告。** **A**:这是刻意的。进不去目标系统时,12 项检查的命令会**全部**失败,结果退化成一片"无法评估"——这种报告比报错更危险,容易被误读成"系统没什么问题"。所以程序会先探测一次能否真正进入目标系统,进不去就直接说明根因(权限)并停下来,请按上一条以管理员身份重开后重试。 **Q:关闭程序时提示「目标系统仍处于挂载状态」。** **A**:这时目标分区还挂着、为 chroot 建立的虚拟目录挂载也尚未解除,而**改动还没同步落盘**。选"是"程序会先解除本工具建立的挂载并 `sync` 再退出;选"否"则保持程序打开,请回到「Live 救援体检」页点「卸载目标分区」并等它完成。若清理过程中出错,程序会取消退出并提示,以免留下可能丢失的改动。注意:只清理本工具建立的东西——若那处挂载是桌面自动挂载的,不会替你卸载。 **Q:后台监控服务提示“启动失败/状态为 exited”。** **A**:如果提示 `bad unit file setting` 或 `status=200/CHDIR`,请检查 `/etc/systemd/system/uos-guardian.service` 文件中的 `ExecStart` 路径是否带有中文。我们推荐使用 `/bin/bash -c "你的绝对路径.sh"` 代替直接调用 Python。 **Q:在 Live 系统里双击/执行后“没反应”,或者提示缺 `python3-pyqt5`。** **A**:Live 的根文件系统通常是只读镜像,装不了依赖。请改用打包好的 `UOSGuardian-x86_64.AppImage`,并通过 `bash run-live.sh` 启动(它会自动选载体、自动提权、无 FUSE 时自动解包运行)。先跑 `bash run-live.sh --check` 可以看到具体卡在哪一步。 > **AppImage 有「挂载运行」与「解包运行」两种方式**,脚本会自动选:有 FUSE 时挂载(启动最快、几乎不占额外空间),没有时解包(稍慢,需要约 300MB 临时空间)。 > 但 `/dev/fuse` 存在**不等于**真挂得上——容器或受限环境里挂载可能被权限/安全策略拒绝,表现就是窗口始终不出现。这种情况下可以**强制解包**: > ```bash > APPIMAGE_EXTRACT_AND_RUN=1 bash run-live.sh # 强制解包方式运行 > TMPDIR=/你的大分区目录 bash run-live.sh # 指定解包落点(/tmp 太小时用) > ``` > 这两个变量都会被脚本尊重,并**显式转发给提权后的进程**——重启进程用的是 `pkexec`,它会清空环境变量,不转发就等于没设。手动运行 AppImage(不经脚本)时也可自行指定,例如 `TMPDIR=/var/tmp ./UOSGuardian-x86_64.AppImage`。 **Q:U 盘里的 AppImage 双击没反应,终端里说 `Permission denied` 或“无法执行二进制文件”。** **A**:这不是文件损坏,而是 U 盘的挂载方式不允许执行——FAT32 没有 Unix 权限位,很多发行版还会把外接盘挂成 `noexec`;这种情况下权限显示为 `-rwxr-xr-x` 也照样跑不起来。`bash run-live.sh` 会自动把 AppImage 复制到本机可执行目录(优先 `~/.cache`、`/var/tmp`)再运行,不用你手动处理(终端会提示「正在复制一份到 …」);手动运行的话,`cp` 到家目录并 `chmod +x` 即可。 **Q:扫描不到硬盘上的系统,或者提示“已挂载”不让挂。** **A**:Live 桌面(udisks)通常已经把硬盘分区自动挂到 `/media/<用户>/<卷标>` 了。本程序识别到这种情况会**直接复用**那处挂载点(下拉框会标注「已挂载在 …,将直接复用」),不会再挂一次——重复挂载同一设备必然失败。若确实需要重新挂载,请在文件管理器里先卸载该分区再扫描。 **Q:目标系统的内核检查老是说“缺内核”,但我明明装过。** **A**:多半是 `/boot` 是独立分区。挂载目标根分区后,`/boot` 只是一个空目录。本程序会读目标系统的 `/etc/fstab` 自动补挂 `/boot` 与 `/boot/efi`;若 fstab 里用的是 `UUID=`/`LABEL=` 且解析不到设备,日志里会明确写出来。 ## 📜 开源许可 本项目基于 [MIT License](LICENSE) 协议开源。免费供个人或商业使用,但请在分发时保留版权声明。 --- > 如果在使用中遇到任何问题,欢迎提供详细反馈!你的 UOS 系统,由我们来守卫。