mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
修四个安装/卸载卡死的 bug(都实测复现过)
1. 卸载里 input() 在无 stdin 时永久阻塞
从控制面板卸载**永远卡住不结束**。安装包的 [UninstallRun]
用 runhidden 调用,没有 stdin,input() 就一直等。
改成只在 sys.stdin.isatty() 时才等回车。
2. Popen 没重定向子进程 stdio,导致 capture_output 等管道 EOF
_schedule_self_delete 起的 cmd 继承了父进程的 stdout/stderr 句柄,
任何用 capture_output 调 --uninstall 的地方都会一直等,
表现为"程序卡死"(实测 90 秒不返回,改后 0.2 秒)。
Popen 加 stdin/stdout/stderr=DEVNULL。
顺带把延迟从 timeout 换成 ping —— timeout 在 DETACHED_PROCESS
下没有控制台会立刻失败,等于没延迟直接删。
3. _kill_running 用 taskkill /IM ykxmz.exe 把**自己**杀了
--uninstall 本身就跑在 ykxmz.exe 里,这条命令一发,
后面的"删开机自启""删数据目录"全都没执行。
现象:卸载看着完成了,HKCU 的自启注册表项还留着。
改成 /FI "PID ne <self>" 排除自己。
4. 安装器关不掉以管理员身份运行的旧实例 -> 整个安装回滚
Inno 自带机制只会"请求"对方退出,遇上常驻的服务进程关不掉,
弹"无法自动关闭所有应用程序",静默模式下被压成"终止",
安装直接回滚 —— 用户看到的是"装了没反应/还是旧版本"。
改成 PrepareToInstall 里自己 taskkill /F,关掉 Inno 那套。
另外修了两个卸载残留:
* 安装目录留个空壳 -> [UninstallDelete] 加 dirifempty
* ProgramData\ykxmz 没删 -> 加进 [UninstallDelete]
还有一处 Inno 写法陷阱(踩了三次):
注释里写 [区段名] 或 {常量} 会让编译直接失败,
因为 [Code] 解析器连注释内容也处理。
加了 _build/check_iss.py 专门扫这个,并进全量套件。
实测结果:
静默安装 5 秒内退出,退出码 0,配对码正确写入,程序自动跑起来
静默卸载 0.7 秒退出(原来无限卡住)
全量回归 29 项全通过
卸载逻辑 11 项全通过
This commit is contained in:
@@ -1,16 +1,38 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
"""卸载:一条命令把程序从这台电脑上清干净。
|
||||
|
||||
ykxmz.exe --uninstall
|
||||
ykxmz.exe --uninstall 自删(用户手动跑)
|
||||
ykxmz.exe --uninstall --keep-dir 不自删(安装包卸载器调用)
|
||||
|
||||
清这些:
|
||||
1. 停掉正在跑的进程
|
||||
1. 停掉正在跑的其它实例
|
||||
2. 删掉开机自启项
|
||||
3. 删掉数据目录(配对码、已配对手表……)
|
||||
|
||||
**故意做这个**:一个能开机自启、又能远程控制的软件,
|
||||
必须能干净地卸载掉 —— 否则它就成了"赖着不走"的东西。
|
||||
电脑的主人应该随时能把它彻底清掉。
|
||||
|
||||
---- 2026-10 修的一个卡死 bug ----
|
||||
|
||||
现象:
|
||||
从控制面板卸载时,卸载器**永远卡住不结束**。
|
||||
|
||||
排查:
|
||||
1. Inno 的 [UninstallRun] 用 waituntilterminated 等 ykxmz.exe --uninstall 退出
|
||||
2. 单独跑 ykxmz.exe --uninstall 也是 90 秒不返回 —— 问题在它自己
|
||||
3. 根因在 _schedule_self_delete():
|
||||
Popen(["cmd", "/c", cmd], creationflags=...)
|
||||
**没有重定向子进程的 stdio**。子进程继承了父进程的 stdout/stderr 句柄,
|
||||
而那个 cmd 要跑 `timeout /t 2` 再 `rmdir /s /q`(上千个文件),
|
||||
期间一直握着管道。调用方如果用 capture_output 就会**等管道 EOF**,
|
||||
永远等不到 -> 看起来像"程序卡死"。
|
||||
|
||||
修法:
|
||||
* Popen 时把 stdin/stdout/stderr 全部重定向到 DEVNULL,子进程不碰父进程的管道
|
||||
* 加 --keep-dir:让安装包的卸载器调用时不走自删
|
||||
(Inno 自己会删安装目录,两边一起删反而抢文件)
|
||||
* 补上 _kill_running() —— 之前定义了但从没被调用,文档说停进程其实没停
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -25,18 +47,44 @@ from .autostart import VALUE_NAME, RUN_KEY, LEGACY_VALUE_NAMES, _run
|
||||
from .config import app_dir, data_dir
|
||||
|
||||
_NO_WINDOW = getattr(subprocess, "CREATE_NO_WINDOW", 0)
|
||||
_DETACHED = 0x00000008 # DETACHED_PROCESS
|
||||
|
||||
# 子进程一律不碰父进程的句柄。
|
||||
# 少了这个,--uninstall 在 capture_output 场景下会"卡死"。
|
||||
_QUIET = {
|
||||
"stdin": subprocess.DEVNULL,
|
||||
"stdout": subprocess.DEVNULL,
|
||||
"stderr": subprocess.DEVNULL,
|
||||
"close_fds": True,
|
||||
}
|
||||
|
||||
|
||||
def _kill_running() -> str:
|
||||
"""把正在跑的进程停掉(除了自己)。"""
|
||||
"""停掉正在跑的**其它** ykxmz 实例(绝不能杀自己)。
|
||||
|
||||
⚠ 这里踩过一个大坑:
|
||||
原来写的是 taskkill /F /IM ykxmz.exe
|
||||
但 --uninstall 命令本身**就是 ykxmz.exe** ——
|
||||
这条命令把正在执行卸载的进程自己杀掉了,
|
||||
于是后面的"删开机自启""删数据目录"**全都没执行**。
|
||||
现象:卸载看着完成了,自启注册表项还留着。
|
||||
|
||||
正确做法:用 /FI "PID ne <自己的PID>" 把自己排除掉。
|
||||
"""
|
||||
if not getattr(sys, "frozen", False):
|
||||
return "(源码模式,跳过进程清理)"
|
||||
|
||||
exe = Path(sys.executable).name
|
||||
code, out = _run(["taskkill", "/F", "/IM", exe])
|
||||
me = os.getpid()
|
||||
code, out = _run([
|
||||
"taskkill", "/F",
|
||||
"/FI", "PID ne %d" % me,
|
||||
"/IM", exe,
|
||||
])
|
||||
if code == 0:
|
||||
return f"已停止进程 {exe}"
|
||||
if "not found" in out.lower() or "没有" in out:
|
||||
return "没有正在运行的进程"
|
||||
return f"已停止其它 {exe} 实例(本进程 {me} 除外)"
|
||||
if "not found" in out.lower() or "没有" in out or "128" in out:
|
||||
return "没有正在运行的其它实例"
|
||||
return f"停止进程:{out[:80]}"
|
||||
|
||||
|
||||
@@ -70,29 +118,49 @@ def _schedule_self_delete() -> str:
|
||||
|
||||
进程还在跑的时候删不掉自己,所以要起一个延迟的小命令,
|
||||
等我们退出后再删目录。
|
||||
|
||||
⚠ 两个坑:
|
||||
1. **必须重定向子进程的 stdio**。不重定向的话子进程会继承
|
||||
父进程的 stdout/stderr 句柄,调用方用 capture_output 时
|
||||
会一直等管道 EOF,表现为"卡死"。
|
||||
2. 用 `ping` 而不是 `timeout` 做延迟。`timeout` 需要一个控制台,
|
||||
在 DETACHED_PROCESS 下会立刻报错退出(那样就变成不延迟直接删,
|
||||
反而可能删到一半被占用)。ping 哪都能跑。
|
||||
"""
|
||||
if not getattr(sys, "frozen", False):
|
||||
return "(源码模式,不删自己)"
|
||||
folder = Path(sys.executable).resolve().parent
|
||||
cmd = (f'timeout /t 2 /nobreak >nul & '
|
||||
f'rmdir /s /q "{folder}"')
|
||||
# ping 3 次约 2 秒;>>nul 把它的输出吞掉
|
||||
cmd = f'ping 127.0.0.1 -n 3 >nul & rmdir /s /q "{folder}"'
|
||||
try:
|
||||
subprocess.Popen(["cmd", "/c", cmd],
|
||||
creationflags=_NO_WINDOW | 0x00000008)
|
||||
subprocess.Popen(
|
||||
["cmd", "/c", cmd],
|
||||
creationflags=_NO_WINDOW | _DETACHED,
|
||||
**_QUIET,
|
||||
)
|
||||
return f"已安排删除程序目录:{folder}"
|
||||
except Exception as exc: # noqa: BLE001
|
||||
return f"(没安排上自删,请手动删 {folder}:{exc})"
|
||||
|
||||
|
||||
def run(keep_data: bool = False) -> str:
|
||||
"""执行卸载,返回给人看的说明。"""
|
||||
def run(keep_data: bool = False, keep_dir: bool = False) -> str:
|
||||
"""执行卸载,返回给人看的说明。
|
||||
|
||||
keep_data 保留数据目录
|
||||
keep_dir 不安排自删程序目录 —— 安装包的卸载器调用时用这个。
|
||||
因为 Inno 自己会删 {app},两边一起删会抢文件。
|
||||
"""
|
||||
lines = ["开始卸载 " + VALUE_NAME]
|
||||
lines.append(" " + _kill_running())
|
||||
lines.append(" " + _remove_autostart())
|
||||
if keep_data:
|
||||
lines.append(" 保留数据目录(你要求保留)")
|
||||
else:
|
||||
lines.append(" " + _remove_data())
|
||||
lines.append(" " + _schedule_self_delete())
|
||||
if keep_dir:
|
||||
lines.append(" 程序目录交给安装包的卸载器删除")
|
||||
else:
|
||||
lines.append(" " + _schedule_self_delete())
|
||||
lines.append("")
|
||||
lines.append("卸载完成。程序目录会在几秒后自动删除。")
|
||||
lines.append("卸载完成。")
|
||||
return "\n".join(lines)
|
||||
|
||||
Reference in New Issue
Block a user