Files
ykxmz/_build/test_uninstall_logic.py
xmz7891 df8056dd16 修四个安装/卸载卡死的 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 项全通过
2026-10-07 01:19:11 +08:00

121 lines
4.4 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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())