fix(desktop): preserve reported recovery state and label retry attempts
This commit is contained in:
1 parent
fae05edca7
commit
dcc8df8320
9 files changed
+37
-16
No files matched your search
@@ -45,8 +45,8 @@
|
||||
en: b42f103358d0dfd7
|
||||
zh: a534164ad9eb00da
|
||||
/desktop-packaging/runtime-file-selection-and-installation-checks:
|
||||
en: 777f4c85bc3325eb
|
||||
zh: 448f2978876a091e
|
||||
en: bfd89e1d139c5729
|
||||
zh: 5d6cced5462ba13e
|
||||
/desktop-packaging/desktop-runtime-preparation:
|
||||
en: a7aa3066e649ffe8
|
||||
zh: 4334637910058c09
|
||||
|
||||
@@ -244,7 +244,7 @@ pwsh -NoProfile -File apps/desktop/scripts/smoke-windows.ps1 -Makensis $Makensis
|
||||
|
||||
The Windows installer checks for a running application at startup and after destination selection, before extracting the new version beside the installation directory. It checks again before replacing directories through same-volume renames. A running application blocks installation; update launches allow up to ten seconds for it to exit. Same-path upgrades preserve the old directory until promotion succeeds; extraction failure leaves it intact, and promotion failure attempts to restore it. The installer removes the old backup before launch. Forced termination or power loss can leave `.new-*` or `.old-*` directories; installation-location and scope migrations retain electron-builder's old-uninstaller flow.
|
||||
|
||||
Directory renames, empty-destination removal, and restoration retry Windows errors 5, 32, and 33 up to ten attempts, with 250 ms increasing delays (11.25 seconds of total waiting per operation). Other errors stop immediately. A replacement failure reports the Windows error, affected paths, and restoration outcome in `directory-failure-<timestamp>.log` beside extraction reports. If restoration fails, keep the backup path named in the report and contact support before reinstalling. Silent installs save the same diagnostic without a dialog.
|
||||
Directory renames, empty-destination removal, and restoration retry Windows errors 5, 32, and 33 up to ten attempts, with 250 ms increasing delays (11.25 seconds of total waiting per operation). Other errors stop immediately. A replacement failure reports the Windows error, affected paths, and restoration outcome in `directory-failure-<timestamp>.log` beside extraction reports. If restoration fails, the installer removes unlocked staging files and preserves the backup. After reporting that failure, exit cleanup does not retry restoration. Keep the backup path named in the report and contact support before reinstalling. Silent installs save the same diagnostic without a dialog.
|
||||
|
||||
When extraction fails, the installer writes a report with the 7-Zip result and its complete error output to the updater cache directory as `%LOCALAPPDATA%\<package-derived name>-updater\installer-logs\extract-failure-<timestamp>.log` (currently `@deepseek-aidsh-desktop-updater`) and shows the first error line with a **Copy error details** button; silent installs write only the report. Unsigned Windows builds (`DSH_DESKTOP_UNSIGNED=1`) name their installer `deepseek-harness-<version>-win-x64-unsigned.exe` so they cannot be mistaken for release artifacts.
|
||||
|
||||
|
||||
@@ -244,7 +244,7 @@ pwsh -NoProfile -File apps/desktop/scripts/smoke-windows.ps1 -Makensis $Makensis
|
||||
|
||||
Windows 安装器在启动时和选定目标目录后检查应用是否正在运行,通过检查后才将新版本解压到安装目录旁边。通过同卷目录改名替换前,安装器会再次检查。运行中的应用会阻止安装;更新启动允许等待应用退出,最长十秒。同路径升级在替换成功前保留旧目录;解压失败时旧版不变,替换失败时尝试恢复旧目录。安装器在启动前清理旧版备份。强制结束安装器或断电可能留下 `.new-*` 或 `.old-*` 目录;不同安装位置或安装范围迁移仍使用 electron-builder 的旧卸载器流程。
|
||||
|
||||
目录改名、空目标目录删除和旧版恢复遇到 Windows 错误 5、32 或 33 时最多尝试十次,每次等待递增 250 毫秒(每个操作合计等待 11.25 秒)。其他错误立即停止。替换失败时,安装器将 Windows 错误、相关路径和恢复结果写入解压报告旁的 `directory-failure-<时间戳>.log`。如果恢复失败,请保留报告所列的备份目录,并在重新安装前联系支持人员。静默安装保存相同的诊断,不显示弹窗。
|
||||
目录改名、空目标目录删除和旧版恢复遇到 Windows 错误 5、32 或 33 时最多尝试十次,每次等待递增 250 毫秒(每个操作合计等待 11.25 秒)。其他错误立即停止。替换失败时,安装器将 Windows 错误、相关路径和恢复结果写入解压报告旁的 `directory-failure-<时间戳>.log`。如果恢复失败,安装器会清理未被占用的暂存文件并保留备份。上报此失败后,退出清理不再重试恢复。请保留报告所列的备份目录,并在重新安装前联系支持人员。静默安装保存相同的诊断,不显示弹窗。
|
||||
|
||||
解压失败时,安装器会把 7-Zip 的结果和完整错误输出写入更新缓存目录 `%LOCALAPPDATA%\<按包名派生>-updater\installer-logs\extract-failure-<时间戳>.log`(当前为 `@deepseek-aidsh-desktop-updater`),并在弹窗中显示首条错误行和 **复制错误信息** 按钮;静默安装只写入报告。未签名的 Windows 构建(`DSH_DESKTOP_UNSIGNED=1`)会将安装包命名为 `deepseek-harness-<版本>-win-x64-unsigned.exe`,以免被误当作发布产物。
|
||||
|
||||
|
||||
@@ -10,9 +10,11 @@ Var dshDirectorySource
|
||||
Var dshDirectoryDestination
|
||||
Var dshRestoreError
|
||||
Var dshDirectoryLogDirectory
|
||||
Var dshRecoveryStopped
|
||||
|
||||
; MoveFileEx preserves the destination and exposes the Windows error before rollback changes it.
|
||||
!macro dshDirectoryOperation SOURCE DESTINATION RESULT
|
||||
; Open files inside a directory can make its rename return access denied (5).
|
||||
!macro dshDirectoryOperation SOURCE DESTINATION RESULT PHASE
|
||||
Push $0
|
||||
Push $1
|
||||
Push $2
|
||||
@@ -38,7 +40,7 @@ Var dshDirectoryLogDirectory
|
||||
FileOpen $2 "$PLUGINSDIR\directory-operations.log" a
|
||||
${IfNot} ${Errors}
|
||||
FileSeek $2 0 END
|
||||
FileWrite $2 "Windows error: ${RESULT}$\r$\n"
|
||||
FileWrite $2 "${PHASE}: Windows error: ${RESULT}$\r$\n"
|
||||
FileClose $2
|
||||
${EndIf}
|
||||
${If} ${RESULT} != 5
|
||||
@@ -59,6 +61,9 @@ Var dshDirectoryLogDirectory
|
||||
!macroend
|
||||
|
||||
!macro dshReportDirectoryFailure
|
||||
${If} $dshRestoreError != 0
|
||||
StrCpy $dshRecoveryStopped "1"
|
||||
${EndIf}
|
||||
!ifmacrodef customInstallerDirectoryFailed
|
||||
!insertmacro customInstallerDirectoryFailed
|
||||
!else
|
||||
@@ -89,6 +94,7 @@ Var dshDirectoryLogDirectory
|
||||
|
||||
!macro dshStageApplication
|
||||
!ifdef DSH_INSTALLER_LOG_DIR
|
||||
; electron-builder defines the cache name after including our function bodies, before this section expands.
|
||||
StrCpy $dshDirectoryLogDirectory "${DSH_INSTALLER_LOG_DIR}"
|
||||
!endif
|
||||
StrCpy $dshFinalDirectory $INSTDIR
|
||||
@@ -103,6 +109,7 @@ Var dshDirectoryLogDirectory
|
||||
StrCpy $dshNewMoved ""
|
||||
StrCpy $dshDirectoryError 0
|
||||
StrCpy $dshRestoreError 0
|
||||
StrCpy $dshRecoveryStopped ""
|
||||
ClearErrors
|
||||
CreateDirectory $dshNewDirectory
|
||||
${If} ${Errors}
|
||||
@@ -129,6 +136,9 @@ Function .onGUIEnd
|
||||
FunctionEnd
|
||||
|
||||
Function dshCleanupDirectories
|
||||
${If} $dshRecoveryStopped == "1"
|
||||
Return
|
||||
${EndIf}
|
||||
${If} $dshFinalDirectory != ""
|
||||
Call dshRollbackDirectories
|
||||
${EndIf}
|
||||
@@ -142,10 +152,11 @@ Function dshRollbackDirectories
|
||||
StrCpy $dshNewMoved ""
|
||||
${EndIf}
|
||||
${If} $dshOldMoved == "1"
|
||||
!insertmacro dshDirectoryOperation "$dshOldDirectory" "$dshFinalDirectory" $dshRestoreError
|
||||
!insertmacro dshDirectoryOperation "$dshOldDirectory" "$dshFinalDirectory" $dshRestoreError "restore"
|
||||
${If} $dshRestoreError != 0
|
||||
; Leave the complete backup in place if another process prevents restoration.
|
||||
DetailPrint $dshOldDirectory
|
||||
RMDir /r "\\?\$dshNewDirectory"
|
||||
Return
|
||||
${EndIf}
|
||||
StrCpy $dshOldMoved ""
|
||||
@@ -166,7 +177,7 @@ Function dshPromoteDirectories
|
||||
${If} ${FileExists} "$dshFinalDirectory\*.*"
|
||||
StrCpy $dshDirectorySource $dshFinalDirectory
|
||||
StrCpy $dshDirectoryDestination $dshOldDirectory
|
||||
!insertmacro dshDirectoryOperation "$dshDirectorySource" "$dshDirectoryDestination" $dshDirectoryError
|
||||
!insertmacro dshDirectoryOperation "$dshDirectorySource" "$dshDirectoryDestination" $dshDirectoryError "backup"
|
||||
${If} $dshDirectoryError != 0
|
||||
Call dshRollbackDirectories
|
||||
!insertmacro dshReportDirectoryFailure
|
||||
@@ -178,7 +189,7 @@ Function dshPromoteDirectories
|
||||
; NSIS can create the destination before the install section starts.
|
||||
StrCpy $dshDirectorySource $dshFinalDirectory
|
||||
StrCpy $dshDirectoryDestination ""
|
||||
!insertmacro dshDirectoryOperation "$dshDirectorySource" "" $dshDirectoryError
|
||||
!insertmacro dshDirectoryOperation "$dshDirectorySource" "" $dshDirectoryError "remove-empty"
|
||||
${If} $dshDirectoryError != 0
|
||||
Call dshRollbackDirectories
|
||||
!insertmacro dshReportDirectoryFailure
|
||||
@@ -189,7 +200,7 @@ Function dshPromoteDirectories
|
||||
ClearErrors
|
||||
StrCpy $dshDirectorySource $dshNewDirectory
|
||||
StrCpy $dshDirectoryDestination $dshFinalDirectory
|
||||
!insertmacro dshDirectoryOperation "$dshDirectorySource" "$dshDirectoryDestination" $dshDirectoryError
|
||||
!insertmacro dshDirectoryOperation "$dshDirectorySource" "$dshDirectoryDestination" $dshDirectoryError "promote"
|
||||
${If} $dshDirectoryError != 0
|
||||
Call dshRollbackDirectories
|
||||
!insertmacro dshReportDirectoryFailure
|
||||
|
||||
@@ -102,14 +102,17 @@ try {
|
||||
$backups = @(Get-ChildItem -LiteralPath $caseRoot -Directory -Filter 'Application.old-*')
|
||||
if ($backups.Count -ne 1 -or [IO.File]::ReadAllText((Join-Path $backups[0].FullName 'asset.txt')) -ne 'old asset' -or
|
||||
[IO.File]::ReadAllText((Join-Path $backups[0].FullName 'obsolete.txt')) -ne 'old only' -or
|
||||
[IO.File]::ReadAllText((Join-Path $target 'unrelated.txt')) -ne 'unrelated file') {
|
||||
[IO.File]::ReadAllText((Join-Path "$target.conflict" 'unrelated.txt')) -ne 'unrelated file') {
|
||||
throw 'Failed restoration did not preserve the backup and the conflicting destination'
|
||||
}
|
||||
if ((Test-Path -LiteralPath $target) -or @(Get-ChildItem -LiteralPath $caseRoot -Directory -Filter 'Application.new-*').Count -ne 0) {
|
||||
throw 'Reported restoration failure retried recovery or retained its unlocked staging directory'
|
||||
}
|
||||
if ($FrameLibrary) {
|
||||
$reports = @(Get-ChildItem -LiteralPath $reportDir -File)
|
||||
if ($reports.Count -ne 1) { throw 'Failed restoration did not save exactly one report' }
|
||||
$report = Get-Content -LiteralPath $reports[0].FullName -Raw -Encoding UTF8
|
||||
foreach ($expected in @('Windows error: 183', 'Restoration error: 183', "Preserved backup: $($backups[0].FullName)")) {
|
||||
foreach ($expected in @('promote: Windows error: 183', 'restore: Windows error: 183', 'Restoration error: 183', "Preserved backup: $($backups[0].FullName)")) {
|
||||
if (-not $report.Contains($expected)) { throw "Failed restoration report lacks '$expected'" }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"welcome-ready-before-first-show",
|
||||
"welcome-presented-on-first-show",
|
||||
"successful-install-paints-100-before-finish",
|
||||
"locked-directory-shows-copyable-error-and-preserves-installation",
|
||||
"locked-directory-shows-error-and-copy-control-and-preserves-installation",
|
||||
"registered-directory-with-trailing-separators",
|
||||
"registered-directory-and-checked-launch",
|
||||
"launch-failure-retry-and-prompt-dismissal",
|
||||
|
||||
@@ -22,7 +22,7 @@ LangString decompressionFailed ${LANG_ENGLISH} "Payload extraction failed"
|
||||
Section
|
||||
InitPluginsDir
|
||||
FileOpen $0 "${TARGET_DIR}.plugin-dir.tmp" w
|
||||
FileWrite $0 "$PLUGINSDIR"
|
||||
FileWriteUTF16LE /BOM $0 "$PLUGINSDIR"
|
||||
FileClose $0
|
||||
Rename "${TARGET_DIR}.plugin-dir.tmp" "${TARGET_DIR}.plugin-dir"
|
||||
!ifdef SOURCE_DLL
|
||||
@@ -39,9 +39,11 @@ Section
|
||||
FileClose $0
|
||||
StrCpy $dshDirectorySource $dshNewDirectory
|
||||
StrCpy $dshDirectoryDestination $dshFinalDirectory
|
||||
!insertmacro dshDirectoryOperation "$dshDirectorySource" "$dshDirectoryDestination" $dshDirectoryError
|
||||
!insertmacro dshDirectoryOperation "$dshDirectorySource" "$dshDirectoryDestination" $dshDirectoryError "promote"
|
||||
Call dshRollbackDirectories
|
||||
!insertmacro dshReportDirectoryFailure
|
||||
Rename $dshFinalDirectory "${TARGET_DIR}.conflict"
|
||||
Call dshCleanupDirectories
|
||||
SetErrorLevel 2
|
||||
Quit
|
||||
!endif
|
||||
|
||||
@@ -141,6 +141,11 @@ int wmain() {
|
||||
"Preserved backup: C:\\Apps\\\xE6\x96\x87\xE4\xBB\xB6.old"}) {
|
||||
assert(directorySaved.find(expected) != std::string::npos);
|
||||
}
|
||||
assert(InstallerReportDirectoryFailure(nullptr, ERROR_ACCESS_DENIED, L"a", L"", 0, L"", log.c_str(), directoryReport.c_str(), 0,
|
||||
L"title", L"heading", L"hint", L"copy", L"expand", L"collapse", L"saved %s", L"unsaved", L"copied") == 1);
|
||||
const std::string removalSaved = ReadBytes(directoryReport);
|
||||
assert(removalSaved.find("Operation: RemoveDirectoryW") != std::string::npos);
|
||||
assert(removalSaved.find("Preserved backup:") == std::string::npos);
|
||||
assert(InstallerReportDirectoryFailure(nullptr, ERROR_ACCESS_DENIED, L"a", L"", 0, L"", log.c_str(), (log + L"\\impossible.txt").c_str(), 0,
|
||||
L"title", L"heading", L"hint", L"copy", L"expand", L"collapse", L"saved %s", L"unsaved", L"copied") == 0);
|
||||
|
||||
|
||||
@@ -179,7 +179,7 @@ try {
|
||||
} finally {
|
||||
$lockedFile.Dispose()
|
||||
}
|
||||
$results.Add('locked-directory-shows-copyable-error-and-preserves-installation')
|
||||
$results.Add('locked-directory-shows-error-and-copy-control-and-preserves-installation')
|
||||
|
||||
$process = Start-Setup dark ''
|
||||
Click-Control $process $copy.INSTALLER_CHOOSE_PATH
|
||||
|
||||
Reference in new issue
Block a user