mirror of
https://github.com/yynxxxxx/Codex-X.git
synced 2026-10-02 03:24:52 +08:00
fix: continue isolated MSI migration without requiring a reboot
This commit is contained in:
@@ -109,8 +109,16 @@ Var CXOldProcess
|
||||
Var CXWaitTicks
|
||||
Var CXExitCode
|
||||
Var CXMigrations
|
||||
Var CXBootIdentity
|
||||
!define CX_MIGRATION_KEY "Software\${MANUFACTURER}\${PRODUCTNAME}\Installer"
|
||||
Var CXLegacyDirectory
|
||||
Var CXPathInput
|
||||
Var CXPathFull
|
||||
Var CXPathSuffix
|
||||
Var CXPathResult
|
||||
Var CXPathDepth
|
||||
Var CXDestinationFull
|
||||
Var CXDestinationReal
|
||||
Var CXDirectoryIndex
|
||||
!define CX_DIRECTORY_KEY "Software\${MANUFACTURER}\${PRODUCTNAME} Installer\LegacyDirectories"
|
||||
|
||||
Function CXLog
|
||||
Exch $R9
|
||||
@@ -133,19 +141,144 @@ Function CXFail
|
||||
Quit
|
||||
FunctionEnd
|
||||
|
||||
Function CXGetBootIdentity
|
||||
; Win32_OperatingSystem.LastBootUpTime is the documented Windows boot time.
|
||||
; Called only for one-time MSI migration / a pending migration, never during
|
||||
; ordinary NSIS updates. The bounded query cannot hang the installer.
|
||||
nsExec::ExecToStack /TIMEOUT=15000 `"$SYSDIR\WindowsPowerShell\v1.0\powershell.exe" -NoLogo -NoProfile -NonInteractive -Command "try { [Console]::Write((Get-CimInstance -ClassName Win32_OperatingSystem -OperationTimeoutSec 10 -ErrorAction Stop).LastBootUpTime.ToFileTimeUtc()) } catch { exit 1 }"`
|
||||
Pop $0
|
||||
Pop $1
|
||||
${If} $0 != "0"
|
||||
${OrIf} $1 == ""
|
||||
Push "无法确认 Windows 重启状态。请重启后重试,或联系支持并提供安装日志。 / Cannot verify the Windows boot state. Restart and retry."
|
||||
Function CXCanonicalDirectory
|
||||
; Resolve the nearest existing ancestor through a real directory handle.
|
||||
; Volume GUID names unify drive letters, junctions, symbolic links and 8.3
|
||||
; aliases. Append only the normalized, non-existent suffix afterwards.
|
||||
System::Call 'shlwapi::PathIsRelativeW(w "$CXPathInput")i.r0'
|
||||
${If} $0 != 0
|
||||
Push "安装目录必须是完整路径。 / An absolute installation path is required."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
StrCpy $CXBootIdentity $1
|
||||
ClearErrors
|
||||
GetFullPathName $CXPathFull "$CXPathInput"
|
||||
${If} ${Errors}
|
||||
Push "无法读取安装目录。 / Cannot resolve the installation directory."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
StrLen $0 $CXPathFull
|
||||
StrCpy $1 $CXPathFull 1 -1
|
||||
${If} $0 > 3
|
||||
${AndIf} $1 == "\"
|
||||
StrCpy $CXPathFull $CXPathFull -1
|
||||
${EndIf}
|
||||
StrCpy $CXPathSuffix ""
|
||||
StrCpy $CXPathDepth 0
|
||||
cx_resolve_ancestor:
|
||||
System::Call 'kernel32::CreateFileW(w "$CXPathFull",i 0,i 7,p 0,i 3,i 0x02000000,p 0)p.r0 ?e'
|
||||
Pop $1
|
||||
${If} $0 != -1
|
||||
System::Call 'kernel32::GetFinalPathNameByHandleW(p r0,w .r2,i ${NSIS_MAX_STRLEN},i 1)i.r3'
|
||||
System::Call 'kernel32::CloseHandle(p r0)'
|
||||
${If} $3 = 0
|
||||
${OrIf} $3 >= ${NSIS_MAX_STRLEN}
|
||||
Push "无法确认安装目录的实际位置。请选择本机上的其他目录。 / Cannot verify the real directory; choose another local path."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
StrCpy $4 $2 1 -1
|
||||
${If} $4 == "\"
|
||||
StrCpy $2 $2 -1
|
||||
${EndIf}
|
||||
StrCpy $CXPathResult "$2$CXPathSuffix\"
|
||||
Return
|
||||
${EndIf}
|
||||
${If} $1 != 2
|
||||
${AndIf} $1 != 3
|
||||
Push "无法访问安装目录 (Windows $1)。请选择本机上的其他目录。 / Cannot access the installation directory."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
${GetParent} "$CXPathFull" $2
|
||||
${GetFileName} "$CXPathFull" $3
|
||||
${If} $2 == ""
|
||||
${OrIf} $2 == $CXPathFull
|
||||
${OrIf} $3 == ""
|
||||
Push "无法确认安装目录的上级位置。 / Cannot resolve the installation directory's parent."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
StrCpy $CXPathSuffix "\$3$CXPathSuffix"
|
||||
StrCpy $CXPathFull $2
|
||||
; GetParent of C:\child can return C:, which is drive-relative to Win32.
|
||||
StrLen $0 $CXPathFull
|
||||
${If} $0 = 2
|
||||
StrCpy $CXPathFull "$CXPathFull\"
|
||||
${EndIf}
|
||||
IntOp $CXPathDepth $CXPathDepth + 1
|
||||
${If} $CXPathDepth > 256
|
||||
Push "安装路径层级过深。 / Installation path is too deeply nested."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
Goto cx_resolve_ancestor
|
||||
FunctionEnd
|
||||
|
||||
Function CXRememberLegacyDirectory
|
||||
System::Call 'msi::MsiGetProductInfoExW(w "$CXLegacyProduct",p 0,i 4,w "InstallLocation",w .r0,*i ${NSIS_MAX_STRLEN})i.r1'
|
||||
${If} $1 != 0
|
||||
${OrIf} $0 == ""
|
||||
Push "无法读取旧版安装目录。请先在系统设置中卸载旧版,再安装新版本。 / Cannot verify the legacy installation directory."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
System::Call 'shlwapi::PathIsRelativeW(w r0)i.r1'
|
||||
${If} $1 != 0
|
||||
Push "旧版安装目录不是有效的完整路径,已停止迁移。 / The legacy installation path is not absolute."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
GetFullPathName $CXLegacyDirectory "$0"
|
||||
StrCpy $0 $CXLegacyDirectory 1 -1
|
||||
${If} $0 != "\"
|
||||
StrCpy $CXLegacyDirectory "$CXLegacyDirectory\"
|
||||
${EndIf}
|
||||
StrCpy $CXPathInput $CXLegacyDirectory
|
||||
Call CXCanonicalDirectory
|
||||
; Retain both lexical and resolved names. A later junction change must not
|
||||
; redirect a delayed removal from the old lexical path into the new app.
|
||||
; Kept outside the normal uninstall key so an interrupted/retried migration
|
||||
; cannot forget these reserved paths. These values contain paths, no secrets.
|
||||
ClearErrors
|
||||
WriteRegStr HKCU "${CX_DIRECTORY_KEY}" "Path_$CXLegacyProduct" $CXLegacyDirectory
|
||||
WriteRegStr HKCU "${CX_DIRECTORY_KEY}" "Real_$CXLegacyProduct" $CXPathResult
|
||||
${If} ${Errors}
|
||||
Push "无法保存旧版安装目录,未卸载旧版。 / Cannot save the legacy directory for safe migration."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Function CXValidateDestination
|
||||
; No filesystem scan is needed for normal fresh installs without history.
|
||||
EnumRegValue $0 HKCU "${CX_DIRECTORY_KEY}" 0
|
||||
${If} $0 == ""
|
||||
Return
|
||||
${EndIf}
|
||||
GetFullPathName $CXDestinationFull "$INSTDIR"
|
||||
StrCpy $0 $CXDestinationFull 1 -1
|
||||
${If} $0 != "\"
|
||||
StrCpy $CXDestinationFull "$CXDestinationFull\"
|
||||
${EndIf}
|
||||
StrCpy $CXPathInput $CXDestinationFull
|
||||
Call CXCanonicalDirectory
|
||||
StrCpy $CXDestinationReal $CXPathResult
|
||||
StrCpy $CXDirectoryIndex 0
|
||||
cx_check_reserved:
|
||||
EnumRegValue $0 HKCU "${CX_DIRECTORY_KEY}" $CXDirectoryIndex
|
||||
${If} $0 == ""
|
||||
Return
|
||||
${EndIf}
|
||||
ReadRegStr $1 HKCU "${CX_DIRECTORY_KEY}" "$0"
|
||||
${If} $1 == ""
|
||||
Push "旧版安装目录记录不完整,已停止安装。 / The legacy directory record is incomplete."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
StrLen $2 $1
|
||||
StrCpy $3 $CXDestinationFull $2
|
||||
StrCpy $4 $CXDestinationReal $2
|
||||
; NSIS StrCmp / LogicLib == are case insensitive. All names end in '\',
|
||||
; so sibling folders such as Codex-X-New do not falsely match Codex-X.
|
||||
${If} $3 == $1
|
||||
${OrIf} $4 == $1
|
||||
Push "新版本需要安装到独立目录。请使用默认用户目录,不要选择旧版目录或它的子目录。 / Choose a separate directory, not the legacy MSI directory or its children."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
IntOp $CXDirectoryIndex $CXDirectoryIndex + 1
|
||||
Goto cx_check_reserved
|
||||
FunctionEnd
|
||||
|
||||
Function CXInitialize
|
||||
@@ -192,15 +325,6 @@ Function CXInitialize
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
${EndIf}
|
||||
ReadRegStr $5 HKCU "${CX_MIGRATION_KEY}" "PendingBoot"
|
||||
${If} $5 != ""
|
||||
Call CXGetBootIdentity
|
||||
${If} $CXBootIdentity == $5
|
||||
Push "上次旧版迁移尚需 Windows 重启。请重启后再运行安装包。 / Restart Windows before continuing the previous MSI migration."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
DeleteRegValue HKCU "${CX_MIGRATION_KEY}" "PendingBoot"
|
||||
${EndIf}
|
||||
Call CXDetectLegacyMsi
|
||||
FunctionEnd
|
||||
|
||||
@@ -226,6 +350,7 @@ Function CXDetectLegacyMsi
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
StrCpy $CXLegacyVersion $0
|
||||
Call CXRememberLegacyDirectory
|
||||
nsis_tauri_utils::SemverCompare "${VERSION}" $CXLegacyVersion
|
||||
Pop $0
|
||||
${If} $0 = -1
|
||||
@@ -300,16 +425,7 @@ Function CXMigrateLegacyMsi
|
||||
Push "发现异常的旧版安装记录,已停止安装。 / Too many legacy registrations; migration stopped."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
Call CXGetBootIdentity
|
||||
; Write before removal so cancellation of this process cannot lose the
|
||||
; reboot barrier after MSI has scheduled file deletion. Known clean outcomes
|
||||
; below clear it. Logging off alone does not bypass the boot-time check.
|
||||
ClearErrors
|
||||
WriteRegStr HKCU "${CX_MIGRATION_KEY}" "PendingBoot" $CXBootIdentity
|
||||
${If} ${Errors}
|
||||
Push "无法保存旧版迁移状态,未卸载旧版。 / Cannot save the migration recovery state."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
Call CXValidateDestination
|
||||
SetDetailsView show
|
||||
Push "正在迁移旧版 $CXLegacyVersion(仅本次需要管理员授权)。 / Migrating the previous MSI installation; administrator approval is needed once."
|
||||
Call CXLog
|
||||
@@ -329,7 +445,6 @@ Function CXMigrateLegacyMsi
|
||||
System::Free $0
|
||||
${If} $1 = 0
|
||||
${OrIf} $4 = 0
|
||||
DeleteRegValue HKCU "${CX_MIGRATION_KEY}" "PendingBoot"
|
||||
Push "未完成管理员授权 (Windows $2),旧版保持不变。请重新运行安装包并允许卸载旧版。 / Administrator approval was not completed."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
@@ -371,12 +486,10 @@ Function CXMigrateLegacyMsi
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
${If} $CXExitCode = 1618
|
||||
DeleteRegValue HKCU "${CX_MIGRATION_KEY}" "PendingBoot"
|
||||
Push "Windows 正在安装其他软件。请等待它完成,再重新运行 Codex-X 安装包。 / Another Windows installation is running. Wait for it to finish and retry."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
${If} $CXExitCode = 1602
|
||||
DeleteRegValue HKCU "${CX_MIGRATION_KEY}" "PendingBoot"
|
||||
Push "已取消旧版卸载,未安装新版本。 / Previous-version removal was cancelled."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
@@ -385,19 +498,17 @@ Function CXMigrateLegacyMsi
|
||||
Push "旧版卸载失败 (Windows $CXExitCode),未安装新版本。请查看安装日志。 / MSI removal failed; no second installation was created."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
${If} $CXExitCode = 3010
|
||||
; Conservatively require completion after reboot before a new install.
|
||||
; Never risk the old MSI's pending removals deleting the new app.
|
||||
Push "旧版已移除,但 Windows 需要重启。请重启后重新运行安装包;不会自动重启。 / Restart Windows, then run this installer again."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
; Verify the exact product really disappeared before touching new files.
|
||||
System::Call 'msi::MsiGetProductInfoExW(w "$CXLegacyProduct",p 0,i 4,w "VersionString",w .r0,*i ${NSIS_MAX_STRLEN})i.r1'
|
||||
${If} $1 != 1605
|
||||
Push "Windows 仍保留旧版安装记录,已停止安装以避免重复。 / Previous MSI is still registered; migration stopped."
|
||||
Call CXFail
|
||||
${EndIf}
|
||||
DeleteRegValue HKCU "${CX_MIGRATION_KEY}" "PendingBoot"
|
||||
Call CXValidateDestination
|
||||
${If} $CXExitCode = 3010
|
||||
Push "旧版卸载完成;Windows 将稍后清理旧目录中的残留文件。新版本安装在独立目录,可继续使用,无需立即重启。 / Legacy cleanup is deferred; the new app can run from its separate directory."
|
||||
Call CXLog
|
||||
${EndIf}
|
||||
Push "旧版 MSI 已安全移除。 / Previous MSI removed."
|
||||
Call CXLog
|
||||
Goto cx_migrate_next
|
||||
@@ -801,6 +912,7 @@ FunctionEnd
|
||||
Section EarlyChecks
|
||||
Call CXWaitForOldApp
|
||||
Call CXWaitForRunningApp
|
||||
Call CXValidateDestination
|
||||
Call CXMigrateLegacyMsi
|
||||
; Abort silent installer if downgrades is disabled
|
||||
!if "${ALLOWDOWNGRADES}" == "false"
|
||||
@@ -913,6 +1025,7 @@ Section WebView2
|
||||
SectionEnd
|
||||
|
||||
Section Install
|
||||
Call CXValidateDestination
|
||||
SetOutPath $INSTDIR
|
||||
|
||||
!ifmacrodef NSIS_HOOK_PREINSTALL
|
||||
|
||||
@@ -17,7 +17,9 @@ Windows 新安装包使用 NSIS EXE,安装到原登录用户的本地应用目
|
||||
|
||||
只提升系统 `msiexec.exe /x {ProductCode}` 子进程的权限;EXE 安装器始终留在原登录用户下,避免使用另一个管理员凭据授权后安装到管理员的个人目录。旧 MSI 保留的 Program Files 路径不作为新安装目录。
|
||||
|
||||
卸载未成功、取消授权、其他安装事务占用、结果未知或要求重启时,停止复制新版本。已卸载但需要重启的情况使用与系统启动关联的标记,避免用户未重启即绕过提示。移除成功后再次验证旧产品注册已消失,才写入新安装。配置、账号、会话目录不属于迁移删除范围。
|
||||
卸载未成功、取消授权、其他安装事务占用或结果未知时,停止复制新版本。移除返回 `0` 或 `3010` 后,再次验证旧产品注册已消失,并确认新目录与卸载前记录的旧目录隔离,才写入新安装。`3010` 表示旧文件可能需要 Windows 后续清理;在上述条件成立时继续安装,不要求用户重启电脑。
|
||||
|
||||
旧目录通过目录句柄解析到真实路径,处理 junction、短路径和驱动别名;目标不能是旧目录或其子目录。旧路径记录保留到异常退出后的重试,防止卸载记录消失后误选旧目录。不会用整机“待重启”状态阻止更新,也不查询启动时间。配置、账号、会话目录不属于迁移删除范围。
|
||||
|
||||
安装器按阶段记录日志,旧版卸载另外记录 MSI 详细日志。长时间等待会持续显示已等待时间和日志位置,不强制结束系统安装事务。
|
||||
|
||||
|
||||
@@ -18,6 +18,8 @@ using System.Text;
|
||||
public static class CodexXMsiSmoke {
|
||||
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern uint MsiEnumRelatedProducts(string code, uint reserved, uint index, StringBuilder product);
|
||||
[DllImport("msi.dll", CharSet = CharSet.Unicode)]
|
||||
public static extern uint MsiGetProductInfoEx(string code, string sid, uint context, string property, StringBuilder value, ref uint length);
|
||||
}
|
||||
'@
|
||||
function Related-Products {
|
||||
@@ -81,18 +83,6 @@ try {
|
||||
Assert-Installed
|
||||
Invoke-Installer $Installer '/S /UPDATE'
|
||||
Assert-Installed
|
||||
# A 3010/incomplete migration barrier must survive immediate retries. Once
|
||||
# the boot identity differs, it must be cleared and allow the install.
|
||||
$pendingKey = 'HKCU:\Software\yynxxxxx\Codex-X\Installer'
|
||||
New-Item -Force $pendingKey | Out-Null
|
||||
$boot = (Get-CimInstance Win32_OperatingSystem).LastBootUpTime.ToFileTimeUtc().ToString()
|
||||
Set-ItemProperty $pendingKey PendingBoot $boot
|
||||
Invoke-Installer $Installer '/S /UPDATE' 1
|
||||
Assert-Installed
|
||||
Set-ItemProperty $pendingKey PendingBoot 'previous-boot-smoke-fixture'
|
||||
Invoke-Installer $Installer '/S /UPDATE'
|
||||
if ((Get-ItemProperty $pendingKey -ErrorAction SilentlyContinue).PendingBoot) { throw 'Previous-boot marker was not cleared.' }
|
||||
Assert-Installed
|
||||
Remove-TestNsis
|
||||
|
||||
Write-Host 'Scenario 2: released v0.3.20 machine MSI -> current-user NSIS -> NSIS update.'
|
||||
@@ -101,13 +91,65 @@ try {
|
||||
if ((Get-FileHash $legacyMsi -Algorithm SHA256).Hash.ToLowerInvariant() -ne $LegacySha256) { throw 'Released MSI hash mismatch.' }
|
||||
Invoke-Installer (Join-Path $env:SystemRoot 'System32\msiexec.exe') "/i `"$legacyMsi`" /qn /norestart /L*v `"$Work\legacy-install.log`""
|
||||
if ($LegacyProductCode -notin @(Related-Products)) { throw 'The genuine legacy MSI did not register.' }
|
||||
# Deliberately keep the real MSI's HKCU manufacturer path to verify that the
|
||||
# NSIS installer does not inherit a legacy Program Files directory.
|
||||
$oldDirectory = [Text.StringBuilder]::new(1024)
|
||||
[uint32]$oldDirectoryLength = 1024
|
||||
$status = [CodexXMsiSmoke]::MsiGetProductInfoEx($LegacyProductCode, $null, 4, 'InstallLocation', $oldDirectory, [ref]$oldDirectoryLength)
|
||||
if ($status -ne 0 -or $oldDirectory.Length -eq 0) { throw 'Could not read actual legacy MSI InstallLocation.' }
|
||||
$oldPath = $oldDirectory.ToString().TrimEnd('\')
|
||||
$oldExe = Join-Path $oldPath 'codex-x.exe'
|
||||
$oldHash = (Get-FileHash $oldExe).Hash
|
||||
# Guard the old directory, a non-existent child, and a junction alias. None
|
||||
# may remove the registered MSI or overwrite its files.
|
||||
Invoke-Installer $Installer "/S /UPDATE /D=$oldPath" 1
|
||||
Invoke-Installer $Installer "/S /UPDATE /D=$oldPath\unsafe-child" 1
|
||||
$alias = Join-Path $Work 'legacy-junction'
|
||||
New-Item -ItemType Junction -Path $alias -Target $oldPath | Out-Null
|
||||
try {
|
||||
Invoke-Installer $Installer "/S /UPDATE /D=$alias\unsafe-child" 1
|
||||
} finally {
|
||||
[IO.Directory]::Delete($alias)
|
||||
}
|
||||
if ($LegacyProductCode -notin @(Related-Products) -or (Get-FileHash $oldExe).Hash -ne $oldHash) {
|
||||
throw 'A refused overlapping installation changed the legacy app.'
|
||||
}
|
||||
# Keep the real MSI's HKCU manufacturer path to verify that NSIS does not
|
||||
# inherit Program Files when the user accepts the default directory.
|
||||
Invoke-Installer $Installer '/S /UPDATE'
|
||||
Assert-Installed
|
||||
Invoke-Installer $Installer '/S /UPDATE'
|
||||
Assert-Installed
|
||||
Write-Host 'Windows installation smoke tests passed; configuration sentinels unchanged.'
|
||||
Remove-TestNsis
|
||||
|
||||
Write-Host 'Scenario 3: MSI returns 3010 after removal; separate NSIS install continues without reboot.'
|
||||
# A separate fixture copy of the hash-verified released MSI requests a reboot
|
||||
# only after REMOVE=ALL. /norestart prevents any OS reboot. This exercises the
|
||||
# genuine Windows Installer 3010 result, not a mocked installer exit code.
|
||||
$rebootMsi = Join-Path $Work 'legacy-deferred-cleanup-fixture.msi'
|
||||
Copy-Item $legacyMsi $rebootMsi
|
||||
$msiAutomation = New-Object -ComObject WindowsInstaller.Installer
|
||||
$database = $msiAutomation.OpenDatabase($rebootMsi, 1)
|
||||
$query = $database.OpenView('INSERT INTO `InstallExecuteSequence` (`Action`, `Condition`, `Sequence`) VALUES (''ScheduleReboot'', ''REMOVE="ALL"'', 6500)')
|
||||
$query.Execute()
|
||||
$query.Close()
|
||||
$summary = $database.GetType().InvokeMember('SummaryInformation', [Reflection.BindingFlags]::GetProperty, $null, $database, @(1))
|
||||
$newPackageCode = '{' + [guid]::NewGuid().ToString().ToUpperInvariant() + '}'
|
||||
[void]$summary.GetType().InvokeMember('Property', [Reflection.BindingFlags]::SetProperty, $null, $summary, @(9, $newPackageCode))
|
||||
$summary.Persist()
|
||||
$database.Commit()
|
||||
foreach ($comObject in @($query, $summary, $database, $msiAutomation)) {
|
||||
[void][Runtime.InteropServices.Marshal]::FinalReleaseComObject($comObject)
|
||||
}
|
||||
Invoke-Installer (Join-Path $env:SystemRoot 'System32\msiexec.exe') "/i `"$rebootMsi`" /qn /norestart /L*v `"$Work\deferred-fixture-install.log`""
|
||||
if ($LegacyProductCode -notin @(Related-Products)) { throw 'The deferred-cleanup MSI fixture did not register.' }
|
||||
$beforeMigration = Get-Date
|
||||
Invoke-Installer $Installer '/S /UPDATE'
|
||||
Assert-Installed
|
||||
$migrationLogs = @(Get-ChildItem (Join-Path $env:LOCALAPPDATA 'Codex-X-updates\logs') -Filter 'install-*.log' |
|
||||
Where-Object { $_.LastWriteTime -ge $beforeMigration -and $_.Name -notlike '*-msi-*' })
|
||||
if (-not ($migrationLogs | Select-String -SimpleMatch 'MSI exit code: 3010')) { throw 'The deferred-cleanup scenario did not actually exercise MSI exit 3010.' }
|
||||
Invoke-Installer $Installer '/S /UPDATE'
|
||||
Assert-Installed
|
||||
Write-Host 'Windows installation smoke tests passed; no reboot needed, configuration sentinels unchanged.'
|
||||
} finally {
|
||||
Get-ChildItem (Join-Path $env:LOCALAPPDATA 'Codex-X-updates\logs') -Filter 'install-*.log' -ErrorAction SilentlyContinue | ForEach-Object {
|
||||
Write-Host "--- $($_.Name) ---"
|
||||
|
||||
Reference in New Issue
Block a user