Files
ykxmz/_build/check_iss.py
T
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

141 lines
4.3 KiB
Python

# -*- coding: utf-8 -*-
r'''检查 Inno Setup 的 .iss 里有没有会害编译失败的写法。
踩过的三个坑,都是 Inno 的 [Code] 解析器**连注释内容也处理**:
1. 注释里写方括号区段名
{ 规范化,后面 [Run] 阶段用得到 }
报 "无效的区段标记",编译直接失败。踩了两次。
2. 注释里写花括号常量
{ 不带 --keep-dir 的话会 rmdir 删 {app} }
Inno 会把 {app} 展开,展开后花括号配不上 ->
报 "Syntax error",编译失败。踩了一次。
3. 注释里写 {param:XXX|}
Inno 会展开它,参数没传就展开成空,同样破坏括号配对。
所以规则:**[Code] 段落里,注释内容不能出现 [Section] 或 {常量}。**
⚠ 判断"是不是注释"必须跳过字符串字面量:
ExpandConstant('{app}\x') <- 这里的 { 在字符串里,不是注释
{ 这是注释 } <- 这里的 { 才是注释
'''
from __future__ import annotations
import re
import sys
from pathlib import Path
ISS = (Path(r"C:\Users\shu\Desktop\deepseekagent\LanRemote")
/ "installer" / "ykxmz.iss")
SECTIONS = {
"Setup", "Types", "Components", "Tasks", "Dirs", "Files", "Icons",
"Ini", "InstallDelete", "Languages", "Messages", "CustomMessages",
"Registry", "Run", "UninstallRun", "UninstallDelete", "Code",
}
def scan_comment_text(line: str, in_comment: bool):
"""返回 (这一行注释里的文字, 扫完后的 in_comment)。
关键:字符串字面量里的 { } 不算注释。
"""
out = []
cursor = 0
in_string = False
length = len(line)
while cursor < length:
ch = line[cursor]
if in_string:
if ch == "'":
# Pascal 用 '' 表示一个单引号
if cursor + 1 < length and line[cursor + 1] == "'":
cursor += 2
continue
in_string = False
cursor += 1
continue
if not in_comment:
if ch == "'":
in_string = True
elif ch == "{":
in_comment = True
cursor += 1
continue
# 在注释里
if ch == "}":
in_comment = False
cursor += 1
continue
out.append(ch)
cursor += 1
return "".join(out), in_comment
def main() -> int:
if not ISS.is_file():
print(" 找不到 %s" % ISS)
return 1
lines = ISS.read_text(encoding="utf-8").splitlines()
code_start = None
for index, line in enumerate(lines):
if line.strip() == "[Code]":
code_start = index
break
if code_start is None:
print(" 没有 [Code] 段落,跳过")
return 0
problems = []
in_comment = False
for index in range(code_start, len(lines)):
line = lines[index]
comment_text, in_comment = scan_comment_text(line, in_comment)
if not comment_text:
continue
# 只有**行首**的 [Section] 才会被 Inno 当成区段标记。
# 行中间的("交给 Run 段用...")不影响 —— 实测编译能过。
m = re.match(r"\s*\[([A-Za-z][A-Za-z0-9]*)\]", comment_text)
if m and m.group(1) in SECTIONS:
problems.append((index + 1,
"注释**行首**有区段名 [%s]" % m.group(1),
line.strip()))
# 花括号常量:注释里出现就会被展开
for match in re.finditer(
r"\{[A-Za-z][A-Za-z0-9]*(?:[:|][^}]*)?\}", comment_text):
problems.append((index + 1,
"注释里有会展开的常量 %s" % match.group(0),
line.strip()))
# 真区段必须顶格
for index, line in enumerate(lines):
if re.match(r"^\s+\[[A-Za-z]+\]\s*$", line):
problems.append((index + 1, "区段名前面有空格", line.strip()))
if problems:
print(" 发现 %d 个会让编译失败的问题:" % len(problems))
for lineno, why, text in problems:
print(" 行 %d: %s" % (lineno, why))
print(" %s" % text[:74])
return 1
print(" [Code] 段落里没有违规写法 ✓")
return 0
if __name__ == "__main__":
sys.exit(main())