mirror of
https://gh-proxy.org/https://github.com/klinsmaya/workdsh.git
synced 2026-11-10 10:27:55 +08:00
fix(win): allow installer upgrades while desktop is running
This commit is contained in:
@@ -0,0 +1,162 @@
|
||||
# Windows 安装器升级与覆盖安装修复证据
|
||||
|
||||
## 结论
|
||||
|
||||
- 结果:通过。
|
||||
- 范围:仅修改 DSH Desktop 的 Electron/NSIS 安装与退出协调;未修改 `deepseek-harness` 子模块或 DSH 底层。
|
||||
- 真实安装链路:`2.0.2 -> 2.0.3` 运行中升级通过,`2.0.3 -> 2.0.3` 运行中覆盖安装通过。
|
||||
- 清理:测试安装目录、卸载注册表项、快捷方式、测试进程、临时目录和活动运行标记均无残留。
|
||||
|
||||
## 关联问题
|
||||
|
||||
- Issue: [#469 Windows 更新安装器误判应用仍在运行](https://github.com/anywhere-labs/dsh-desktop/issues/469)
|
||||
- 已有基础修复:[PR #471](https://github.com/anywhere-labs/dsh-desktop/pull/471),提交 `edc9574f447c90866e85cfd1d40f718be7737432`,将“安装目录下任意进程”改为精确匹配 `DSH Desktop.exe`。
|
||||
- 本次补充:安装器先通过 `--dsh-installer-quit` 请求运行中的桌面端有序退出,等待清理完成后再覆盖文件;旧版不支持该参数时,继续使用精确进程名的停止/强制停止回退。
|
||||
|
||||
## 测试基线
|
||||
|
||||
| 项目 | 值 |
|
||||
| --- | --- |
|
||||
| 日期 | 2026-08-25(Asia/Shanghai) |
|
||||
| 系统 | Microsoft Windows 11 专业版,10.0.26200,64-bit |
|
||||
| 分支 | `fix/windows-installer-upgrade-evidence` |
|
||||
| 基线提交 | `2172b1b2f2b0de4c2b3a1d8b55f11f8083a9305e` |
|
||||
| Node.js | `v24.15.0` |
|
||||
| Yarn | `4.18.0`(通过 Corepack) |
|
||||
| DSH 子模块 | `b150a551b8d465e31e418e1b2eaf5e79bbb7d28e`,未修改 |
|
||||
|
||||
## 安装器身份
|
||||
|
||||
| 安装器 | PE 版本 | 大小(字节) | SHA-256 |
|
||||
| --- | --- | ---: | --- |
|
||||
| `DSH-Desktop-2.0.2-x64-Setup.exe` | `2.0.2` | 132417238 | `B31F63F8CF70D3FC07ED2AE36E5DE7B1939E604BDB3BE097DE3383A82A06A787` |
|
||||
| `DSH-Desktop-2.0.3-x64-Setup.exe` | `2.0.3` | 132758556 | `AE2FD0820803A69B7C953B9831FC029EE06499E07078DC8B45497C17A858743A` |
|
||||
|
||||
候选产物:`dsh-plugin-desktop/dist/DSH-Desktop-2.0.3-x64-Setup.exe`。
|
||||
|
||||
## 问题复现
|
||||
|
||||
执行:
|
||||
|
||||
```powershell
|
||||
& .\dsh-plugin-desktop\scripts\reproduce-windows-installer-running-check.ps1
|
||||
```
|
||||
|
||||
脚本在临时“安装目录”中启动真实的 `DSH Helper.exe` 进程,并比较旧目录前缀判断与修复后的精确名称判断:
|
||||
|
||||
```json
|
||||
{
|
||||
"oldInstallDirectoryPredicateMatchedHelper": true,
|
||||
"fixedExecutableNamePredicateMatchedHelper": false,
|
||||
"reproduced": true
|
||||
}
|
||||
```
|
||||
|
||||
这证明旧逻辑会把安装目录内的无关辅助进程误判为桌面主程序,而精确匹配不会。
|
||||
|
||||
## 有序退出运行时探针
|
||||
|
||||
执行:
|
||||
|
||||
```powershell
|
||||
& .\dsh-plugin-desktop\scripts\probe-windows-installer-quit.ps1 `
|
||||
-CandidateApp .\dsh-plugin-desktop\dist\win-unpacked\DSH Desktop.exe
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
```json
|
||||
{
|
||||
"candidateVersion": "2.0.3.0",
|
||||
"primaryProcessId": 50372,
|
||||
"activeRunMarkerObserved": true,
|
||||
"quitRequestExitCode": 0,
|
||||
"primaryProcessStopped": true,
|
||||
"activeRunMarkerCleared": true,
|
||||
"testRootRemoved": true,
|
||||
"error": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
探针的主实例和退出请求实例使用相同的临时 Electron `userData`,验证单实例事件能够触发 Cordis/桌面运行时的有序退出,并在退出前清除 `active-run.json`。
|
||||
|
||||
## 真实升级与覆盖安装
|
||||
|
||||
执行:
|
||||
|
||||
```powershell
|
||||
& .\dsh-plugin-desktop\scripts\smoke-windows-installer-upgrade.ps1 `
|
||||
-BaseInstaller E:\qwq\DSHPLU\DSH-desktop\DSH-Desktop-2.0.2-x64-Setup.exe `
|
||||
-CandidateInstaller .\dsh-plugin-desktop\dist\DSH-Desktop-2.0.3-x64-Setup.exe
|
||||
```
|
||||
|
||||
安装位置和 `DSH_HOME` 使用 `%TEMP%\dsh-installer-upgrade-<GUID>`。为与生产安装器的单实例身份一致,Electron 使用当前 Windows 账户的默认 `userData`;脚本在开始前要求不存在活动运行标记,并在最终清理中再次验证标记不存在。
|
||||
|
||||
| 阶段 | 进程状态 | 退出码 | 耗时 | 文件版本 |
|
||||
| --- | --- | ---: | ---: | --- |
|
||||
| 安装基线 | 安装后启动 PID `20288` | 0 | 363170 ms | `2.0.2.0` |
|
||||
| 运行中升级 | 旧版进程已停止 | 0 | 464535 ms | `2.0.3.0` |
|
||||
| 启动候选 | 启动 PID `42716`,观察到活动标记 | - | - | `2.0.3.0` |
|
||||
| 运行中同版本覆盖 | 候选进程已停止,活动标记已清除 | 0 | 435635 ms | `2.0.3.0` |
|
||||
| 静默卸载 | 完成 | 0 | - | - |
|
||||
|
||||
最终 JSON 关键字段:
|
||||
|
||||
```json
|
||||
{
|
||||
"baseInstallExitCode": 0,
|
||||
"baseInstalledVersion": "2.0.2.0",
|
||||
"baseProcessStarted": true,
|
||||
"upgradeExitCode": 0,
|
||||
"upgradedVersion": "2.0.3.0",
|
||||
"baseProcessStopped": true,
|
||||
"candidateProcessStarted": true,
|
||||
"activeRunMarkerObserved": true,
|
||||
"overwriteExitCode": 0,
|
||||
"overwriteVersion": "2.0.3.0",
|
||||
"candidateProcessStopped": true,
|
||||
"activeRunMarkerCleared": true,
|
||||
"uninstallExitCode": 0,
|
||||
"installRootRemoved": true,
|
||||
"uninstallEntryRemoved": true,
|
||||
"shortcutsRemoved": true,
|
||||
"testProcessesRemaining": 0,
|
||||
"activeRunMarkerAbsentAfterCleanup": true,
|
||||
"error": null,
|
||||
"success": true
|
||||
}
|
||||
```
|
||||
|
||||
`2.0.2` 不识别新退出参数,因此首次升级按设计走精确主进程名回退;`2.0.3` 同版本覆盖观察到标记从存在变为清除,证明新版本走了有序退出路径。
|
||||
|
||||
## 自动化回归
|
||||
|
||||
执行:
|
||||
|
||||
```powershell
|
||||
corepack yarn check:win-package
|
||||
```
|
||||
|
||||
结果:
|
||||
|
||||
- 构建通过。
|
||||
- TypeScript 类型检查通过。
|
||||
- Windows 打包门禁:13 个测试文件、180 个测试全部通过。
|
||||
- 运行时闭包单测:4/4 通过。
|
||||
- 运行时闭包:201 个一方节点形成闭合、可达的运行图。
|
||||
- `git diff --check` 通过。
|
||||
|
||||
## 最终残留复核
|
||||
|
||||
```json
|
||||
{
|
||||
"InstallEntries": 0,
|
||||
"Processes": 0,
|
||||
"TestTempRoots": 0,
|
||||
"ActiveRunMarkerPresent": false,
|
||||
"Shortcuts": 0
|
||||
}
|
||||
```
|
||||
|
||||
候选安装器未做代码签名验证;本报告用 PE 版本、文件大小和 SHA-256 固定被测二进制身份。
|
||||
@@ -0,0 +1,61 @@
|
||||
; Check the exact app process before launching the quit handoff. This preserves
|
||||
; the #469 fix: unrelated helpers under $INSTDIR must never block an upgrade.
|
||||
Var pid
|
||||
|
||||
!macro customCheckAppRunning
|
||||
!insertmacro IS_POWERSHELL_AVAILABLE
|
||||
!insertmacro FIND_PROCESS "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
${if} $R0 != 0
|
||||
Goto dsh_installer_app_stopped
|
||||
${endIf}
|
||||
|
||||
IfFileExists "$INSTDIR\${APP_EXECUTABLE_FILENAME}" 0 dsh_installer_scoped_fallback
|
||||
; Newer versions receive this through Electron's single-instance channel.
|
||||
; 2.0.2 ignores it, so the scoped builder fallback remains necessary for
|
||||
; the first upgrade to a version that supports orderly shutdown.
|
||||
ExecWait '"$INSTDIR\${APP_EXECUTABLE_FILENAME}" --dsh-installer-quit'
|
||||
StrCpy $R1 0
|
||||
|
||||
dsh_installer_wait_for_exit:
|
||||
!insertmacro FIND_PROCESS "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
${if} $R0 != 0
|
||||
Goto dsh_installer_app_stopped
|
||||
${endIf}
|
||||
IntOp $R1 $R1 + 1
|
||||
${if} $R1 < 12
|
||||
Sleep 500
|
||||
Goto dsh_installer_wait_for_exit
|
||||
${endIf}
|
||||
|
||||
dsh_installer_scoped_fallback:
|
||||
; The patched builder macros match DSH Desktop.exe, not every executable
|
||||
; below $INSTDIR. They handle pre-handoff releases and stubborn processes.
|
||||
MessageBox MB_OKCANCEL|MB_ICONEXCLAMATION "$(appRunning)" /SD IDOK IDOK dsh_installer_stop_app
|
||||
Quit
|
||||
|
||||
dsh_installer_stop_app:
|
||||
DetailPrint "$(appClosing)"
|
||||
; KILL_PROCESS's tasklist fallback excludes $pid. The installer never has
|
||||
; the application executable name, so zero is a safe sentinel here.
|
||||
StrCpy $pid 0
|
||||
!insertmacro KILL_PROCESS "${APP_EXECUTABLE_FILENAME}" 0
|
||||
Sleep 500
|
||||
StrCpy $R1 0
|
||||
|
||||
dsh_installer_wait_for_fallback:
|
||||
!insertmacro FIND_PROCESS "${APP_EXECUTABLE_FILENAME}" $R0
|
||||
${if} $R0 != 0
|
||||
Goto dsh_installer_app_stopped
|
||||
${endIf}
|
||||
IntOp $R1 $R1 + 1
|
||||
${if} $R1 > 1
|
||||
MessageBox MB_RETRYCANCEL|MB_ICONEXCLAMATION "$(appCannotBeClosed)" /SD IDCANCEL IDRETRY dsh_installer_wait_for_fallback
|
||||
Quit
|
||||
${endIf}
|
||||
Sleep 1000
|
||||
!insertmacro KILL_PROCESS "${APP_EXECUTABLE_FILENAME}" 1
|
||||
Sleep 500
|
||||
Goto dsh_installer_wait_for_fallback
|
||||
|
||||
dsh_installer_app_stopped:
|
||||
!macroend
|
||||
@@ -125,7 +125,7 @@
|
||||
"verify:operations": "node --test scripts/operation-reliability.spec.mjs && node scripts/verify-operation-reliability.mjs",
|
||||
"verify:notices": "node scripts/verify-licenses.mjs --notices THIRD_PARTY_NOTICES.md",
|
||||
"check": "yarn run build && yarn run typecheck && yarn run test && yarn run verify:closure && yarn run verify:cli && yarn run verify:loader && yarn run verify:profile && yarn run verify:licenses && yarn run verify:operations",
|
||||
"check:win-package": "yarn workspace dsh-community-market build && yarn run build && yarn run typecheck && vitest run tests/package.spec.ts tests/package-win.spec.ts tests/update-checker.spec.ts tests/update-download.spec.ts tests/verify-win-installer.spec.ts tests/verify-win-portable.spec.ts tests/verify-packaged-runtime.spec.ts tests/windows-agent-presets.spec.ts tests/windows-pwsh-sandbox.spec.ts tests/windows-volume-diagnostics.spec.ts tests/window-options.spec.ts && yarn run verify:closure",
|
||||
"check:win-package": "yarn workspace dsh-community-market build && yarn run build && yarn run typecheck && vitest run tests/package.spec.ts tests/package-win.spec.ts tests/desktop-installer-quit.spec.ts tests/installer-nsh.spec.ts tests/update-checker.spec.ts tests/update-download.spec.ts tests/verify-win-installer.spec.ts tests/verify-win-portable.spec.ts tests/verify-packaged-runtime.spec.ts tests/windows-agent-presets.spec.ts tests/windows-pwsh-sandbox.spec.ts tests/windows-volume-diagnostics.spec.ts tests/window-options.spec.ts && yarn run verify:closure",
|
||||
"check:mac-package": "yarn workspace dsh-community-market build && yarn run build && yarn run typecheck && vitest run tests/package.spec.ts tests/package-mac.spec.ts tests/verify-mac-smoke.spec.ts tests/verify-packaged-runtime.spec.ts tests/mac-universal.spec.ts && yarn run verify:closure",
|
||||
"start": "node lib/bin.js",
|
||||
"dev": "yarn run build && node lib/bin.js",
|
||||
@@ -351,6 +351,7 @@
|
||||
"icon": "build/app-icon.png"
|
||||
},
|
||||
"nsis": {
|
||||
"include": "installer.nsh",
|
||||
"license": "THIRD_PARTY_NOTICES.md",
|
||||
"oneClick": false,
|
||||
"perMachine": false,
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CandidateApp
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($env:OS -ne 'Windows_NT') {
|
||||
throw 'This probe requires Windows.'
|
||||
}
|
||||
|
||||
$taskAppPath = (Resolve-Path -LiteralPath $CandidateApp).Path
|
||||
$taskTempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
||||
$taskRoot = Join-Path $taskTempRoot ("dsh-installer-quit-probe-" + [guid]::NewGuid().ToString('N'))
|
||||
$taskUserData = Join-Path $taskRoot 'user-data'
|
||||
$taskDshHome = Join-Path $taskRoot 'dsh-home'
|
||||
$taskActiveRunMarker = Join-Path $taskUserData 'crash-evidence\active-run.json'
|
||||
$taskPrimary = $null
|
||||
$taskResult = [ordered]@{
|
||||
scenario = 'isolated packaged app receives installer quit handoff'
|
||||
testRoot = $taskRoot
|
||||
candidateApp = $taskAppPath
|
||||
candidateVersion = (Get-Item -LiteralPath $taskAppPath).VersionInfo.ProductVersion
|
||||
primaryProcessId = $null
|
||||
activeRunMarkerObserved = $false
|
||||
quitRequestExitCode = $null
|
||||
primaryProcessStopped = $false
|
||||
activeRunMarkerCleared = $false
|
||||
testRootRemoved = $false
|
||||
error = $null
|
||||
success = $false
|
||||
}
|
||||
|
||||
function Wait-TaskCondition([scriptblock]$taskCondition, [string]$taskDescription, [int]$taskTimeoutSeconds = 30) {
|
||||
$taskDeadline = [DateTime]::UtcNow.AddSeconds($taskTimeoutSeconds)
|
||||
do {
|
||||
if (& $taskCondition) { return }
|
||||
Start-Sleep -Milliseconds 250
|
||||
} while ([DateTime]::UtcNow -lt $taskDeadline)
|
||||
throw "Timed out waiting for $taskDescription."
|
||||
}
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $taskRoot | Out-Null
|
||||
$taskStartInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$taskStartInfo.FileName = $taskAppPath
|
||||
$taskStartInfo.Arguments = "--user-data-dir=`"$taskUserData`""
|
||||
$taskStartInfo.WorkingDirectory = Split-Path -Parent $taskAppPath
|
||||
$taskStartInfo.UseShellExecute = $false
|
||||
$taskStartInfo.CreateNoWindow = $true
|
||||
$taskStartInfo.EnvironmentVariables['DSH_HOME'] = $taskDshHome
|
||||
$taskPrimary = [System.Diagnostics.Process]::Start($taskStartInfo)
|
||||
$taskResult.primaryProcessId = $taskPrimary.Id
|
||||
|
||||
Wait-TaskCondition { Test-Path -LiteralPath $taskActiveRunMarker } 'active run marker creation'
|
||||
$taskResult.activeRunMarkerObserved = $true
|
||||
|
||||
$taskQuitStartInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$taskQuitStartInfo.FileName = $taskAppPath
|
||||
$taskQuitStartInfo.Arguments = "--user-data-dir=`"$taskUserData`" --dsh-installer-quit"
|
||||
$taskQuitStartInfo.WorkingDirectory = Split-Path -Parent $taskAppPath
|
||||
$taskQuitStartInfo.UseShellExecute = $false
|
||||
$taskQuitStartInfo.CreateNoWindow = $true
|
||||
$taskQuitStartInfo.EnvironmentVariables['DSH_HOME'] = $taskDshHome
|
||||
$taskQuitRequest = [System.Diagnostics.Process]::Start($taskQuitStartInfo)
|
||||
$taskQuitRequest.WaitForExit()
|
||||
$taskResult.quitRequestExitCode = $taskQuitRequest.ExitCode
|
||||
|
||||
Wait-TaskCondition {
|
||||
$taskPrimary.Refresh()
|
||||
$taskPrimary.HasExited
|
||||
} 'primary app exit' 15
|
||||
$taskResult.primaryProcessStopped = $true
|
||||
|
||||
Wait-TaskCondition { -not (Test-Path -LiteralPath $taskActiveRunMarker) } 'active run marker cleanup' 15
|
||||
$taskResult.activeRunMarkerCleared = $true
|
||||
} catch {
|
||||
$taskResult.error = $_.Exception.Message
|
||||
} finally {
|
||||
if ($null -ne $taskPrimary) {
|
||||
$taskPrimary.Refresh()
|
||||
if (-not $taskPrimary.HasExited) {
|
||||
Stop-Process -Id $taskPrimary.Id -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
}
|
||||
|
||||
$taskResolvedRoot = [System.IO.Path]::GetFullPath($taskRoot)
|
||||
if (-not $taskResolvedRoot.StartsWith($taskTempRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
-not ([System.IO.Path]::GetFileName($taskResolvedRoot)).StartsWith('dsh-installer-quit-probe-', [System.StringComparison]::Ordinal)) {
|
||||
throw "Refusing to remove unexpected probe path: $taskResolvedRoot"
|
||||
}
|
||||
if (Test-Path -LiteralPath $taskResolvedRoot) {
|
||||
Remove-Item -LiteralPath $taskResolvedRoot -Recurse -Force
|
||||
}
|
||||
$taskResult.testRootRemoved = -not (Test-Path -LiteralPath $taskResolvedRoot)
|
||||
}
|
||||
|
||||
$taskResult.success = (
|
||||
$null -eq $taskResult.error -and
|
||||
$taskResult.activeRunMarkerObserved -and
|
||||
$taskResult.quitRequestExitCode -eq 0 -and
|
||||
$taskResult.primaryProcessStopped -and
|
||||
$taskResult.activeRunMarkerCleared -and
|
||||
$taskResult.testRootRemoved
|
||||
)
|
||||
|
||||
$taskResult | ConvertTo-Json
|
||||
if (-not $taskResult.success) {
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
param()
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($env:OS -ne 'Windows_NT') {
|
||||
throw 'This reproduction requires Windows.'
|
||||
}
|
||||
|
||||
$taskRoot = Join-Path ([System.IO.Path]::GetTempPath()) ("dsh-issue-469-" + [guid]::NewGuid().ToString('N'))
|
||||
$taskHelperPath = Join-Path $taskRoot 'DSH Helper.exe'
|
||||
$taskHelper = $null
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $taskRoot | Out-Null
|
||||
Copy-Item -LiteralPath (Join-Path $env:WINDIR 'System32\ping.exe') -Destination $taskHelperPath
|
||||
$taskHelper = Start-Process -FilePath $taskHelperPath -ArgumentList '-t', '127.0.0.1' -PassThru -WindowStyle Hidden
|
||||
Start-Sleep -Milliseconds 500
|
||||
|
||||
$taskProcesses = @(Get-CimInstance -ClassName Win32_Process)
|
||||
$taskOldMatches = @($taskProcesses | Where-Object {
|
||||
$_.Path -and $_.Path.StartsWith($taskRoot, [System.StringComparison]::CurrentCultureIgnoreCase)
|
||||
})
|
||||
$taskFixedMatches = @($taskProcesses | Where-Object {
|
||||
$_.Path -and [System.IO.Path]::GetFileName($_.Path) -ieq 'DSH Desktop.exe'
|
||||
})
|
||||
|
||||
[ordered]@{
|
||||
scenario = 'unrelated helper executable under the DSH install directory'
|
||||
helperPid = $taskHelper.Id
|
||||
helperPath = $taskHelperPath
|
||||
oldInstallDirectoryPredicateMatchedHelper = $taskOldMatches.ProcessId -contains $taskHelper.Id
|
||||
fixedExecutableNamePredicateMatchedHelper = $taskFixedMatches.ProcessId -contains $taskHelper.Id
|
||||
reproduced = ($taskOldMatches.ProcessId -contains $taskHelper.Id) -and -not ($taskFixedMatches.ProcessId -contains $taskHelper.Id)
|
||||
} | ConvertTo-Json
|
||||
} finally {
|
||||
if ($null -ne $taskHelper -and -not $taskHelper.HasExited) {
|
||||
Stop-Process -Id $taskHelper.Id -Force
|
||||
$taskHelper.WaitForExit()
|
||||
}
|
||||
if (Test-Path -LiteralPath $taskRoot) {
|
||||
$taskResolvedRoot = [System.IO.Path]::GetFullPath($taskRoot)
|
||||
$taskResolvedTemp = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
||||
if (-not $taskResolvedRoot.StartsWith($taskResolvedTemp, [System.StringComparison]::OrdinalIgnoreCase)) {
|
||||
throw "Refusing to remove unexpected path: $taskResolvedRoot"
|
||||
}
|
||||
Remove-Item -LiteralPath $taskResolvedRoot -Recurse -Force
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,290 @@
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$BaseInstaller,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$CandidateInstaller
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
if ($env:OS -ne 'Windows_NT') {
|
||||
throw 'This smoke test requires Windows.'
|
||||
}
|
||||
|
||||
$taskBaseInstaller = (Resolve-Path -LiteralPath $BaseInstaller).Path
|
||||
$taskCandidateInstaller = (Resolve-Path -LiteralPath $CandidateInstaller).Path
|
||||
$taskBaseExpectedVersion = (Get-Item -LiteralPath $taskBaseInstaller).VersionInfo.ProductVersion
|
||||
$taskCandidateExpectedVersion = (Get-Item -LiteralPath $taskCandidateInstaller).VersionInfo.ProductVersion
|
||||
$taskExistingProcesses = @(Get-CimInstance Win32_Process | Where-Object {
|
||||
$_.Name -ieq 'DSH Desktop.exe'
|
||||
})
|
||||
$taskUninstallRoots = @(
|
||||
'HKCU:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
||||
'HKLM:\Software\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
||||
'HKLM:\Software\WOW6432Node\Microsoft\Windows\CurrentVersion\Uninstall\*'
|
||||
)
|
||||
$taskExistingInstalls = @(Get-ItemProperty $taskUninstallRoots -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.DisplayName -match '^DSH Desktop'
|
||||
})
|
||||
|
||||
if ($taskExistingProcesses.Count -gt 0 -or $taskExistingInstalls.Count -gt 0) {
|
||||
throw 'Refusing to run while an existing DSH Desktop process or installation is present.'
|
||||
}
|
||||
|
||||
$taskTempRoot = [System.IO.Path]::GetFullPath([System.IO.Path]::GetTempPath())
|
||||
$taskRoot = Join-Path $taskTempRoot ("dsh-installer-upgrade-" + [guid]::NewGuid().ToString('N'))
|
||||
$taskInstallRoot = Join-Path $taskRoot 'app'
|
||||
$taskUserData = Join-Path $env:APPDATA 'DSH Desktop'
|
||||
$taskDshHome = Join-Path $taskRoot 'dsh-home'
|
||||
$taskActiveRunMarker = Join-Path $taskUserData 'crash-evidence\active-run.json'
|
||||
$taskAppPath = Join-Path $taskInstallRoot 'DSH Desktop.exe'
|
||||
$taskUninstallerPath = Join-Path $taskInstallRoot 'Uninstall DSH Desktop.exe'
|
||||
if (Test-Path -LiteralPath $taskActiveRunMarker) {
|
||||
throw 'Refusing to overwrite an existing DSH Desktop active run marker.'
|
||||
}
|
||||
$taskResult = [ordered]@{
|
||||
scenario = 'temporary install and DSH_HOME: 2.0.2 to 2.0.3 upgrade and fixed-version overwrite'
|
||||
testRoot = $taskRoot
|
||||
baseInstaller = $taskBaseInstaller
|
||||
baseInstallerSha256 = (Get-FileHash -LiteralPath $taskBaseInstaller -Algorithm SHA256).Hash
|
||||
baseExpectedVersion = $taskBaseExpectedVersion
|
||||
candidateInstaller = $taskCandidateInstaller
|
||||
candidateInstallerSha256 = (Get-FileHash -LiteralPath $taskCandidateInstaller -Algorithm SHA256).Hash
|
||||
candidateExpectedVersion = $taskCandidateExpectedVersion
|
||||
desktopUserData = $taskUserData
|
||||
baseInstallExitCode = $null
|
||||
baseInstallElapsedMs = $null
|
||||
baseInstalledVersion = $null
|
||||
baseProcessId = $null
|
||||
baseProcessStarted = $false
|
||||
upgradeExitCode = $null
|
||||
upgradeElapsedMs = $null
|
||||
upgradedVersion = $null
|
||||
baseProcessStopped = $false
|
||||
candidateProcessId = $null
|
||||
candidateProcessStarted = $false
|
||||
activeRunMarkerObserved = $false
|
||||
overwriteExitCode = $null
|
||||
overwriteElapsedMs = $null
|
||||
overwriteVersion = $null
|
||||
activeRunMarkerCleared = $false
|
||||
candidateProcessStopped = $false
|
||||
uninstallExitCode = $null
|
||||
installRootRemoved = $false
|
||||
uninstallEntryRemoved = $false
|
||||
shortcutsRemoved = $false
|
||||
testProcessesRemaining = $null
|
||||
activeRunMarkerAbsentAfterCleanup = $false
|
||||
error = $null
|
||||
success = $false
|
||||
}
|
||||
|
||||
function Start-TaskInstaller([string]$taskInstallerPath) {
|
||||
$taskStopwatch = [System.Diagnostics.Stopwatch]::StartNew()
|
||||
$taskProcess = Start-Process -FilePath $taskInstallerPath -ArgumentList @(
|
||||
'/S'
|
||||
'/currentuser'
|
||||
"/D=$taskInstallRoot"
|
||||
) -PassThru -Wait -WindowStyle Hidden
|
||||
$taskStopwatch.Stop()
|
||||
return [ordered]@{
|
||||
exitCode = $taskProcess.ExitCode
|
||||
elapsedMs = $taskStopwatch.ElapsedMilliseconds
|
||||
}
|
||||
}
|
||||
|
||||
function Start-TaskDesktop {
|
||||
$taskStartInfo = [System.Diagnostics.ProcessStartInfo]::new()
|
||||
$taskStartInfo.FileName = $taskAppPath
|
||||
$taskStartInfo.WorkingDirectory = $taskInstallRoot
|
||||
$taskStartInfo.UseShellExecute = $false
|
||||
$taskStartInfo.EnvironmentVariables['DSH_HOME'] = $taskDshHome
|
||||
$taskStartInfo.EnvironmentVariables['ELECTRON_ENABLE_LOGGING'] = '1'
|
||||
return [System.Diagnostics.Process]::Start($taskStartInfo)
|
||||
}
|
||||
|
||||
function Wait-TaskProcess([bool]$taskShouldExist, [int]$taskTimeoutSeconds = 30) {
|
||||
$taskDeadline = [DateTime]::UtcNow.AddSeconds($taskTimeoutSeconds)
|
||||
do {
|
||||
$taskMatches = @(Get-CimInstance Win32_Process | Where-Object {
|
||||
$_.ExecutablePath -and $_.ExecutablePath.Equals($taskAppPath, [System.StringComparison]::OrdinalIgnoreCase)
|
||||
})
|
||||
if (($taskMatches.Count -gt 0) -eq $taskShouldExist) {
|
||||
return $taskMatches
|
||||
}
|
||||
Start-Sleep -Milliseconds 250
|
||||
} while ([DateTime]::UtcNow -lt $taskDeadline)
|
||||
throw "Timed out waiting for DSH Desktop process state: shouldExist=$taskShouldExist"
|
||||
}
|
||||
|
||||
function Wait-TaskActiveRunMarker([bool]$taskShouldExist, [int]$taskTimeoutSeconds = 30) {
|
||||
$taskDeadline = [DateTime]::UtcNow.AddSeconds($taskTimeoutSeconds)
|
||||
do {
|
||||
$taskMarkerExists = Test-Path -LiteralPath $taskActiveRunMarker
|
||||
if ($taskMarkerExists -eq $taskShouldExist) {
|
||||
return $taskActiveRunMarker
|
||||
}
|
||||
Start-Sleep -Milliseconds 250
|
||||
} while ([DateTime]::UtcNow -lt $taskDeadline)
|
||||
throw "Timed out waiting for active-run marker state: shouldExist=$taskShouldExist"
|
||||
}
|
||||
|
||||
function ConvertTo-TaskVersion([string]$taskVersion) {
|
||||
$taskParsedVersion = [version]$taskVersion
|
||||
return [version]::new(
|
||||
$taskParsedVersion.Major,
|
||||
$taskParsedVersion.Minor,
|
||||
[Math]::Max(0, $taskParsedVersion.Build),
|
||||
[Math]::Max(0, $taskParsedVersion.Revision)
|
||||
)
|
||||
}
|
||||
|
||||
function Test-TaskVersionEquals([string]$taskActualVersion, [string]$taskExpectedVersion) {
|
||||
return (ConvertTo-TaskVersion $taskActualVersion) -eq (ConvertTo-TaskVersion $taskExpectedVersion)
|
||||
}
|
||||
|
||||
try {
|
||||
New-Item -ItemType Directory -Path $taskRoot | Out-Null
|
||||
|
||||
$taskBaseInstall = Start-TaskInstaller $taskBaseInstaller
|
||||
$taskResult.baseInstallExitCode = $taskBaseInstall.exitCode
|
||||
$taskResult.baseInstallElapsedMs = $taskBaseInstall.elapsedMs
|
||||
if ($taskBaseInstall.exitCode -ne 0 -or -not (Test-Path -LiteralPath $taskAppPath)) {
|
||||
throw "Base installer failed with exit code $($taskBaseInstall.exitCode)."
|
||||
}
|
||||
$taskResult.baseInstalledVersion = (Get-Item -LiteralPath $taskAppPath).VersionInfo.ProductVersion
|
||||
if (-not (Test-TaskVersionEquals $taskResult.baseInstalledVersion $taskBaseExpectedVersion)) {
|
||||
throw "Expected base version $taskBaseExpectedVersion but installed $($taskResult.baseInstalledVersion)."
|
||||
}
|
||||
|
||||
$taskBaseProcess = Start-TaskDesktop
|
||||
Wait-TaskProcess $true | Out-Null
|
||||
$taskResult.baseProcessId = $taskBaseProcess.Id
|
||||
$taskResult.baseProcessStarted = -not $taskBaseProcess.HasExited
|
||||
|
||||
$taskUpgrade = Start-TaskInstaller $taskCandidateInstaller
|
||||
$taskResult.upgradeExitCode = $taskUpgrade.exitCode
|
||||
$taskResult.upgradeElapsedMs = $taskUpgrade.elapsedMs
|
||||
if ($taskUpgrade.exitCode -ne 0) {
|
||||
throw "Candidate upgrade failed with exit code $($taskUpgrade.exitCode)."
|
||||
}
|
||||
Wait-TaskProcess $false | Out-Null
|
||||
$taskResult.baseProcessStopped = $true
|
||||
$taskResult.upgradedVersion = (Get-Item -LiteralPath $taskAppPath).VersionInfo.ProductVersion
|
||||
if (-not (Test-TaskVersionEquals $taskResult.upgradedVersion $taskCandidateExpectedVersion)) {
|
||||
throw "Expected upgraded version $taskCandidateExpectedVersion but installed $($taskResult.upgradedVersion)."
|
||||
}
|
||||
|
||||
$taskCandidateProcess = Start-TaskDesktop
|
||||
Wait-TaskProcess $true | Out-Null
|
||||
$taskResult.candidateProcessId = $taskCandidateProcess.Id
|
||||
$taskResult.candidateProcessStarted = -not $taskCandidateProcess.HasExited
|
||||
Wait-TaskActiveRunMarker $true | Out-Null
|
||||
$taskResult.activeRunMarkerObserved = $true
|
||||
|
||||
$taskOverwrite = Start-TaskInstaller $taskCandidateInstaller
|
||||
$taskResult.overwriteExitCode = $taskOverwrite.exitCode
|
||||
$taskResult.overwriteElapsedMs = $taskOverwrite.elapsedMs
|
||||
if ($taskOverwrite.exitCode -ne 0) {
|
||||
throw "Candidate overwrite failed with exit code $($taskOverwrite.exitCode)."
|
||||
}
|
||||
Wait-TaskProcess $false | Out-Null
|
||||
$taskResult.candidateProcessStopped = $true
|
||||
Wait-TaskActiveRunMarker $false | Out-Null
|
||||
$taskResult.activeRunMarkerCleared = $true
|
||||
$taskResult.overwriteVersion = (Get-Item -LiteralPath $taskAppPath).VersionInfo.ProductVersion
|
||||
if (-not (Test-TaskVersionEquals $taskResult.overwriteVersion $taskCandidateExpectedVersion)) {
|
||||
throw "Expected overwrite version $taskCandidateExpectedVersion but installed $($taskResult.overwriteVersion)."
|
||||
}
|
||||
} catch {
|
||||
$taskResult.error = $_.Exception.Message
|
||||
} finally {
|
||||
$taskTestProcesses = @(Get-CimInstance Win32_Process | Where-Object {
|
||||
$_.ExecutablePath -and $_.ExecutablePath.StartsWith(
|
||||
$taskInstallRoot,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)
|
||||
})
|
||||
foreach ($taskProcess in $taskTestProcesses) {
|
||||
Stop-Process -Id $taskProcess.ProcessId -Force -ErrorAction SilentlyContinue
|
||||
}
|
||||
|
||||
if (Test-Path -LiteralPath $taskUninstallerPath) {
|
||||
$taskUninstaller = Start-Process -FilePath $taskUninstallerPath -ArgumentList '/S' -PassThru -Wait -WindowStyle Hidden
|
||||
$taskResult.uninstallExitCode = $taskUninstaller.ExitCode
|
||||
}
|
||||
|
||||
Start-Sleep -Milliseconds 500
|
||||
$taskResolvedRoot = [System.IO.Path]::GetFullPath($taskRoot)
|
||||
if (-not $taskResolvedRoot.StartsWith($taskTempRoot, [System.StringComparison]::OrdinalIgnoreCase) -or
|
||||
-not ([System.IO.Path]::GetFileName($taskResolvedRoot)).StartsWith('dsh-installer-upgrade-', [System.StringComparison]::Ordinal)) {
|
||||
throw "Refusing to remove unexpected test path: $taskResolvedRoot"
|
||||
}
|
||||
if ($taskResult.uninstallExitCode -eq 0 -and (Test-Path -LiteralPath $taskResolvedRoot)) {
|
||||
Remove-Item -LiteralPath $taskResolvedRoot -Recurse -Force
|
||||
}
|
||||
$taskResult.installRootRemoved = -not (Test-Path -LiteralPath $taskResolvedRoot)
|
||||
|
||||
$taskRemainingProcesses = @(Get-CimInstance Win32_Process | Where-Object {
|
||||
$_.ExecutablePath -and $_.ExecutablePath.StartsWith(
|
||||
$taskInstallRoot,
|
||||
[System.StringComparison]::OrdinalIgnoreCase
|
||||
)
|
||||
})
|
||||
$taskResult.testProcessesRemaining = $taskRemainingProcesses.Count
|
||||
|
||||
$taskRemainingInstalls = @(Get-ItemProperty $taskUninstallRoots -ErrorAction SilentlyContinue | Where-Object {
|
||||
$_.DisplayName -match '^DSH Desktop'
|
||||
})
|
||||
$taskResult.uninstallEntryRemoved = $taskRemainingInstalls.Count -eq 0
|
||||
|
||||
$taskShortcutRoots = @(
|
||||
[Environment]::GetFolderPath('Desktop')
|
||||
[Environment]::GetFolderPath('StartMenu')
|
||||
[Environment]::GetFolderPath('CommonDesktopDirectory')
|
||||
[Environment]::GetFolderPath('CommonStartMenu')
|
||||
) | Where-Object { $_ -and (Test-Path -LiteralPath $_) }
|
||||
$taskRemainingShortcuts = @($taskShortcutRoots | ForEach-Object {
|
||||
Get-ChildItem -LiteralPath $_ -Filter '*DSH Desktop*' -Recurse -ErrorAction SilentlyContinue
|
||||
})
|
||||
$taskResult.shortcutsRemoved = $taskRemainingShortcuts.Count -eq 0
|
||||
|
||||
if (Test-Path -LiteralPath $taskActiveRunMarker) {
|
||||
try {
|
||||
$taskMarkerRecord = Get-Content -LiteralPath $taskActiveRunMarker -Raw | ConvertFrom-Json
|
||||
if ($taskMarkerRecord.pid -eq $taskResult.baseProcessId -or
|
||||
$taskMarkerRecord.pid -eq $taskResult.candidateProcessId) {
|
||||
Remove-Item -LiteralPath $taskActiveRunMarker -Force
|
||||
}
|
||||
} catch {}
|
||||
}
|
||||
$taskResult.activeRunMarkerAbsentAfterCleanup = -not (Test-Path -LiteralPath $taskActiveRunMarker)
|
||||
}
|
||||
|
||||
$taskResult.success = (
|
||||
$null -eq $taskResult.error -and
|
||||
$taskResult.baseInstallExitCode -eq 0 -and
|
||||
(Test-TaskVersionEquals $taskResult.baseInstalledVersion $taskBaseExpectedVersion) -and
|
||||
$taskResult.baseProcessStarted -and
|
||||
$taskResult.upgradeExitCode -eq 0 -and
|
||||
(Test-TaskVersionEquals $taskResult.upgradedVersion $taskCandidateExpectedVersion) -and
|
||||
$taskResult.baseProcessStopped -and
|
||||
$taskResult.candidateProcessStarted -and
|
||||
$taskResult.activeRunMarkerObserved -and
|
||||
$taskResult.overwriteExitCode -eq 0 -and
|
||||
(Test-TaskVersionEquals $taskResult.overwriteVersion $taskCandidateExpectedVersion) -and
|
||||
$taskResult.candidateProcessStopped -and
|
||||
$taskResult.activeRunMarkerCleared -and
|
||||
$taskResult.uninstallExitCode -eq 0 -and
|
||||
$taskResult.installRootRemoved -and
|
||||
$taskResult.uninstallEntryRemoved -and
|
||||
$taskResult.shortcutsRemoved -and
|
||||
$taskResult.testProcessesRemaining -eq 0 -and
|
||||
$taskResult.activeRunMarkerAbsentAfterCleanup
|
||||
)
|
||||
|
||||
$taskResult | ConvertTo-Json
|
||||
if (-not $taskResult.success) {
|
||||
exit 1
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
/** Windows installer flag that requests an orderly Desktop shutdown. */
|
||||
export const DESKTOP_INSTALLER_QUIT_FLAG = '--dsh-installer-quit'
|
||||
|
||||
/** Return whether one process invocation belongs to the installer quit handoff. */
|
||||
export function isDesktopInstallerQuitRequest(
|
||||
argv: readonly string[],
|
||||
platform: NodeJS.Platform,
|
||||
): boolean {
|
||||
return platform === 'win32' && argv.includes(DESKTOP_INSTALLER_QUIT_FLAG)
|
||||
}
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { provideCmdline } from '@deepseek-ai/dsh-cmdline'
|
||||
import { resolveDshHome } from '@deepseek-ai/dsh-home-paths'
|
||||
import { DSH_LAUNCH_ENVIRONMENT_KEY } from '@deepseek-ai/dsh-launch-environment'
|
||||
import { isDesktopInstallerQuitRequest } from './desktop-installer-quit.ts'
|
||||
import {
|
||||
installDesktopDshRuntime,
|
||||
installDesktopPnpmRuntime,
|
||||
@@ -184,6 +185,10 @@ async function start(): Promise<void> {
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
if (isDesktopInstallerQuitRequest(process.argv, process.platform)) {
|
||||
app.quit()
|
||||
return
|
||||
}
|
||||
|
||||
let shutdown: DesktopShutdown | undefined
|
||||
let removeShutdownRequests: (() => void) | undefined
|
||||
@@ -347,7 +352,11 @@ async function start(): Promise<void> {
|
||||
}
|
||||
}
|
||||
|
||||
app.on('second-instance', () => {
|
||||
app.on('second-instance', (_event, argv) => {
|
||||
if (isDesktopInstallerQuitRequest(argv, process.platform)) {
|
||||
requestQuit(0)
|
||||
return
|
||||
}
|
||||
if (startupRecoveryWindow !== undefined) startupRecoveryWindow.show()
|
||||
else runtime.show()
|
||||
})
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import {
|
||||
DESKTOP_INSTALLER_QUIT_FLAG,
|
||||
isDesktopInstallerQuitRequest,
|
||||
} from '../src/desktop-installer-quit.ts'
|
||||
|
||||
describe('Desktop installer quit request', () => {
|
||||
it('accepts only the dedicated flag on Windows', () => {
|
||||
expect(DESKTOP_INSTALLER_QUIT_FLAG).toBe('--dsh-installer-quit')
|
||||
expect(isDesktopInstallerQuitRequest(
|
||||
['DSH Desktop.exe', DESKTOP_INSTALLER_QUIT_FLAG],
|
||||
'win32',
|
||||
)).toBe(true)
|
||||
expect(isDesktopInstallerQuitRequest(['DSH Desktop.exe', '--quit'], 'win32')).toBe(false)
|
||||
expect(isDesktopInstallerQuitRequest(
|
||||
['DSH Desktop', DESKTOP_INSTALLER_QUIT_FLAG],
|
||||
'darwin',
|
||||
)).toBe(false)
|
||||
})
|
||||
|
||||
it('handles first- and second-instance requests without showing a window', () => {
|
||||
const main = readFileSync(join(process.cwd(), 'src', 'main.ts'), 'utf8')
|
||||
const lock = main.indexOf('if (!app.requestSingleInstanceLock())')
|
||||
const earlyQuit = main.indexOf(
|
||||
'if (isDesktopInstallerQuitRequest(process.argv, process.platform))',
|
||||
)
|
||||
const startup = main.indexOf('let shutdown: DesktopShutdown')
|
||||
const secondInstance = main.indexOf("app.on('second-instance', (_event, argv) => {")
|
||||
const secondQuit = main.indexOf(
|
||||
'if (isDesktopInstallerQuitRequest(argv, process.platform))',
|
||||
secondInstance,
|
||||
)
|
||||
const show = main.indexOf('startupRecoveryWindow.show()', secondInstance)
|
||||
|
||||
expect(lock).toBeGreaterThanOrEqual(0)
|
||||
expect(earlyQuit).toBeGreaterThan(lock)
|
||||
expect(earlyQuit).toBeLessThan(startup)
|
||||
expect(secondInstance).toBeGreaterThan(startup)
|
||||
expect(secondQuit).toBeGreaterThan(secondInstance)
|
||||
expect(secondQuit).toBeLessThan(show)
|
||||
expect(main.slice(secondQuit, show)).toContain('requestQuit(0)')
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,36 @@
|
||||
import { readFileSync } from 'node:fs'
|
||||
import { join } from 'node:path'
|
||||
import { describe, expect, it } from 'vitest'
|
||||
import { DESKTOP_INSTALLER_QUIT_FLAG } from '../src/desktop-installer-quit.ts'
|
||||
|
||||
describe('Windows NSIS running-app handoff', () => {
|
||||
it('checks for the exact app before requesting orderly shutdown', () => {
|
||||
const script = readFileSync(join(process.cwd(), 'build', 'installer.nsh'), 'utf8')
|
||||
const firstDetection = script.indexOf('!insertmacro FIND_PROCESS')
|
||||
const request = script.indexOf(DESKTOP_INSTALLER_QUIT_FLAG)
|
||||
const wait = script.indexOf('dsh_installer_wait_for_exit:')
|
||||
const fallback = script.indexOf('dsh_installer_scoped_fallback:')
|
||||
|
||||
expect(script).toContain('!macro customCheckAppRunning')
|
||||
expect(script).toContain('Var pid')
|
||||
expect(script).toContain('ExecWait')
|
||||
expect(script).toContain('$INSTDIR\\${APP_EXECUTABLE_FILENAME}')
|
||||
expect(script).toContain('!insertmacro IS_POWERSHELL_AVAILABLE')
|
||||
expect(firstDetection).toBeGreaterThanOrEqual(0)
|
||||
expect(request).toBeGreaterThan(firstDetection)
|
||||
expect(wait).toBeGreaterThan(request)
|
||||
expect(fallback).toBeGreaterThan(wait)
|
||||
})
|
||||
|
||||
it('waits for graceful disposal before using the scoped builder fallback', () => {
|
||||
const script = readFileSync(join(process.cwd(), 'build', 'installer.nsh'), 'utf8')
|
||||
|
||||
expect(script).toContain('$R1 < 12')
|
||||
expect(script).toContain('Sleep 500')
|
||||
expect(script).toContain('!insertmacro KILL_PROCESS "${APP_EXECUTABLE_FILENAME}" 0')
|
||||
expect(script).toContain('!insertmacro KILL_PROCESS "${APP_EXECUTABLE_FILENAME}" 1')
|
||||
expect(script).not.toContain('taskkill')
|
||||
expect(script).not.toContain('nsProcess::KillProcess')
|
||||
expect(script).not.toContain('getProcessInfo.nsh')
|
||||
})
|
||||
})
|
||||
@@ -603,6 +603,7 @@ describe('published package surface', () => {
|
||||
}])
|
||||
expect(manifest.build?.win?.artifactName).toBe('DSH-Desktop-${version}-${arch}-Portable.${ext}')
|
||||
expect(manifest.build?.nsis).toEqual({
|
||||
include: 'installer.nsh',
|
||||
license: 'THIRD_PARTY_NOTICES.md',
|
||||
oneClick: false,
|
||||
perMachine: false,
|
||||
@@ -632,6 +633,8 @@ describe('published package surface', () => {
|
||||
expect(manifest.scripts?.['check:win-package']).toContain('yarn run build')
|
||||
expect(manifest.scripts?.['check:win-package']).toContain('yarn run typecheck')
|
||||
expect(manifest.scripts?.['check:win-package']).toContain('tests/package-win.spec.ts')
|
||||
expect(manifest.scripts?.['check:win-package']).toContain('tests/desktop-installer-quit.spec.ts')
|
||||
expect(manifest.scripts?.['check:win-package']).toContain('tests/installer-nsh.spec.ts')
|
||||
expect(manifest.scripts?.['check:win-package']).toContain('tests/verify-win-portable.spec.ts')
|
||||
expect(manifest.scripts?.['check:win-package']).toContain('tests/update-checker.spec.ts')
|
||||
expect(manifest.scripts?.['check:win-package']).toContain('tests/update-download.spec.ts')
|
||||
|
||||
Reference in New Issue
Block a user