+ $tmpZip = Join-Path $RsyncDir "download-$RsyncAsset"\r
+ Invoke-WebRequest -Uri $RsyncUrl -OutFile $tmpZip -UseBasicParsing\r
+ Write-Host " Downloaded $RsyncAsset ($([math]::Round((Get-Item $tmpZip).Length / 1MB, 2)) MB)"\r
+\r
+ # Verify against the .sha256 published beside it. Same origin, so this is an\r
+ # integrity check on the transfer rather than a defence against a hostile\r
+ # release - but a truncated or proxy-mangled download is the failure that\r
+ # actually happens, and it fails here instead of mid-transfer later.\r
+ #\r
+ # -OutFile, not .Content: GitHub serves the .sha256 as\r
+ # application/octet-stream, and Invoke-WebRequest hands back a byte[] rather\r
+ # than a string for any non-text content type, so .Content would compare the\r
+ # first BYTE against the hash and fail on every correct download.\r
+ $tmpSha = "$tmpZip.sha256"\r
+ Invoke-WebRequest -Uri "$RsyncUrl.sha256" -OutFile $tmpSha -UseBasicParsing\r
+ $want = (((Get-Content $tmpSha -Raw) -split '\s+')[0]).Trim().ToLower()\r
+ Remove-Item $tmpSha -Force -ErrorAction SilentlyContinue\r
+ $got = (Get-FileHash $tmpZip -Algorithm SHA256).Hash.ToLower()\r
+ if ($want -and $want -ne $got) {\r
+ Remove-Item $tmpZip -Force\r
+ throw "SHA-256 mismatch for ${RsyncAsset}: expected $want, got $got"\r
+ }\r
+ Write-Host " SHA-256 verified: $got"\r
+\r
+ # Unpack to a scratch directory and move out the files we asked for, rather\r
+ # than expanding straight over the install directory: the zip is the unit\r
+ # that was checksummed, and this way a future release adding something to it\r
+ # cannot quietly drop that something onto the machine PATH.\r
+ $unpack = Join-Path $RsyncDir '.unpack'\r
+ if (Test-Path $unpack) { Remove-Item -Recurse -Force $unpack }\r
+ Expand-Archive -Path $tmpZip -DestinationPath $unpack -Force\r
+ Remove-Item $tmpZip -Force\r
+ foreach ($f in 'rsync.exe', 'ssh.exe', 'COPYING.txt', 'NOTICE-ssh.txt') {\r
+ $src = Join-Path $unpack $f\r
+ if (-not (Test-Path $src)) { continue }\r
+ if ($f -eq 'ssh.exe' -and -not $WantSsh) { continue }\r
+ Move-Item -Path $src -Destination (Join-Path $RsyncDir $f) -Force\r
+ }\r
+ Remove-Item -Recurse -Force $unpack\r
+\r
+ # Prove the unpacked ssh.exe actually starts, and DELETE it if it does not.\r
+ #\r
+ # This matters more than it looks. rsync.exe prefers an ssh.exe sitting in its\r
+ # own directory, so a present-but-unstartable one does not degrade to the\r
+ # in-box client - it breaks rsync outright, and the error you get is a remote\r
+ # shell that died rather than anything naming ssh.exe. The way to land there\r
+ # is a missing or too-old System32 libcrypto.dll. Removing it is the repair:\r
+ # rsync then falls back to the ssh on the PATH, which is the in-box client.\r
+ #\r
+ # This also used to fire on every ARM64 run, when the x64 asset was the only\r
+ # 64-bit one published and its ssh.exe could not load the ARM64 libcrypto\r
+ # (exit 0xC0000135, STATUS_DLL_NOT_FOUND). The arm64 asset fixed that at the\r
+ # source; the check stays because it is how that was found in the first place.\r
+ #\r
+ # EAP back to Continue for the call: ssh -V writes its version to STDERR, and\r
+ # under $ErrorActionPreference = 'Stop' a native command's stderr becomes a\r
+ # terminating error, so a WORKING client would look like a broken one.\r
+ # $global:LASTEXITCODE is cleared first because an exe that cannot start at\r
+ # all throws without setting one, and the stale 0 from the previous native\r
+ # command would otherwise read as success.\r
+ #\r
+ # DRAIN THE OUTPUT BEFORE READING $LASTEXITCODE. `... | Select-Object -First 1`\r
+ # was the original spelling here, and it is a trap: Select-Object stops the\r
+ # pipeline the instant it has its one object, and that is when ssh.exe has\r
+ # WRITTEN its banner - not when it has exited. PowerShell then tears the\r
+ # pipeline down, kills the still-running process, and reports -1. Measured on a\r
+ # healthy binary on an x64 box: 24 of 25 runs reported exit -1 through\r
+ # Select-Object, and 25 of 25 reported 0 when the output was collected first.\r
+ #\r
+ # So this check was deleting a perfectly good ssh.exe on almost every run, on\r
+ # every architecture, and the resulting fallback to the in-box client - the\r
+ # ~17MB/s one this whole step exists to avoid - looked like a property of the\r
+ # release rather than a bug in the test. Collect, THEN take the first line.\r
+ $SshExe = Join-Path $RsyncDir 'ssh.exe'\r
+ if ($WantSsh -and (Test-Path $SshExe)) {\r
+ $prevEap = $ErrorActionPreference\r
+ $ErrorActionPreference = 'Continue'\r
+ $global:LASTEXITCODE = $null\r
+ $sshVer = $null\r
+ try { $sshOut = (& $SshExe -V 2>&1); $sshVer = $sshOut | Select-Object -First 1 } catch { }\r
+ finally { $ErrorActionPreference = $prevEap }\r
+ if ($LASTEXITCODE -eq 0) {\r
+ Write-Host " ssh.exe runs: $sshVer"\r
+ } else {\r
+ $why = if ($null -eq $LASTEXITCODE) { 'it would not start' } else { "exit $LASTEXITCODE" }\r
+ Write-Warning "The release's ssh.exe does not run here ($why)$(if ($sshVer) { ": $sshVer" }). Removing it so rsync.exe falls back to the ssh on the PATH instead of failing on it."\r
+ Remove-Item $SshExe -Force -ErrorAction SilentlyContinue\r
+ $WantSsh = $false\r
+ }\r
+ }\r
+ Write-Host " Installed $RsyncExe$(if ($WantSsh) { ' and the ssh.exe it runs' })"\r