From 5f7e79b089d358c32c1dc04ecc2740f134f90940 Mon Sep 17 00:00:00 2001 From: Codex Date: Fri, 11 Sep 2026 14:04:12 +0800 Subject: [PATCH] feat: add configuration-preserving CUPS project updater --- README.md | 38 +++- setup_cups.sh | 31 ++- tests/test_update.py | 381 ++++++++++++++++++++++++++++++++++++ update_cups.py | 455 +++++++++++++++++++++++++++++++++++++++++++ 4 files changed, 895 insertions(+), 10 deletions(-) create mode 100644 tests/test_update.py create mode 100644 update_cups.py diff --git a/README.md b/README.md index be8be1f..378b153 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,7 @@ - 驱动管理器账号密码可通过脚本菜单修改 - 网络和打印服务守护(断网自动切 DHCP,CUPS 卡死自动恢复) - 静态 IP 支持网关变更自愈:先切 DHCP,再按原静态 IP 尾号迁移到新网段 +- 独立程序更新(保留配置和密码,更新前备份,失败自动回滚) - 支持一键卸载 ## 快速安装 @@ -149,7 +150,8 @@ lpinfo -v | grep usb ## 文件说明 ``` -├── setup_cups.sh # 一键安装/卸载脚本 +├── setup_cups.sh # 安装/卸载/更新菜单 +├── update_cups.py # 独立更新器(Python 3.9+) ├── watchdog/ # 网络和打印服务守护脚本 ├── cups-templates-zh_CN/ # 中文界面模板(65个文件) ├── cups-docroot-zh_CN/ # CUPS 中文首页和前端汉化脚本 @@ -191,20 +193,38 @@ python3 -B -m unittest discover -s tests -v 测试会模拟系统命令和驱动安装,不会修改真实网络、安装驱动或重启服务。 覆盖守护锁释放与连续失败恢复、多网卡地址选择、APT 等待/超时、PDF 备用驱动、DEB 架构筛选和基础访问控制。 -通过这些测试不等于通过实机验证;USB 打印、AirPrint、NetworkManager/netplan 切换仍需在目标设备验证。 +更新器测试使用临时安装目录和模拟的 systemctl,覆盖预览、取消、配置保留、重复更新、服务状态恢复、写入失败及中断回滚。 +通过这些测试不等于通过实机验证;systemd 更新流程、USB 打印、AirPrint、NetworkManager/netplan 切换仍需在目标设备验证。 -## 已部署设备升级提示 +## 更新已部署设备 -更新 Git 仓库不会自动更新 `/opt` 下正在运行的文件。已安装设备需要部署更新后的 -`watchdog/network-watchdog.sh`、`watchdog/print-watchdog.sh` 到 `/opt/cups-watchdog/`, -以及 `cups-driver-manager/driver_manager.py` 到 `/opt/cups-driver-manager/`,保留原有配置和账号密码。 +更新 Git 仓库不会自动更新设备上的程序。请在打印服务器的 **root 终端** 执行以下命令, +或下载最新版 `setup_cups.sh` 后选择菜单 **8:更新已安装程序**。旧版安装脚本没有更新入口,需要先获取新版。 -旧版守护脚本可能在 `/run/cups-watchdog/` 留下 `network.lock`、`print.lock` 目录。 -部署新文件后重启设备一次,可清除 `/run` 中的旧锁并加载新代码。 -不要在守护脚本仍运行时直接删除锁目录,以免破坏并发互斥。 +```bash +# 下载最新版脚本并进入更新流程(显示文件清单后需要输入 y 确认) +curl -fsSL https://git.workyai.cn/237899745/S905L3A/raw/branch/master/setup_cups.sh | bash -s -- --update + +# 仅检查和预览,不修改已安装文件、不停止服务 +curl -fsSL https://git.workyai.cn/237899745/S905L3A/raw/branch/master/setup_cups.sh | bash -s -- --update --dry-run + +# 已有最新版脚本时;非交互环境可追加 --yes,明确确认更新 +bash setup_cups.sh --update --yes +``` + +需要 Linux、systemd、Bash 和 Python 3.9+;缺少依赖时会退出,不会自动安装软件包。 +默认从 Gitea 的 `master` 下载,并检查压缩包路径和程序语法。下载失败会退出,不自动切到可能尚未同步的镜像。 +可信的离线仓库副本也可使用 `python3 update_cups.py --source-dir . --dry-run` 检查,去掉 `--dry-run` 后确认更新。 + +- 只替换已经安装且内容有变化的驱动管理器、两个守护脚本、中文模板和首页脚本;没有变化时不会重启服务。不会补装缺失的可选组件或新增文件,也不会执行安装脚本中的配置迁移。 +- 不改打印机队列、CUPS/网络/守护配置、账号密码、systemd 服务文件及开机启用状态;不升级系统软件包、不重启 CUPS。手工修改过的程序文件或中文页面会被仓库版本替换,请先看预览清单。 +- 更新前在 `/var/backups/cups-project/` 保存旧程序文件、校验值及原服务状态。先暂停原本运行的守护定时器,等待正在运行的守护检查结束,再替换文件。驱动管理器代码有变化时,其 Web 页面会短暂不可用。 +- 请在没有驱动上传/安装或手动守护操作时更新。检测到仍在运行的守护或驱动安装子进程会取消;不会强杀它们。确认守护空闲后,更新器会清理默认 `/run/cups-watchdog/` 中旧版遗留的空锁目录,无需为此重启设备。 +- 替换或服务启动检查失败时,会尝试自动恢复旧文件和原服务运行状态;备份始终保留。断电、磁盘损坏或强制杀进程不能保证自动回滚,应根据报错及备份内的 `manifest.json` 人工恢复。 ## 更新日志 +- 2026-09-11: 新增菜单第 8 项及 `--update`,支持更新预览、配置保留、备份校验、失败回滚和隔离回归测试。 - 2026-09-11: 修复守护锁、多网卡静态 IP、APT 等待、PDF 备用驱动和多架构 DEB 安装问题,添加隔离回归测试。 - 2024-12-01: 添加卸载功能、优化打印速度 - 2024-11-30: 修复 CSS 兼容 CUPS 2.4.7、添加 PDF 打印机选项 diff --git a/setup_cups.sh b/setup_cups.sh index 31184ee..60b98fb 100755 --- a/setup_cups.sh +++ b/setup_cups.sh @@ -6,6 +6,7 @@ # 用法: # 安装: chmod +x setup_cups.sh && ./setup_cups.sh # 卸载: ./setup_cups.sh --uninstall +# 更新: ./setup_cups.sh --update # set -e @@ -55,6 +56,24 @@ download_repo_archive() { curl -fsSL -o "$output_file" "$REPO_ARCHIVE_FALLBACK_URL" 2>/dev/null } +# 独立更新入口,不调用安装或配置函数。 +update_cups() { + check_root + command -v python3 >/dev/null 2>&1 || error "更新功能需要 Python 3.9+,请先安装 Python 3" + # 在子 shell 中下载独立更新器,不调用安装流程或覆盖当前安装脚本。 + ( + update_dir=$(mktemp -d) + trap 'rm -f "$update_dir/update_cups.py"; rmdir "$update_dir" 2>/dev/null || true' EXIT + update_url="https://git.workyai.cn/237899745/S905L3A/raw/branch/master/update_cups.py" + info "正在下载更新器..." + if ! curl -fsSL --connect-timeout 15 --max-time 120 -o "$update_dir/update_cups.py" "$update_url" && \ + ! wget -q --timeout=30 --tries=2 -O "$update_dir/update_cups.py" "$update_url"; then + error "更新器下载失败,未修改已安装文件" + fi + python3 -B "$update_dir/update_cups.py" --archive-url "$REPO_ARCHIVE_URL" "$@" + ) +} + # 检查是否为root用户 check_root() { if [ "$(id -u)" != "0" ]; then @@ -2034,6 +2053,8 @@ show_help() { echo " $0 显示主菜单" echo " $0 --install 直接安装 CUPS 打印服务" echo " $0 --uninstall 直接卸载 CUPS 打印服务" + echo " $0 --update 更新已安装的程序,保留配置和密码" + echo " 可追加 --dry-run(仅检查)或 --yes(确认更新)" echo " $0 --driver-manager-auth" echo " 修改驱动管理器用户名和密码" echo " $0 --help 显示此帮助信息" @@ -2147,9 +2168,10 @@ show_menu() { echo " 5) 查看当前网络状态(静态/DHCP)" echo " 6) 一键共享所有打印机" echo " 7) 修改驱动管理器用户名和密码" + echo " 8) 更新已安装程序(保留配置和密码)" echo " 0) 退出" echo "" - read -p " 请输入选项 [0-7]: " choice < /dev/tty + read -p " 请输入选项 [0-8]: " choice < /dev/tty case "$choice" in 1) @@ -2192,6 +2214,9 @@ show_menu() { read -p "按 Enter 返回主菜单..." < /dev/tty show_menu ;; + 8) + update_cups + ;; 0) echo "已退出" exit 0 @@ -2212,6 +2237,10 @@ case "${1:-}" in --install|-i) main ;; + --update) + shift + update_cups "$@" + ;; --driver-manager-auth|--dm-auth) change_driver_manager_auth ;; diff --git a/tests/test_update.py b/tests/test_update.py new file mode 100644 index 0000000..8bfdfc9 --- /dev/null +++ b/tests/test_update.py @@ -0,0 +1,381 @@ +"""Updater transaction and service tests; all targets live in temporary roots.""" + +import importlib.util +from contextlib import ExitStack, nullcontext +import io +import json +import os +from pathlib import Path +import stat +import subprocess +import sys +import unittest +from unittest.mock import patch +import zipfile + +from test_regressions import IsolatedTest, ROOT, bash_path, function + + +spec = importlib.util.spec_from_file_location('cups_project_updater', ROOT / 'update_cups.py') +updater = importlib.util.module_from_spec(spec) +sys.modules[spec.name] = updater +spec.loader.exec_module(updater) + + +class FakeServices: + def __init__(self, fail_restore=False, fail_pause=False): + self.calls = [] + self.fail_restore = fail_restore + self.fail_pause = fail_pause + + def snapshot(self, manager_changed): + self.calls.append(('snapshot', manager_changed)) + + def pause(self): + self.calls.append('pause') + if self.fail_pause: + raise updater.UpdateError('simulated busy service') + + def restore(self): + self.calls.append('restore') + if self.fail_restore: + self.fail_restore = False + raise updater.UpdateError('simulated startup failure') + + def stop_for_rollback(self): + self.calls.append('stop_for_rollback') + + +class UpdateTests(IsolatedTest): + def setUp(self): + super().setUp() + self.source = self.directory / 'release' + self.system = self.directory / 'system' + self.source.mkdir() + self.system.mkdir() + self.write(self.source, 'setup_cups.sh', b'#!/bin/bash\necho installer\n') + for source, target in updater.PROGRAMS.items(): + new = b'version = 2\n' if source.endswith('.py') else b'#!/bin/bash\necho new\n' + old = b'version = 1\n' if source.endswith('.py') else b'#!/bin/bash\necho old\n' + self.write(self.source, source, new) + self.write(self.system, target, old) + self.write(self.source, 'cups-templates-zh_CN/header.tmpl', b'
new') + self.write(self.source, 'cups-templates-zh_CN/trailer.tmpl', b'