Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 17 additions & 3 deletions apps/desktop/resources/installer.nsh
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,30 @@
; 接受);dev 仍独立名,dev 安装器绝不误伤同机并存的正式安装。注册表键名
; Windows 大小写不敏感,shell 键 "Cindy" 与历史写入的 "cindy" 是同一个键,
; 行为零变化。
!macro customInit
; Check if the app is already running
; 安装与卸载都必须在改动安装目录前停掉正在运行的 app。尤其是卸载:
; 如果先删 resources 再让存量窗口处理关闭事件,托盘图标创建会失败,主窗口也会
; 因「收起到托盘」语义而保持打开。两个入口共用这段循环,避免以后只修一侧。
!macro stopRunningProduct CONFIRM_MESSAGE
check_running:
nsProcess::_FindProcess "${APP_EXECUTABLE_FILENAME}"
Pop $R0
${If} $R0 == 0
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION \
"${PRODUCT_FILENAME} 正在运行,请先关闭后再继续安装。$\n$\n点击「确定」将在关闭后继续。" \
"${CONFIRM_MESSAGE}" \
/SD IDOK \
IDOK kill_app
Abort
kill_app:
nsProcess::_KillProcess "${APP_EXECUTABLE_FILENAME}"
Pop $R0
Sleep 1000
Goto check_running
${EndIf}
!macroend

!macro customInit
!insertmacro stopRunningProduct \
"${PRODUCT_FILENAME} 正在运行,请先关闭后再继续安装。$\n$\n点击「确定」将在关闭后继续。"

; 删旧快捷方式:老 .lnk 里 IconLocation 仍指向上一版 exe 的资源索引,
; 新版 .ico 内多尺寸顺序/数量变化后那个索引会落到另一张图。
Expand All @@ -36,6 +45,11 @@
Delete "$APPDATA\Microsoft\Internet Explorer\Quick Launch\User Pinned\TaskBar\${SHORTCUT_NAME}.lnk"
!macroend

!macro customUnInit
!insertmacro stopRunningProduct \
"${PRODUCT_FILENAME} 正在运行,需要先关闭才能卸载。$\n$\n点击「确定」将关闭 ${PRODUCT_FILENAME} 并继续卸载。"
!macroend

!macro customInstall
; 注册文件夹右键菜单 "通过 <区域名> 打开" (与 main/folderContextMenu.ts 写的是同一组键)。
; 双重保险:installer 写一次让首装即可用, app 启动时的 registerFolderContextMenu()
Expand Down
29 changes: 29 additions & 0 deletions apps/desktop/src/main/__tests__/installerNsh.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
import * as fs from 'node:fs';
import * as path from 'node:path';
import { describe, expect, it } from 'vitest';

const installerScript = fs.readFileSync(
path.resolve(__dirname, '../../../resources/installer.nsh'),
'utf8',
);

function macroBody(name: string): string {
const match = installerScript.match(
new RegExp(`!macro ${name}(?: [^\\r\\n]*)?\\r?\\n([\\s\\S]*?)!macroend`),
);
expect(match, `missing NSIS macro: ${name}`).not.toBeNull();
return match?.[1] ?? '';
}

describe('Windows installer process shutdown contract', () => {
it('stops the packaged app before both install and uninstall mutate its files', () => {
const stopRunningProduct = macroBody('stopRunningProduct');

expect(stopRunningProduct).toContain('nsProcess::_FindProcess "${APP_EXECUTABLE_FILENAME}"');
expect(stopRunningProduct).toContain('nsProcess::_KillProcess "${APP_EXECUTABLE_FILENAME}"');
expect(stopRunningProduct).toContain('/SD IDOK');
expect(stopRunningProduct).toContain('Goto check_running');
expect(macroBody('customInit')).toContain('!insertmacro stopRunningProduct');
expect(macroBody('customUnInit')).toContain('!insertmacro stopRunningProduct');
});
});