mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-19 15:20:44 +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:
@@ -0,0 +1,120 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
r'''验证卸载模块的逻辑(不依赖提权、不依赖真的装一遍)。
|
||||
|
||||
为什么单独测这个:
|
||||
安装包的整体周期测试需要管理员权限,而这个环境里有几个
|
||||
杀不掉的僵尸卸载器进程挡着。但**卸载逻辑本身**可以直接调,
|
||||
不需要装、不需要提权。
|
||||
|
||||
要验证的三件事(都是这轮修的真 bug):
|
||||
1. _kill_running 不能用 taskkill /IM 把自己也杀掉
|
||||
—— 那样后面的删自启/删数据全都不执行
|
||||
2. run() 必须真的调到 _remove_autostart
|
||||
3. run(keep_dir=True) 不能安排自删(会和 Inno 卸载器抢目录)
|
||||
'''
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
|
||||
sys.path.insert(0, str(ROOT / "windows"))
|
||||
|
||||
import lanremote.uninstall as uni # noqa: E402
|
||||
|
||||
passed = failed = 0
|
||||
|
||||
|
||||
def check(name, ok, detail=""):
|
||||
global passed, failed
|
||||
if ok:
|
||||
passed += 1
|
||||
print(" [PASS] " + name)
|
||||
else:
|
||||
failed += 1
|
||||
print(" [FAIL] " + name + " " + str(detail))
|
||||
|
||||
|
||||
def main() -> int:
|
||||
print(" 一、_kill_running 不会杀自己")
|
||||
source = (ROOT / "windows" / "lanremote" / "uninstall.py").read_text(
|
||||
encoding="utf-8")
|
||||
check("用了 PID 排除(/FI \"PID ne\")",
|
||||
'PID ne' in source and "os.getpid()" in source,
|
||||
"没找到排除自己的逻辑")
|
||||
check("没有裸的 /IM ykxmz.exe",
|
||||
'"/IM", exe' in source and 'PID ne' in source,
|
||||
"taskkill 参数形态不对")
|
||||
# 直接模拟一下
|
||||
os.environ.setdefault("YKXMZ_DATA", str(ROOT / "_build" / "_uninst_test"))
|
||||
result = uni._kill_running()
|
||||
print(" _kill_running() -> %s" % result)
|
||||
check("调用后本进程还活着", True) # 能走到这里就说明没被杀
|
||||
|
||||
print()
|
||||
print(" 二、run() 会依次做三件事")
|
||||
calls = []
|
||||
originals = {
|
||||
"_kill_running": uni._kill_running,
|
||||
"_remove_autostart": uni._remove_autostart,
|
||||
"_remove_data": uni._remove_data,
|
||||
"_schedule_self_delete": uni._schedule_self_delete,
|
||||
}
|
||||
|
||||
def spy(name, value):
|
||||
def wrapper(*args, **kwargs):
|
||||
calls.append(name)
|
||||
if callable(value):
|
||||
return value(*args, **kwargs)
|
||||
return value
|
||||
return wrapper
|
||||
|
||||
try:
|
||||
uni._kill_running = spy("kill", originals["_kill_running"])
|
||||
uni._remove_autostart = spy("autostart", lambda: "(测试替身)")
|
||||
uni._remove_data = spy("data", lambda: "(测试替身)")
|
||||
uni._schedule_self_delete = spy("selfdel", lambda: "(测试替身)")
|
||||
|
||||
text = uni.run(keep_data=True) # keep_data 免得真删数据
|
||||
print(" run() 返回:")
|
||||
for line in text.splitlines():
|
||||
print(" " + line)
|
||||
|
||||
check("调了 _kill_running", "kill" in calls, calls)
|
||||
check("调了 _remove_autostart(这一步以前会被自己的 taskkill 干掉)",
|
||||
"autostart" in calls, calls)
|
||||
check("调了 _schedule_self_delete", "selfdel" in calls, calls)
|
||||
|
||||
print()
|
||||
print(" 三、keep_dir=True 时不安排自删")
|
||||
calls.clear()
|
||||
uni.run(keep_data=True, keep_dir=True)
|
||||
check("调了 _kill_running", "kill" in calls, calls)
|
||||
check("调了 _remove_autostart", "autostart" in calls, calls)
|
||||
check("**没有**安排自删(避免和 Inno 卸载器抢目录)",
|
||||
"selfdel" not in calls, calls)
|
||||
|
||||
finally:
|
||||
uni._kill_running = originals["_kill_running"]
|
||||
uni._remove_autostart = originals["_remove_autostart"]
|
||||
uni._remove_data = originals["_remove_data"]
|
||||
uni._schedule_self_delete = originals["_schedule_self_delete"]
|
||||
|
||||
print()
|
||||
print(" 四、_schedule_self_delete 重定向了子进程 stdio")
|
||||
check("Popen 用了 DEVNULL 重定向",
|
||||
"_QUIET" in source and "DEVNULL" in source,
|
||||
"没重定向的话 capture_output 会等管道 EOF,表现为卡死")
|
||||
check("用 ping 而不是 timeout 做延迟",
|
||||
"ping 127.0.0.1" in source,
|
||||
"timeout 在 DETACHED_PROCESS 下没控制台,会立刻失败")
|
||||
|
||||
print()
|
||||
print("=" * 54)
|
||||
print(" 通过 %d 项,失败 %d 项" % (passed, failed))
|
||||
return 0 if failed == 0 else 1
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Reference in New Issue
Block a user