diff --git a/install.ps1 b/install.ps1 index 8558012..e52edad 100644 --- a/install.ps1 +++ b/install.ps1 @@ -131,8 +131,13 @@ function Install-Binary { # The running daemon may come from another installation directory. # Use the downloaded CLI: older versions have broken Windows liveness checks. # Stop verifies daemon identity and uses the current BSK_HOME. + # Fail closed for both new installs and replacements; never discard daemon metadata here. & $Source daemon stop - if ($LASTEXITCODE -ne 0) { throw "could not stop bsk daemon; installation was not changed" } + if ($LASTEXITCODE -ne 0) { + $daemonHome = if ($env:BSK_HOME) { $env:BSK_HOME } else { Join-Path $HOME ".bsk" } + $daemonInfoPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath((Join-Path $daemonHome "daemon.json")) + throw "could not stop bsk daemon; installation was not changed. Stop any running bsk daemon, remove '$daemonInfoPath', then retry the installer." + } if ([System.IO.File]::Exists($Target)) { # PowerShell 5.1 converts $null to an empty path for string parameters. [System.IO.File]::Replace($staged, $Target, [NullString]::Value) @@ -140,7 +145,6 @@ function Install-Binary { else { [System.IO.File]::Move($staged, $Target) } - Write-Log "daemon will restart automatically on the next browser command" } finally { if ([System.IO.File]::Exists($staged)) { [System.IO.File]::Delete($staged) } @@ -193,13 +197,17 @@ function Main { } $tempDir = Join-Path ([System.IO.Path]::GetTempPath()) ([System.IO.Path]::GetRandomFileName()) - New-Item -ItemType Directory -Path $tempDir -Force | Out-Null + [System.IO.Directory]::CreateDirectory($tempDir) | Out-Null try { $archivePath = Join-Path $tempDir $archiveName Write-Log "downloading ${downloadUrl}" - Invoke-WebRequest -Uri $downloadUrl -OutFile $archivePath -UseBasicParsing -ErrorAction Stop + # PowerShell 5.1's -OutFile treats brackets as wildcards. Write raw HTTP bytes literally. + $response = Invoke-WebRequest -Uri $downloadUrl -UseBasicParsing -ErrorAction Stop + try { + [System.IO.File]::WriteAllBytes($archivePath, $response.RawContentStream.ToArray()) + } finally { $response.RawContentStream.Dispose() } if ($expectedSha) { Write-Log "verifying checksum" @@ -221,9 +229,7 @@ function Main { Write-Die "bsk.exe not found in archive" } - if (-not (Test-Path -LiteralPath $InstallDir)) { - New-Item -ItemType Directory -Path $InstallDir -Force | Out-Null - } + [System.IO.Directory]::CreateDirectory($InstallDir) | Out-Null Install-Binary -Source (Join-Path $tempDir "bsk.exe") -Target (Join-Path $InstallDir "bsk.exe") @@ -245,12 +251,13 @@ function Main { $command = Get-Command bsk -ErrorAction SilentlyContinue if (-not $command -or $command.CommandType -ne "Application" -or $command.Source -ine $bskPath) { - Write-Log "warning: 'bsk' does not resolve to $bskPath; check Get-Command bsk -All for a conflicting command" + Write-Log "warning: 'bsk' does not resolve to $bskPath in this session; check Get-Command bsk -All for a conflicting command" } Write-Log "done" Write-Host "" - Write-Host "Open a new terminal (PowerShell / Git Bash) for PATH changes to take full effect." + Write-Host "This PATH check covers the current session only; a new terminal may prefer a Machine PATH entry or alias." + Write-Host "In a new PowerShell terminal, run Get-Command bsk -All and confirm the first result is $bskPath." } finally { Remove-Item -LiteralPath $tempDir -Recurse -Force -ErrorAction SilentlyContinue diff --git a/scripts/install-windows.test.ps1 b/scripts/install-windows.test.ps1 index 0e3d793..9d70b84 100644 --- a/scripts/install-windows.test.ps1 +++ b/scripts/install-windows.test.ps1 @@ -24,9 +24,12 @@ foreach ($statement in $ast.EndBlock.Statements) { function Assert-Equal($Actual, $Expected) { if ($Actual -cne $Expected) { throw "expected [$Expected], got [$Actual]" } } -function Assert-Fails([scriptblock]$Action) { +function Assert-Fails([scriptblock]$Action, [string]$MessageContains) { $failed = $false - try { & $Action } catch { $failed = $true } + try { & $Action } catch { + $failed = $true + if ($MessageContains -and -not $_.Exception.Message.Contains($MessageContains)) { throw } + } if (-not $failed) { throw "expected failure" } } @@ -127,7 +130,7 @@ try { if ($LASTEXITCODE -ne 0) { throw "Git Bash failed" } Assert-Equal $actual ('/c' + $special.Substring(2).Replace('\', '/')) - $env:BSK_HOME = Join-Path $root "home" + $env:BSK_HOME = Join-Path $root "home [state]" $env:BSK_AUTO_UPDATE = "off" [IO.Directory]::CreateDirectory($env:BSK_HOME) | Out-Null $sourceDir = Join-Path $root "download" @@ -137,7 +140,8 @@ try { $targetDir = Join-Path $root "中文 space [x] & install" [IO.Directory]::CreateDirectory($targetDir) | Out-Null $target = Join-Path $targetDir "bsk.exe" - Install-Binary $source $target + $firstInstallOutput = Install-Binary $source $target 6>&1 | Out-String + if ($firstInstallOutput -match 'daemon will restart') { throw 'fresh install reported a daemon restart' } Assert-Equal (Get-FileHash -LiteralPath $target).Hash (Get-FileHash -LiteralPath $source).Hash $daemon = Start-Process -FilePath $target -ArgumentList @('daemon', 'start', '--foreground', '--port', '0') -WindowStyle Hidden -PassThru @@ -182,15 +186,19 @@ try { # Refuse replacement when daemon identity cannot be verified. [IO.File]::WriteAllText($infoPath, 'invalid daemon metadata') $before = (Get-FileHash -LiteralPath $target).Hash - Assert-Fails { Install-Binary $source $target } + Assert-Fails { Install-Binary $source $target } -MessageContains $infoPath Assert-Equal (Get-FileHash -LiteralPath $target).Hash $before # A failed stop must also prevent installation to a previously empty target. $blockedTarget = Join-Path $newTargetDir "blocked.exe" - Assert-Fails { Install-Binary $source $blockedTarget } + Assert-Fails { Install-Binary $source $blockedTarget } -MessageContains $infoPath if (Test-Path -LiteralPath $blockedTarget) { throw "failed stop created an installation" } if (@(Get-ChildItem -LiteralPath $newTargetDir -Filter "*.install-*").Count) { throw "failed stop leaked staging files" } + Assert-Equal ([IO.File]::ReadAllText($infoPath)) 'invalid daemon metadata' + # Following the reported recovery path must allow the same installation to succeed. [IO.File]::Delete($infoPath) + Install-Binary $source $blockedTarget + Assert-Equal (Get-FileHash -LiteralPath $blockedTarget).Hash (Get-FileHash -LiteralPath $source).Hash # A remaining lock must fail without truncation or staging debris. $lock = [IO.File]::Open($target, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::None) @@ -271,7 +279,11 @@ try { $script:UserPath = "$targetDir;$InstallDir;$($InstallDir.ToUpperInvariant())" $env:PATH = "$targetDir;$InstallDir;$oldPath" Assert-Equal (Get-Command bsk).Source $target - Main + $installOutput = Main 6>&1 | Out-String + if ($installOutput -notmatch 'current session only' -or + $installOutput -notmatch 'Machine PATH' -or $installOutput -notmatch 'Get-Command bsk -All') { + throw 'install did not explain PATH verification in a new terminal' + } Assert-Equal $downloads.Count 1 Assert-Equal (Get-Command bsk).Source $installed Assert-Equal $script:UserPath "$InstallDir;$targetDir"