Files
ykxmz/installer/ykxmz.iss
T
2026-10-07 15:44:49 +08:00

353 lines
14 KiB
Plaintext
Raw 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.
; ============================================================================
; 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 "1.8.2"
#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
; 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-1.8.2.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;
{ 把字符串里的非数字滤掉。 }
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。 }
procedure InitializeWizard();
begin
{ 静默部署可以带 /CODE=123456 直接把码传进来(电教批量装用这个)。
注意:Inno 的 [Code] 段连**注释里的花括号**都会当常量去解析,
所以这里不能直接写 param 那种写法,否则编译器报语法错误。 }
ForcedCode := OnlyDigits(ExpandConstant('{param:CODE|}'));
CodePage := CreateInputQueryPage(wpSelectTasks,
'配对码',
'给这台电脑定一个 6 位配对码',
'手表上要输这一串数字才能连上这台电脑。自己定个好记的,' +
'比如 789113。装完之后也可以在被控端窗口里改。');
CodePage.Add('6 位配对码:', False);
if ForcedCode <> '' then
CodePage.Values[0] := ForcedCode;
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;
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;
{ 卸载前先停掉正在跑的进程,不然文件删不干净。
必须在这里做,**不能**改到 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;