From ea1847d5eecd1482e8032f1ed47930f35a8df583 Mon Sep 17 00:00:00 2001 From: Stella-xixi <2559689615@qq.com> Date: Wed, 2 Sep 2026 00:33:27 +0800 Subject: [PATCH] fix(desktop): stop Cindy before Windows uninstall Signed-off-by: Stella-xixi <2559689615@qq.com> --- apps/desktop/resources/installer.nsh | 20 +++++++++++-- .../src/main/__tests__/installerNsh.test.ts | 29 +++++++++++++++++++ 2 files changed, 46 insertions(+), 3 deletions(-) create mode 100644 apps/desktop/src/main/__tests__/installerNsh.test.ts diff --git a/apps/desktop/resources/installer.nsh b/apps/desktop/resources/installer.nsh index aa013f55b24..ccd677cdcd5 100644 --- a/apps/desktop/resources/installer.nsh +++ b/apps/desktop/resources/installer.nsh @@ -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 内多尺寸顺序/数量变化后那个索引会落到另一张图。 @@ -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() diff --git a/apps/desktop/src/main/__tests__/installerNsh.test.ts b/apps/desktop/src/main/__tests__/installerNsh.test.ts new file mode 100644 index 00000000000..810f5d27c4a --- /dev/null +++ b/apps/desktop/src/main/__tests__/installerNsh.test.ts @@ -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'); + }); +});