diff --git a/install.ps1 b/install.ps1 index 4e75057..29aa895 100644 --- a/install.ps1 +++ b/install.ps1 @@ -315,16 +315,44 @@ function Install-Binary($Source, $Destination) { $dir = Split-Path -Parent $Destination $name = Split-Path -Leaf $Destination $tmp = Join-Path $dir ".$name.tmp.$PID" - Copy-Item $Source $tmp -Force + Copy-Item -LiteralPath $Source -Destination $tmp -Force try { - Move-Item $tmp $Destination -Force + Move-Item -LiteralPath $tmp -Destination $Destination -Force } catch { - Remove-Item $tmp -Force -ErrorAction SilentlyContinue - throw + $pending = "$Destination.pending.$PID" + try { + Move-Item -LiteralPath $tmp -Destination $pending -Force + Start-DeferredBinaryReplacement $pending $Destination + Write-Warning "binary is in use; staged replacement for when Dosh exits: $Destination" + } + catch { + Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue + Remove-Item -LiteralPath $pending -Force -ErrorAction SilentlyContinue + throw + } } } +function Start-DeferredBinaryReplacement($Pending, $Destination) { + $payload = @{ Pending = $Pending; Destination = $Destination } | ConvertTo-Json -Compress + $payload64 = [Convert]::ToBase64String([System.Text.Encoding]::UTF8.GetBytes($payload)) + $script = @" +`$ErrorActionPreference = 'SilentlyContinue' +`$payload = [System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String('$payload64')) | ConvertFrom-Json +for (`$i = 0; `$i -lt 3600; `$i++) { + if (-not (Test-Path -LiteralPath `$payload.Pending)) { exit 0 } + Move-Item -LiteralPath `$payload.Pending -Destination `$payload.Destination -Force + if (`$?) { exit 0 } + Start-Sleep -Seconds 1 +} +exit 1 +"@ + $encoded = [Convert]::ToBase64String([System.Text.Encoding]::Unicode.GetBytes($script)) + $ps = [System.Diagnostics.Process]::GetCurrentProcess().MainModule.FileName + Start-Process -FilePath $ps -ArgumentList @("-NoProfile", "-EncodedCommand", $encoded) -WindowStyle Hidden | Out-Null +} + function Normalize-PathForCompare($Path) { [System.IO.Path]::GetFullPath($Path).TrimEnd( [System.IO.Path]::DirectorySeparatorChar, @@ -440,7 +468,7 @@ function Install-Prebuilt { if ($bench) { Install-Binary $bench.FullName (Join-Path $bindir "dosh-bench.exe") } - Install-Binary (Join-Path $bindir "dosh-client.exe") (Join-Path $bindir "dosh.exe") + Install-Binary $client.FullName (Join-Path $bindir "dosh.exe") return $true } catch { diff --git a/tests/release_scripts.rs b/tests/release_scripts.rs index 9e1a437..32fda97 100644 --- a/tests/release_scripts.rs +++ b/tests/release_scripts.rs @@ -298,12 +298,30 @@ fn windows_prebuilt_install_requires_client_but_not_bench() { assert!(ps1.contains("throw \"prebuilt archive missing dosh-client.exe\"")); assert!(ps1.contains("-Filter \"dosh-bench.exe\"")); assert!(ps1.contains("if ($bench)")); + assert!(ps1.contains("Install-Binary $client.FullName (Join-Path $bindir \"dosh.exe\")")); + assert!(!ps1.contains("Install-Binary (Join-Path $bindir \"dosh-client.exe\")")); assert!( !ps1.contains("throw \"prebuilt archive missing dosh-bench.exe\""), "Windows prebuilt install should not fall back to source just because dosh-bench is absent" ); } +#[test] +fn windows_installer_stages_locked_binary_replacements() { + let ps1 = include_str!("../install.ps1"); + assert!(ps1.contains("function Start-DeferredBinaryReplacement")); + assert!(ps1.contains("$pending = \"$Destination.pending.$PID\"")); + assert!(ps1.contains("Move-Item -LiteralPath $tmp -Destination $pending -Force")); + assert!(ps1.contains("Start-DeferredBinaryReplacement $pending $Destination")); + assert!(ps1.contains("binary is in use; staged replacement")); + assert!(ps1.contains("for (`$i = 0; `$i -lt 3600; `$i++)")); + assert!(ps1.contains( + "Move-Item -LiteralPath `$payload.Pending -Destination `$payload.Destination -Force" + )); + assert!(ps1.contains("[System.Text.Encoding]::Unicode.GetBytes($script)")); + assert!(ps1.contains("Start-Process -FilePath $ps")); +} + #[test] fn windows_installer_adds_user_path_idempotently() { let ps1 = include_str!("../install.ps1");