$ErrorActionPreference = 'Stop' Add-Type -AssemblyName System.Net.Http $RetryMax = 5 $arch = if ($env:PROCESSOR_ARCHITECTURE -eq 'ARM64') { 'arm64' } else { 'x64' } $bin = 'rtme.sh'; $exe = "$bin.exe"; $tmp = "$exe.part" $base = if ($env:RTME_INSTALL_BASE_URL) { $env:RTME_INSTALL_BASE_URL } else { 'https://rtme.sh/send' } $url = "$base/install.bin?os=windows&arch=$arch" $ua = 'rtme.sh-installer' function Fail([string]$m) { Write-Error $m; exit 1 } function New-Error([string]$message, [int]$status = 0, [bool]$localWrite = $false) { $e = [System.Exception]::new($message) if ($status) { $e | Add-Member NoteProperty StatusCode $status } if ($localWrite) { $e | Add-Member NoteProperty LocalWrite $true } $e } function Invoke-Retry([string]$what, [scriptblock]$op) { for ($attempt = 1; ; $attempt++) { try { return & $op } catch { $status = if ($_.Exception.PSObject.Properties['StatusCode']) { [int]$_.Exception.StatusCode } else { 0 } $retry = $_.Exception -is [System.Net.Http.HttpRequestException] -or $_.Exception -is [System.Threading.Tasks.TaskCanceledException] -or ($_.Exception -is [System.IO.IOException] -and -not ($_.Exception.PSObject.Properties['LocalWrite'] -and $_.Exception.LocalWrite)) -or ($status -ge 500 -and $status -lt 600) if (-not $retry -or $attempt -ge $RetryMax) { throw } $detail = if ($status) { "HTTP $status" } else { $_.Exception.Message } Write-Host "-> retrying $what ($(($attempt + 1))/$RetryMax): $detail" Start-Sleep -Seconds $attempt } } } $LookupError = { param([int]$status) if ($status -eq 404) { return "error: no binary for windows/$arch" } if ($status) { return "error: failed to check installer release metadata (HTTP $status)" } return 'error: failed to check installer release metadata' } $h = [System.Net.Http.HttpClientHandler]::new(); $h.AllowAutoRedirect = $false $c = [System.Net.Http.HttpClient]::new($h); $c.DefaultRequestHeaders.UserAgent.ParseAdd($ua) try { $version = (Invoke-Retry 'metadata lookup' { $r = $null try { $r = $c.GetAsync($url, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult() $status = [int]$r.StatusCode; $v = $null; $version = if ($r.Headers.TryGetValues('x-rtme-version', [ref]$v)) { @($v)[0] } else { $null } if ($status -ge 400) { throw (New-Error 'lookup failed' $status) } if (-not $version -or ($status -ge 300 -and -not $r.Headers.Location)) { throw (New-Error 'lookup failed') } $version } finally { if ($r) { $r.Dispose() } } }) } catch { $status = if ($_.Exception.PSObject.Properties['StatusCode']) { [int]$_.Exception.StatusCode } else { 0 }; Fail (& $LookupError $status) } finally { $c.Dispose(); $h.Dispose() } Write-Host "-> downloading $bin $version for windows/$arch..." Remove-Item $tmp -Force -ErrorAction SilentlyContinue $h = [System.Net.Http.HttpClientHandler]::new() $c = [System.Net.Http.HttpClient]::new($h); $c.Timeout = [TimeSpan]::FromMinutes(30); $c.DefaultRequestHeaders.UserAgent.ParseAdd($ua) $activity = "Downloading $bin $version" try { Invoke-Retry 'download' { $r = $s = $f = $null; $local = $false try { Remove-Item $tmp -Force -ErrorAction SilentlyContinue $r = $c.GetAsync($url, [System.Net.Http.HttpCompletionOption]::ResponseHeadersRead).GetAwaiter().GetResult() if (-not $r.IsSuccessStatusCode) { throw (New-Error 'download failed' ([int]$r.StatusCode)) } $s = $r.Content.ReadAsStreamAsync().GetAwaiter().GetResult() $local = $true; $f = [System.IO.File]::Open($tmp, [System.IO.FileMode]::Create, [System.IO.FileAccess]::Write, [System.IO.FileShare]::None); $local = $false $n = $r.Content.Headers.ContentLength; $b = [byte[]]::new(1MB); $w = 0L; $p = -1 while (($read = $s.Read($b, 0, $b.Length)) -gt 0) { $local = $true; $f.Write($b, 0, $read); $local = $false; $w += $read if ($n) { $q = [int][Math]::Min(100, [Math]::Floor($w * 100.0 / $n)) if ($q -ne $p) { Write-Progress -Activity $activity -Status "$([Math]::Round($w / 1MB, 1)) / $([Math]::Round($n / 1MB, 1)) MiB" -PercentComplete $q; $p = $q } } else { Write-Progress -Activity $activity -Status "$([Math]::Round($w / 1MB, 1)) MiB" } } } catch { Remove-Item $tmp -Force -ErrorAction SilentlyContinue if ($local -or $_.Exception -is [System.UnauthorizedAccessException] -or ($_.Exception.PSObject.Properties['LocalWrite'] -and $_.Exception.LocalWrite)) { throw (New-Error 'write failed' 0 $true) } throw } finally { if ($f) { $f.Dispose() }; if ($s) { $s.Dispose() }; if ($r) { $r.Dispose() }; Write-Progress -Activity $activity -Completed } } } catch { Remove-Item $tmp -Force -ErrorAction SilentlyContinue if ($_.Exception.PSObject.Properties['LocalWrite'] -and $_.Exception.LocalWrite) { Fail "error: failed to write .\$exe" } $status = if ($_.Exception.PSObject.Properties['StatusCode']) { [int]$_.Exception.StatusCode } else { 0 } if ($status) { Fail "error: failed to download $bin $version (HTTP $status)" } Fail "error: failed to download $bin $version" } finally { $c.Dispose(); $h.Dispose() } try { $old = $null if (Test-Path $exe -PathType Container) { throw 'directory target' } if (Test-Path $exe -PathType Leaf) { try { Remove-Item $exe -Force } catch { $old = ".\$bin.old-$([guid]::NewGuid().ToString('N').Substring(0, 8)).exe" Move-Item -LiteralPath $exe -Destination $old } } try { Move-Item -LiteralPath $tmp -Destination $exe } catch { if ($old -and (Test-Path $old) -and -not (Test-Path $exe)) { try { Move-Item -LiteralPath $old -Destination $exe } catch {} } throw } } catch { Remove-Item $tmp -Force -ErrorAction SilentlyContinue; Fail "error: failed to write .\$exe" } Write-Host "-> installed .\$bin" if ($old) { Write-Host "-> moved old running binary to $old" }