fix(updater): await helper file locks before starting setup
This commit is contained in:
1 parent
df34a7d91a
commit
0fca583304
4 files changed
+60
-8
No files matched your search
@@ -23,6 +23,9 @@ Retain package SHA-256/size checks, commit authority, participant identity and
|
||||
exit waiting, installer exit-code checks, and the installed version probe.
|
||||
Remove the additional requirement that a same-version main binary change hash.
|
||||
Record the installed hash as evidence, not as a repair-success predicate.
|
||||
Before starting setup, retry exclusive write access to installed Hook helper files
|
||||
for five seconds. Short-lived forwarding can finish; persistent locks fail before
|
||||
any installation file is replaced. The helper never kills unrelated processes.
|
||||
|
||||
## Rejected alternatives
|
||||
|
||||
@@ -39,11 +42,13 @@ asset naming rule changes.
|
||||
|
||||
## Validation
|
||||
|
||||
The native handoff suite passes ten scenarios, including a byte-identical main
|
||||
The native handoff suite passes twelve scenarios, including a byte-identical main
|
||||
binary with a repaired helper. The former same-version/no-change assertion was
|
||||
incorrect; its negative intent is retained as `upgrade-noop`, which returns
|
||||
installer success but leaves the wrong application version and must still fail.
|
||||
Cancellation, checksum/identity failures and unprepared processes remain covered.
|
||||
A released helper-file lock permits installation; a held lock must prevent setup
|
||||
from starting and recover the unchanged original application.
|
||||
|
||||
## Supersedes
|
||||
|
||||
|
||||
@@ -67,6 +67,30 @@ function Check-UnpreparedProcesses {
|
||||
}
|
||||
}
|
||||
|
||||
function Wait-RuntimeHelperFiles {
|
||||
# New helpers have a bounded lifetime, but can still be draining as the
|
||||
# application exits. Check before setup copies any file. Old stuck helpers
|
||||
# remain a visible failure; never terminate processes by their image name.
|
||||
$deadline = [DateTime]::UtcNow.AddSeconds(5)
|
||||
$helpers = @('runtime\pebrel-hook.exe', 'pebrel-hook.exe',
|
||||
'runtime\nebula-hook.exe', 'nebula-hook.exe')
|
||||
while ($true) {
|
||||
$busy = $null
|
||||
foreach ($relative in $helpers) {
|
||||
$path = Join-Path $installation $relative
|
||||
if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { continue }
|
||||
try {
|
||||
$probe = [System.IO.FileStream]::new($path, [System.IO.FileMode]::Open,
|
||||
[System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
|
||||
$probe.Dispose()
|
||||
} catch { $busy = $_.Exception }
|
||||
}
|
||||
if (-not $busy) { return }
|
||||
if ([DateTime]::UtcNow -ge $deadline) { throw $busy }
|
||||
Start-Sleep -Milliseconds 100
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
if ((Get-Item -LiteralPath $PlanPath).Length -gt 1048576) { throw 'Update plan exceeds limit' }
|
||||
$plan = Get-Content -LiteralPath $PlanPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
@@ -133,6 +157,7 @@ try {
|
||||
Check-UnpreparedProcesses
|
||||
# No Restart Manager process-name shutdown: every participant has already
|
||||
# saved and exited. DIR reuses this validated installation without a chooser.
|
||||
Wait-RuntimeHelperFiles
|
||||
$setupLog = Join-Path $transaction 'installer.log'
|
||||
$arguments = '/SP- /VERYSILENT /SUPPRESSMSGBOXES /NORESTART /NOCLOSEAPPLICATIONS /NORESTARTAPPLICATIONS' +
|
||||
' /DIR="' + $installation + '" /LOG="' + $setupLog + '"'
|
||||
|
||||
+5
-3
@@ -17,9 +17,11 @@ class UpdateProcess {
|
||||
if (!File.Exists(Path.Combine(directory, "parent-exited"))) return 3;
|
||||
if (Environment.GetEnvironmentVariable("PEBREL_HANDOFF_FAIL_INSTALL") == "1") return 17;
|
||||
if (Environment.GetEnvironmentVariable("PEBREL_HANDOFF_NOOP_INSTALL") == "1") return 0;
|
||||
File.Copy(Path.Combine(directory, "candidate.exe"), Path.Combine(target, "pebrel.exe"), true);
|
||||
Directory.CreateDirectory(Path.Combine(target, "runtime"));
|
||||
File.WriteAllText(Path.Combine(target, "runtime", "pebrel-hook.exe"), "repaired helper fixture");
|
||||
try {
|
||||
File.Copy(Path.Combine(directory, "candidate.exe"), Path.Combine(target, "pebrel.exe"), true);
|
||||
Directory.CreateDirectory(Path.Combine(target, "runtime"));
|
||||
File.WriteAllText(Path.Combine(target, "runtime", "pebrel-hook.exe"), "repaired helper fixture");
|
||||
} catch (IOException) { return 5; }
|
||||
return 0;
|
||||
#else
|
||||
if (args.Length > 0 && args[0] == "--version") {
|
||||
|
||||
@@ -36,7 +36,7 @@ function Write-Json([string]$Path, $Value) {
|
||||
}
|
||||
|
||||
$results = @()
|
||||
foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'installer-failure', 'other-process', 'late-process', 'reinstall', 'repair-identical', 'upgrade-noop')) {
|
||||
foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'installer-failure', 'other-process', 'late-process', 'reinstall', 'repair-identical', 'upgrade-noop', 'helper-unlocked', 'helper-held')) {
|
||||
$directory = Join-Path $OutputRoot $scenario
|
||||
$installation = Join-Path $directory 'installed app'
|
||||
$transaction = Join-Path $directory 'transaction'
|
||||
@@ -54,7 +54,15 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
|
||||
$null = $parent.Handle
|
||||
$runner = $null
|
||||
$other = $null
|
||||
$helperLock = $null
|
||||
try {
|
||||
if ($scenario -in @('helper-unlocked', 'helper-held')) {
|
||||
$null = New-Item -ItemType Directory -Path (Join-Path $installation 'runtime') -Force
|
||||
$helperPath = Join-Path $installation 'runtime\pebrel-hook.exe'
|
||||
[System.IO.File]::WriteAllText($helperPath, 'old helper fixture')
|
||||
$helperLock = [System.IO.FileStream]::new($helperPath, [System.IO.FileMode]::Open,
|
||||
[System.IO.FileAccess]::Read, [System.IO.FileShare]::Read)
|
||||
}
|
||||
$plan = @{
|
||||
schema = 1; transaction = $scenario; executable = $executable; installation = $installation
|
||||
config_directory = $config; installer = $installer
|
||||
@@ -104,8 +112,14 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
|
||||
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup ran while the old process was alive'
|
||||
[System.IO.File]::WriteAllText((Join-Path $directory 'exit-parent'), 'exit')
|
||||
Assert ($parent.WaitForExit(5000)) 'Fixture parent did not exit'
|
||||
if ($scenario -eq 'helper-unlocked') {
|
||||
Start-Sleep -Milliseconds 350
|
||||
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup started with the helper still locked'
|
||||
$helperLock.Dispose()
|
||||
$helperLock = $null
|
||||
}
|
||||
Wait-File $resultPath
|
||||
if ($scenario -in @('success', 'installer-failure', 'reinstall', 'repair-identical', 'upgrade-noop')) {
|
||||
if ($scenario -in @('success', 'installer-failure', 'reinstall', 'repair-identical', 'upgrade-noop', 'helper-unlocked', 'helper-held')) {
|
||||
Wait-File (Join-Path $directory 'new-launch')
|
||||
$launch = [System.IO.File]::ReadAllLines((Join-Path $directory 'new-launch'))
|
||||
Assert ($launch[0] -eq $executable) 'Relaunch selected another installation'
|
||||
@@ -117,25 +131,31 @@ foreach ($scenario in @('cancel', 'success', 'checksum', 'creation-time', 'insta
|
||||
}
|
||||
Assert ($runner.WaitForExit(5000)) 'Helper did not finish'
|
||||
$result = Get-Content -LiteralPath $resultPath -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||
Assert ($result.success -eq ($scenario -in @('success', 'reinstall', 'repair-identical'))) 'Unexpected helper outcome'
|
||||
Assert ($result.success -eq ($scenario -in @('success', 'reinstall', 'repair-identical', 'helper-unlocked'))) 'Unexpected helper outcome'
|
||||
if ($result.success) {
|
||||
$expected = (Get-FileHash -LiteralPath $payload -Algorithm SHA256).Hash
|
||||
Assert ($result.executable_sha256 -eq $expected) 'Installed binary differs from candidate'
|
||||
Assert (Test-Path -LiteralPath (Join-Path $installation 'runtime\pebrel-hook.exe')) 'Successful setup did not repair the helper'
|
||||
Assert ([System.IO.File]::ReadAllText((Join-Path $installation 'runtime\pebrel-hook.exe')) -eq 'repaired helper fixture') 'Helper still contains the original payload'
|
||||
}
|
||||
if ($scenario -in @('other-process', 'late-process')) {
|
||||
Assert (-not $other.HasExited) 'Update stopped an unprepared process'
|
||||
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'Setup ran with an unprepared process'
|
||||
}
|
||||
if ($scenario -in @('installer-failure', 'upgrade-noop')) {
|
||||
if ($scenario -in @('installer-failure', 'upgrade-noop', 'helper-held')) {
|
||||
Assert $result.recovered_original 'Untouched old executable was not recovered'
|
||||
}
|
||||
if ($scenario -eq 'helper-held') {
|
||||
Assert (-not (Test-Path (Join-Path $directory 'installer-started'))) 'A blocked helper allowed a partial installation'
|
||||
Assert ($result.error -match 'pebrel-hook.exe') 'The blocked file is missing from the failure details'
|
||||
}
|
||||
$probe = [System.IO.FileStream]::new($plan.guard_path, [System.IO.FileMode]::OpenOrCreate,
|
||||
[System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
|
||||
$probe.Dispose()
|
||||
$results += @{ scenario = $scenario; passed = $true }
|
||||
Write-Output "$scenario : passed"
|
||||
} finally {
|
||||
if ($helperLock) { $helperLock.Dispose() }
|
||||
if ($runner -and -not $runner.HasExited) { $runner.Kill(); $runner.WaitForExit() }
|
||||
if (-not $parent.HasExited) {
|
||||
[System.IO.File]::WriteAllText((Join-Path $directory 'exit-parent'), 'exit')
|
||||
|
||||
Reference in new issue
Block a user