mirror of
https://gitee.com/xmz7891/ykxmz.git
synced 2026-10-18 13:40:45 +00:00
按用户定的规矩,这一轮有新功能,升第二位:2.0.0 -> 2.1.0。
== 一、网站挂了会自己救 ==
用户原话:「网站还是容易挂,建议继续增加隧道稳定性,并在后台设立
检测单元,实时检测网站状态,一旦检测到网站挂了,立即启动紧急预案」。
新增 relay/monitor.sh + lanremote-monitor.timer:
每 60 秒从**公网**查一次 /v1/health(不是查 127.0.0.1)
为什么不查本机:本机 200 只说明中转进程活着,用户真正关心的是
经过 Cloudflare 边缘 + 隧道 + 本机三段之后能不能打开。实测过
隧道两条实例都掉了、本机 curl 照样 200,只有从公网测才发现 502。
连续 3 次不通才动手(这条 4G 线路本来就有 2~4% 偶发失败),
救完 10 分钟内不再动手(否则预案刚重启完隧道还没连上,
下一次检查又救一遍,来回重启反而永远连不上)。
新增 relay/rescue.sh:五级阶梯,每级做完立刻复验,好了就停
1 重启隧道 不动中转,在线设备不掉线
2 重启中转服务 设备要重连,但很快
3 重新施加网络调优 MSS / MTU 跑偏时用
4 重启无线网卡 4G / WiFi 卡死时用
5 重启整机 最后的底牌,**默认不开**(要 RESCUE_REBOOT=1)
理由:如果问题出在供电或上游线路,重启只会让恢复更慢;
而且半夜自动重启一台没人管的机器风险太大。
== 二、抢救报告 + 后台分区 ==
用户原话:「如果紧急预案未能解决,则将问题打包成 .txt,然后在
中转站设置里面加一个分区专门放还有下载这个 txt。解决后将解决问题
的方法加入紧急预案」。
* 五级都没救回来 -> 把现场(内存/进程/隧道连接数/路由和 MTU、
三份日志尾部、内核异常)整理成 rescue_reports/<时间戳>.txt
* 后台新增「抢救报告」分区,列出报告(带结论摘要),点一下下载
* 下载走单独的 /rescue/<名字>,**不发缓存头**(内容会变,
也不该让 Cloudflare 边缘留着)。和 /download/ 分开是有意的:
那个是给用户的安装包,文件名带版本号、内容永不改变,
所以敢发 604800 长缓存
* **要登录才能下** —— 报告里有内网 IP、进程表、日志尾部,
是运维信息。第一版把这条路由插在 _admin_gate() 之前,
等于匿名可下,自己查出来补上了
* 每次自动抢救成功,把「第几级奏效」写进 rescue_knowledge.md;
监测单元读它,下次同类症状优先试历史上管用的那一级
(这就是用户说的"解决后将解决问题的方法加入紧急预案")
== 三、网络热门音效 ==
用户原话:「再加上近期网络热门音效,什么冰冰冰,啊哈哈哈哈,
各种老牧师音效,可以直接远程播放」。
**版权说明**:网上那些「冰冰冰」「啊哈哈哈」是**有版权的真实录音**
(影视片段 / 别人的作品),不能下载打包进安装包 —— 那既侵权,
也会让这个项目在 Gitee 那边直接踩线(这个仓库已经被判过一次
"内容可能含有违规信息")。
所以:
* 内置 8 个**程序合成**的音效(正弦/方波现场算,零版权风险、
不占安装包体积):冰冰冰 / 啊哈哈哈 / 老牧师 / 叮 / 当当当 /
警报 / 鼓点 / 弹簧。名字对着感觉来,但不是那些录音本身
* 做「自己放进来」的目录:mp3/wav 丢进 data/sfx/ 就自动出现在
列表里,照样能远程播。目录里放了一份说明
* 手表首页新增「音效」按钮,点开选一个,电脑就响
(复用现成的 showChooser,不新建面板)
协议加 sfx_list / sfx_play 两条命令。
== 四、修「安卓设置页一片空白」 ==
用户报的。根因:设置页**建好了但忘了 root.addView()** ——
showOnly() 让它 VISIBLE 也没用,它根本不在视图树里。
这类"漏挂"编译完全不报,只在运行时表现为空白。已加断言
_tests/test_watch_panels_wired.py 盯着:每个面板都必须挂到 root、
并且纳入 showOnly()。
== 五、全量测试改成后台静默 ==
用户原话:「把全量测试改为在后台静默运行不要打开任何窗口或锁屏」。
会开窗口的 6 个阶段(含开**全屏**那个)现在跑在独立的 Windows
隐藏桌面上(_tests/hidden_desktop.py)。窗口是**真的** —— 全屏、
可见、几何、Win32 枚举都真实,测试有效性一点没损失 ——
只是不在用户眼前,全屏也不会锁屏。
顺带修了一个一直在骗人的检测:判断"锁屏"时只看 OpenInputDesktop
成不成功是不对的 —— **锁屏时它同样成功**(那时输入桌面是 Winlogon)。
于是截屏类测试在锁屏时被记成 FAIL 而不是 SKIP,把真问题淹掉了
(用户电脑锁了一夜,我就是被这个误导过)。改成看输入桌面的**名字**。
现在锁屏也能跑全量:该 SKIP 的 SKIP,该跑的照跑。
== 六、顺手修的真 bug ==
* windows/lanremote/server.py 的 sfx_play 分支用了不存在的变量名 body
(应该是 msg)-> **一调就 NameError**。pyflakes 那关抓出来的,
说明"未定义名"这条检查有用。
* _build/package_remote.py 的文件白名单只有 .py/.sh/.ico,
**.service/.timer 收不进服务端 zip** -> 新机器上监测根本不启动,
而且不报错、只是静默不跑。已加。
* protocol.py 被我补丁插入了 LF 行到 CRLF 文件里 -> 行尾混用,编码审计红。
* deploy.sh 的 watchdog 块被我改锚点时吃掉了 start/say 两行
(bash -n 通过、只是少两条执行语句)—— 改完读回来核对才发现。
* _build/CHANGELOG.md 的 2.1.0 说明里写了"管理后台",
触发对外材料清洁度检查。改成不提管理页(用户定的规矩:
对外只能提官网)。
全量回归 46 个阶段全部通过。
458 lines
18 KiB
Plaintext
458 lines
18 KiB
Plaintext
; ============================================================================
|
||
; ykxmz 教室电脑手表遥控 · 被控端安装包
|
||
;
|
||
; 用 Inno Setup 6 编译:
|
||
; "C:\Program Files (x86)\Inno Setup 6\ISCC.exe" installer\ykxmz.iss
|
||
;
|
||
; ── 设计说明 ──
|
||
;
|
||
; 尽量用 Inno 自带的机制,不手搓:
|
||
; * 许可协议页 -> LicenseFile(三份文档合并成一份,点"我接受"才能继续)
|
||
; * 开机自启 -> [Tasks](标准勾选框)
|
||
; * 装完启动 -> [Run] postinstall(标准勾选框)
|
||
; * 安装目录 -> DisableDirPage=no(让用户自己选,默认 Program Files)
|
||
; * 卸载 -> [UninstallRun] 调被控端自己的 --uninstall
|
||
; 只有"填 6 位配对码"这一页 Inno 没有现成的,用标准的
|
||
; CreateInputQueryPage —— 那是 Inno 的官方 API,不算自己造轮子。
|
||
;
|
||
; ── 用户确定的设置 ──
|
||
; 安装位置 默认 C:\Program Files\ykxmz,安装时可改
|
||
; 数据目录 能写 exe 旁就写,不能就转 ProgramData(程序内部处理)
|
||
; 三份文档 放安装包里,统一接受才能下一步
|
||
; 配对码 安装向导里填 6 位
|
||
; 快捷方式 不建开始菜单、不建桌面
|
||
; 开机自启 默认勾上
|
||
; 装完启动 立即静默启动
|
||
; 卸载 一并删掉,什么都不留
|
||
; ============================================================================
|
||
|
||
#define MyAppName "ykxmz"
|
||
#define MyAppVersion "2.1.0"
|
||
#define MyAppPublisher "小麦子"
|
||
#define MyAppExeName "ykxmz.exe"
|
||
|
||
[Setup]
|
||
AppId={{8F3C1D42-7A5E-4B91-9C6D-2E8F4A1B7C30}
|
||
AppName={#MyAppName}
|
||
AppVersion={#MyAppVersion}
|
||
AppVerName={#MyAppName} {#MyAppVersion}
|
||
AppPublisher={#MyAppPublisher}
|
||
VersionInfoVersion={#MyAppVersion}
|
||
|
||
; 默认装到 Program Files\ykxmz,但安装向导里有一页让用户自己改
|
||
DefaultDirName={autopf}\ykxmz
|
||
DisableDirPage=no
|
||
|
||
; 三份文档合并成一份许可页:看完点「我接受」才能继续
|
||
LicenseFile=license-combined.txt
|
||
|
||
; 不建开始菜单组(用户明确不要)
|
||
DisableProgramGroupPage=yes
|
||
AllowNoIcons=yes
|
||
|
||
; 装到 Program Files 和改防火墙都要管理员
|
||
PrivilegesRequired=admin
|
||
ArchitecturesAllowed=x64compatible
|
||
ArchitecturesInstallIn64BitMode=x64compatible
|
||
|
||
; 安装/升级前由 PrepareToInstall 自己强杀正在运行的被控端。
|
||
;
|
||
; 为什么不用 Inno 自带的关程序机制(默认开着):
|
||
; 它只会礼貌地"请求"对方退出,遇上以管理员身份启动的常驻进程
|
||
; 关不掉,然后弹出"安装程序无法自动关闭所有应用程序",
|
||
; 静默模式下这个消息框被压成"终止",**安装直接回滚** ——
|
||
; 用户看到的现象是"装了没反应/还是旧版本"。
|
||
; (2026-10 实测踩到:旧实例在跑,新版本装不上,配对码也没写进去。)
|
||
;
|
||
; 我们自己在 PrepareToInstall 里 taskkill /F,干净利落。
|
||
; 关掉 Inno 自带的那套,免得它先弹框把安装拦下来。
|
||
CloseApplications=no
|
||
RestartApplications=no
|
||
|
||
OutputDir=..\dist
|
||
OutputBaseFilename=ykxmz-setup-{#MyAppVersion}
|
||
SetupIconFile=..\windows\app.ico
|
||
UninstallDisplayIcon={app}\{#MyAppExeName}
|
||
UninstallDisplayName={#MyAppName}
|
||
|
||
Compression=lzma2/max
|
||
SolidCompression=yes
|
||
WizardStyle=modern
|
||
ShowLanguageDialog=no
|
||
DisableWelcomePage=no
|
||
|
||
[Languages]
|
||
Name: "chinese"; MessagesFile: "compiler:Default.isl"
|
||
|
||
[Tasks]
|
||
; 用户要的:默认勾上开机自启(取消勾选也允许)
|
||
Name: "autostart"; Description: "开机自动启动(推荐给教室电脑)"; \
|
||
GroupDescription: "启动选项:"; Flags: checkedonce
|
||
|
||
[Files]
|
||
; 被控端整个目录(ykxmz.exe + _internal)
|
||
Source: "..\dist_exe\ykxmz\*"; DestDir: "{app}"; \
|
||
Flags: recursesubdirs createallsubdirs ignoreversion
|
||
|
||
; 三份文档也放一份到安装目录,装完还能翻
|
||
Source: "..\docs\LICENSE.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||
Source: "..\docs\BEFORE-INSTALL.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||
Source: "..\docs\AFTER-INSTALL.txt"; DestDir: "{app}\docs"; Flags: ignoreversion
|
||
|
||
[Dirs]
|
||
; ⚠ 这一节很关键 —— 不配的话装完一运行就报 WinError 5 拒绝访问。
|
||
;
|
||
; 被控端的配置存在 %ProgramData%\ykxmz\data 下(exe 旁边写不进去时的回退)。
|
||
; 安装包是管理员跑的,它建的目录默认只有管理员能改;
|
||
; 之后普通用户(老师的账号)跑被控端时:
|
||
; 能新建文件,但覆盖不了管理员建的那份 settings.json
|
||
; 结果就是"装完打开报权限错误"。
|
||
;
|
||
; Permissions: users-modify 把所有用户的写权限打开,才是真的能用。
|
||
; 注意末尾不带反斜杠,否则 Inno 会给它单独建一层同名子目录。
|
||
Name: "{commonappdata}\ykxmz"; Permissions: users-modify
|
||
Name: "{commonappdata}\ykxmz\data"; Permissions: users-modify
|
||
|
||
[InstallDelete]
|
||
; 早期版本把配置写在程序目录下的 data\ 里。
|
||
; 现在数据统一放 ProgramData(见 config.py 的 data_dir),
|
||
; 这里把旧的那份清掉 —— 留着的话,两份配置并存,
|
||
; 以后排查问题会被误导("我明明改了配对码怎么没变")。
|
||
Type: filesandordirs; Name: "{app}\data"
|
||
|
||
[Run]
|
||
; 收尾三步,顺序很重要:先写配对码,再设开机自启,最后才启动。
|
||
;
|
||
; ⚠ 为什么放在这里,而不是 [Code] 的 CurStepChanged(ssPostInstall):
|
||
; 实测这个 Inno 版本的 ssPostInstall 回调**不会触发**
|
||
; (日志显示回调进来了、CurStep=1,但 CurStep <> ssPostInstall 成立;
|
||
; 而 Ord(ssPostInstall) 又报类型不匹配,没法打出来对比)。
|
||
; 放在那里 --code 永远不执行,配对码永远是空的。
|
||
; Run 段是安装完成后必然执行的,靠得住。
|
||
;
|
||
; {code:GetPairingCode} 是 Inno 的写法:调同名 Pascal 函数取值。
|
||
; Check 函数返回 False 就跳过这一条。
|
||
|
||
; 1. 写配对码
|
||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--code {code:GetPairingCode}"; \
|
||
Flags: runhidden; Check: HasPairingCode
|
||
|
||
; 1b. 写中转服务器(向导里留空就跳过)。
|
||
; 用户要求"安装时就能设中转服务器" —— 装的时候是第一次设置,
|
||
; 最该一次填好,不然装完还得去双击 exe 找设置窗口。
|
||
;
|
||
; 用了 --relay-url 而不是 --relay:
|
||
; --relay 会连接一次然后退出(人工排查用),
|
||
; 放在这条命令链里会让后面的 --silent 跑不到
|
||
; --relay-url 只写配置,不连不问,适合安装流程
|
||
;
|
||
; 地址两侧的引号是必须的:URL 里有 "//" 和可能的 ":",
|
||
; 不加引号 Inno 会按空格切参数。
|
||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--relay-url ""{code:GetRelayUrl}"""; \
|
||
Flags: runhidden; Check: HasRelay
|
||
|
||
; 2. 开机自启
|
||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--autostart on"; \
|
||
Flags: runhidden; Tasks: autostart
|
||
|
||
; 3. 启动。
|
||
; 非静默:标准 postinstall 勾选框,用户可以取消。
|
||
; 静默: /VERYSILENT 下 postinstall 那条被 skipifsilent 跳过,所以单独来一条。
|
||
;
|
||
; ⚠ 都用 nowait —— --silent 是常驻守护进程,等它退出等于永远不返回。
|
||
; 代价是安装器和它是父子关系:用"等整个进程树"的方式调安装器会卡住
|
||
; (PowerShell 的 Start-Process -Wait 就是)。
|
||
; 学校批量部署请用 cmd:
|
||
; cmd /c ykxmz-setup-2.1.0.exe /VERYSILENT /CODE=123456
|
||
; cmd 只等安装器本身,不等孙子进程,不会卡。
|
||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||
Description: "立即启动(后台运行,不显示窗口)"; \
|
||
Flags: nowait postinstall skipifsilent
|
||
|
||
Filename: "{app}\{#MyAppExeName}"; Parameters: "--silent"; \
|
||
Flags: nowait runhidden; Check: WizardSilent
|
||
|
||
; 让用户看一眼装完须知
|
||
Filename: "{app}\docs\AFTER-INSTALL.txt"; \
|
||
Description: "打开「安装后须知」"; \
|
||
Flags: postinstall shellexec unchecked skipifsilent
|
||
|
||
[UninstallDelete]
|
||
; 被控端会自己删数据目录,这里兜底再清一遍安装目录里的残留。
|
||
; {commonappdata}\ykxmz 也要清 —— 安装时 [Dirs] 建了它,
|
||
; 卸载时**不会自动删**([Dirs] 建的目录不归 [UninstallDelete] 管),
|
||
; 不写会留个空目录在那儿。
|
||
Type: filesandordirs; Name: "{app}\data"
|
||
Type: filesandordirs; Name: "{app}\docs"
|
||
Type: filesandordirs; Name: "{app}\_internal"
|
||
Type: filesandordirs; Name: "{commonappdata}\ykxmz"
|
||
|
||
; Inno 默认只在目录空时删它,而卸载器自己的临时文件
|
||
; (_unins.tmp 之类)经常让目录"非空",于是留下一个空壳。
|
||
; 显式写 dirifempty 兜底。
|
||
Type: dirifempty; Name: "{app}"
|
||
|
||
[Code]
|
||
var
|
||
CodePage: TInputQueryWizardPage;
|
||
ForcedCode: String;
|
||
ForcedRelay: String;
|
||
|
||
{ 把字符串里的非数字滤掉。 }
|
||
function OnlyDigits(const raw: String): String;
|
||
var
|
||
i: Integer;
|
||
ch: Char;
|
||
begin
|
||
Result := '';
|
||
for i := 1 to Length(raw) do
|
||
begin
|
||
ch := raw[i];
|
||
if (ch >= '0') and (ch <= '9') then
|
||
Result := Result + ch;
|
||
end;
|
||
end;
|
||
|
||
{ 配对码 + 中转服务器那一页。
|
||
Inno 没有现成的输入页,用官方的 CreateInputQueryPage(不算自己造轮子)。
|
||
|
||
为什么要在这里也能填中转服务器(用户要求):
|
||
装的时候是"第一次设置",那时候最该一次把地址填好 ——
|
||
不然装完还得让人去双击 exe 找设置窗口,很容易漏。
|
||
装完之后照样能改:双击 exe 打开设置窗口就有这一栏。 }
|
||
procedure InitializeWizard();
|
||
begin
|
||
{ 静默部署可以带 /CODE=123456 /RELAY=https://... 直接把值传进来。
|
||
注意:Inno 的 [Code] 段连**注释里的花括号**都会当常量去解析,
|
||
所以这里不能直接写 param 那种写法,否则编译器报语法错误。 }
|
||
ForcedCode := OnlyDigits(ExpandConstant('{param:CODE|}'));
|
||
ForcedRelay := Trim(ExpandConstant('{param:RELAY|}'));
|
||
|
||
CodePage := CreateInputQueryPage(wpSelectTasks,
|
||
'配对码和中转服务器',
|
||
'给这台电脑定一个 6 位配对码',
|
||
'手表上要输这一串数字才能连上这台电脑。自己定个好记的,' +
|
||
'比如 789113。' + #13#10 + #13#10 +
|
||
'中转服务器是**跨网络**才需要的(比如在办公室管教室)。' +
|
||
'只在一个 WiFi 里用就留空,那样手表和电脑直接连,最快。');
|
||
CodePage.Add('6 位配对码:', False);
|
||
CodePage.Add('中转服务器(可以不填):', False);
|
||
if ForcedCode <> '' then
|
||
CodePage.Values[0] := ForcedCode;
|
||
if ForcedRelay <> '' then
|
||
CodePage.Values[1] := ForcedRelay;
|
||
end;
|
||
|
||
{ 规范化中转地址。规则必须和 run_agent.py 的 normalize_relay_url() 一致:
|
||
去首尾空白 -> 没协议头就补 https:// -> 去末尾斜杠
|
||
两边不一致会出现"向导里显示一个样、文件里存另一个样"的怪事。
|
||
|
||
⚠ 不用 LowerCase:Inno Pascal 各版本支持不一致,
|
||
直接四种写法都比一遍最保险。 }
|
||
function NormalizeRelay(raw: String): String;
|
||
var
|
||
s: String;
|
||
begin
|
||
s := Trim(raw);
|
||
if s = '' then
|
||
begin
|
||
Result := '';
|
||
Exit;
|
||
end;
|
||
{ 先削末尾斜杠再补协议头 —— 顺序反了会把 https:// 的斜杠也削掉 }
|
||
while (Length(s) > 0) and (s[Length(s)] = '/') do
|
||
Delete(s, Length(s), 1);
|
||
if (Pos('http://', s) <> 1) and (Pos('HTTP://', s) <> 1)
|
||
and (Pos('https://', s) <> 1) and (Pos('HTTPS://', s) <> 1) then
|
||
s := 'https://' + s;
|
||
Result := s;
|
||
end;
|
||
|
||
{ 校验:必须是 6 位数字。
|
||
注意 —— **静默模式下这一页不会显示**,值是空的,
|
||
这时候绝不能拦(否则 /VERYSILENT 安装必然失败,我第一版就栽在这)。 }
|
||
function NextButtonClick(CurPageID: Integer): Boolean;
|
||
var
|
||
raw: String;
|
||
digits: String;
|
||
begin
|
||
Result := True;
|
||
if CurPageID <> CodePage.ID then
|
||
Exit;
|
||
if WizardSilent then
|
||
Exit;
|
||
|
||
raw := CodePage.Values[0];
|
||
digits := OnlyDigits(raw);
|
||
if (Length(digits) <> 6) and (raw <> '') then
|
||
begin
|
||
MsgBox('配对码要 6 位数字。' + #13#10 + #13#10 +
|
||
'你现在填的是:' + raw + #13#10 +
|
||
'(过滤掉非数字后剩 ' + IntToStr(Length(digits)) + ' 位)' + #13#10 + #13#10 +
|
||
'重新填一下,或者直接点「取消」退出安装。',
|
||
mbError, MB_OK);
|
||
Result := False;
|
||
Exit;
|
||
end;
|
||
{ 规范化,后面 [Run] 阶段用得到 }
|
||
CodePage.Values[0] := digits;
|
||
|
||
{ ---- 中转地址的检查 ----
|
||
只做最基本的:不许有空格、不许有明显不对的字符。
|
||
不做"能不能连上"的检查 —— 装的时候那台服务器可能还没起,
|
||
卡在这里会让用户装不下去。地址写错了装完还能改。 }
|
||
raw := Trim(CodePage.Values[1]);
|
||
if raw <> '' then
|
||
begin
|
||
if Pos(' ', raw) > 0 then
|
||
begin
|
||
MsgBox('中转服务器地址里不能有空格。' + #13#10 + #13#10 +
|
||
'你填的是:' + raw + #13#10 + #13#10 +
|
||
'只填域名就行,比如 relay.你的域名.com' + #13#10 +
|
||
'(协议头 https:// 不写也会自动加上)',
|
||
mbError, MB_OK);
|
||
Result := False;
|
||
Exit;
|
||
end;
|
||
if Pos('.', raw) = 0 then
|
||
begin
|
||
MsgBox('中转服务器地址看着不像个域名。' + #13#10 + #13#10 +
|
||
'你填的是:' + raw + #13#10 + #13#10 +
|
||
'应该形如 relay.你的域名.com。' + #13#10 +
|
||
'只在一个 WiFi 里用的话,这一栏留空即可。',
|
||
mbError, MB_OK);
|
||
Result := False;
|
||
Exit;
|
||
end;
|
||
end;
|
||
{ 规范化后再写回,让用户看到最终会用哪个地址 }
|
||
CodePage.Values[1] := NormalizeRelay(raw);
|
||
end;
|
||
|
||
{ 最终用的配对码:命令行 /CODE 优先,其次是向导里填的。
|
||
两个都没有就返回空串 —— 那就跳过 --code,让被控端自己生成一个。 }
|
||
function EffectiveCode(): String;
|
||
begin
|
||
Result := ForcedCode;
|
||
if Result = '' then
|
||
Result := OnlyDigits(CodePage.Values[0]);
|
||
if Length(Result) <> 6 then
|
||
Result := '';
|
||
end;
|
||
|
||
{ 收尾命令现在放在 [Run] 段(见那里的注释)。
|
||
原来放在 CurStepChanged(ssPostInstall) 里 —— 实测这个 Inno 版本的
|
||
ssPostInstall **根本不会触发**,所以 --code 从来没执行过,
|
||
配对码永远是空的。留这个函数只是为了以后有人想改回来时能看到这段历史。 }
|
||
procedure CurStepChanged(CurStep: TSetupStep);
|
||
begin
|
||
{ 故意什么都不做。真要恢复旧写法,先确认 ssPostInstall 会触发。 }
|
||
end;
|
||
|
||
{ ===== 给 [Run] 段用的取值 / 判断函数 ===== }
|
||
|
||
{ 配对码。配 HasPairingCode 一起用 —— 空串时那条命令会被跳过。 }
|
||
function GetPairingCode(Param: String): String;
|
||
begin
|
||
Result := EffectiveCode();
|
||
end;
|
||
|
||
{ 有配对码才跑那条 --code 命令 }
|
||
function HasPairingCode(): Boolean;
|
||
begin
|
||
Result := EffectiveCode() <> '';
|
||
end;
|
||
|
||
{ 最终用的中转地址:命令行 /RELAY 优先,其次是向导里填的。
|
||
空的就返回空串 —— 那条命令会被 HasRelay 跳过。 }
|
||
function EffectiveRelay(): String;
|
||
begin
|
||
Result := NormalizeRelay(ForcedRelay);
|
||
if Result = '' then
|
||
Result := NormalizeRelay(CodePage.Values[1]);
|
||
end;
|
||
|
||
{ 中转服务器。配 HasRelay 一起用 —— 留空时那条命令会被跳过,
|
||
这样"只在一个 WiFi 里用"的人不会多写一个空地址进配置。 }
|
||
function GetRelayUrl(Param: String): String;
|
||
begin
|
||
Result := EffectiveRelay();
|
||
end;
|
||
|
||
{ 填了中转地址才跑那条 --relay-url 命令 }
|
||
function HasRelay(): Boolean;
|
||
begin
|
||
Result := EffectiveRelay() <> '';
|
||
end;
|
||
|
||
{ 卸载前先停掉正在跑的进程,不然文件删不干净。
|
||
|
||
必须在这里做,**不能**改到 UninstallRun 区段去:
|
||
UninstallRun 是在文件都删完之后才跑的,
|
||
那时候"停进程"已经没意义了(文件早没了)。
|
||
|
||
必须带 --keep-dir:
|
||
InitializeUninstall 在卸载器开始删文件**之前**执行。
|
||
不带 --keep-dir 的话被控端会安排一个后台 rmdir 删安装目录,
|
||
而卸载器紧接着也在删同一个目录 —— 两边抢文件会**永久卡住**。
|
||
(2026-10 实测:从控制面板卸载无限等待。两个原因叠加:
|
||
这一行没带 --keep-dir;以及被控端里 input() 在无 stdin 时阻塞。)
|
||
}
|
||
{ 安装开始前强杀正在运行的被控端。
|
||
|
||
必须自己杀,不能指望 Inno 自带的关程序机制 ——
|
||
它关不掉以管理员身份启动的常驻进程,然后弹框,静默模式下
|
||
被压成"终止",导致整个安装回滚。
|
||
|
||
taskkill /F /IM ykxmz.exe 只会杀别的实例;
|
||
这个进程本身是 setup 的辅助进程,名字不是 ykxmz.exe,不受影响。
|
||
|
||
返回空串 = 没问题,可以继续安装。 }
|
||
function PrepareToInstall(var NeedsRestart: Boolean): String;
|
||
var
|
||
ResultCode: Integer;
|
||
begin
|
||
Result := '';
|
||
NeedsRestart := False;
|
||
|
||
if Exec('taskkill.exe', '/F /IM ykxmz.exe', '',
|
||
SW_HIDE, ewWaitUntilTerminated, ResultCode) then
|
||
begin
|
||
{ 0 = 杀掉了;128 = 本来就没有在跑。都算正常。 }
|
||
end;
|
||
|
||
{ 给它一点时间释放文件句柄,否则覆盖 exe 还是会"拒绝访问" }
|
||
Sleep(1200);
|
||
end;
|
||
|
||
{ 卸载前先停掉正在跑的进程,不然文件删不干净。
|
||
|
||
三个参数缺一不可,每个都对应一个实测踩过的卡死/残留问题:
|
||
|
||
* 必须在这里做,**不能**改到 UninstallRun 区段去
|
||
—— UninstallRun 是在文件都删完之后才跑的,
|
||
那时候"停进程"已经没意义了(文件早没了)。
|
||
|
||
* 必须带 --keep-dir
|
||
—— InitializeUninstall 在卸载器开始删文件**之前**执行。
|
||
不带的话被控端会安排一个后台 rmdir 删安装目录,
|
||
而卸载器紧接着也在删同一个目录 —— 两边抢文件**永久卡住**。
|
||
|
||
* 必须带 --quiet
|
||
—— 卸载器是 runhidden 调的,进程没有控制台。
|
||
被控端里的 attach_console() 会 AllocConsole + open("CONIN$"),
|
||
在无控制台的隐藏进程里**永久阻塞**;input() 也因为没 stdin 卡住。
|
||
实测卡住的进程只有 1 个线程、0.16 秒 CPU,就是停在这一步。
|
||
--quiet 让它彻底不碰控制台,结果写进 uninstall.log。
|
||
实测:不带 --quiet 无限卡住,带了 0.6 秒返回。
|
||
}
|
||
function InitializeUninstall(): Boolean;
|
||
var
|
||
ResultCode: Integer;
|
||
begin
|
||
Result := True;
|
||
Exec(ExpandConstant('{app}\{#MyAppExeName}'),
|
||
'--uninstall --keep-dir --quiet',
|
||
ExpandConstant('{app}'), SW_HIDE, ewWaitUntilTerminated, ResultCode);
|
||
end;
|