ci / test (push) Waiting to run
ci / fuzz-smoke (push) Waiting to run
ci / windows-client (push) Waiting to run
ci / package-release (linux-x86_64, ubuntu-latest) (push) Waiting to run
ci / package-release (macos-aarch64, macos-14) (push) Waiting to run
ci / package-release (macos-x86_64, macos-13) (push) Waiting to run
ci / package-release (windows-x86_64, windows-latest) (push) Waiting to run
ci / publish-gitea-release (push) Blocked by required conditions
ci / remote-bench (push) Waiting to run
469 lines
15 KiB
PowerShell
469 lines
15 KiB
PowerShell
param(
|
|
[ValidateSet("client")]
|
|
[string]$Role = $(if ($env:DOSH_ROLE) { $env:DOSH_ROLE } else { "client" }),
|
|
[string]$Repo = $(if ($env:DOSH_REPO) { $env:DOSH_REPO } else { "https://git.palav.dev/Palav/dosh.git" }),
|
|
[string]$Server = $env:DOSH_SERVER,
|
|
[string]$DoshHost = $(if ($env:DOSH_HOST) { $env:DOSH_HOST } else { $env:DOSH_DOSH_HOST }),
|
|
[int]$Port = $(if ($env:DOSH_PORT) { [int]$env:DOSH_PORT } else { 50000 }),
|
|
[string]$Prefix = $(if ($env:PREFIX) { $env:PREFIX } else { Join-Path $HOME ".local" }),
|
|
[switch]$UsePrebuilt = $(-not $env:DOSH_USE_PREBUILT -or $env:DOSH_USE_PREBUILT -ne "0"),
|
|
[string]$BinaryUrl = $env:DOSH_BINARY_URL,
|
|
[string]$BinaryBase = $env:DOSH_BINARY_BASE,
|
|
[string]$BinaryName = $env:DOSH_BINARY_NAME,
|
|
[string]$BinaryVersion = $(if ($env:DOSH_BINARY_VERSION) { $env:DOSH_BINARY_VERSION } else { "latest" }),
|
|
[string]$UpdateCache = $(if ($env:DOSH_UPDATE_CACHE) { $env:DOSH_UPDATE_CACHE } else { Join-Path $HOME ".cache\dosh\source" }),
|
|
[switch]$BinaryRequired = $($env:DOSH_BINARY_REQUIRED -and $env:DOSH_BINARY_REQUIRED -ne "0"),
|
|
[switch]$ForceConfig
|
|
)
|
|
|
|
$ErrorActionPreference = "Stop"
|
|
|
|
function Require-Command($Name) {
|
|
if (-not (Get-Command $Name -ErrorAction SilentlyContinue)) {
|
|
throw "missing required command: $Name"
|
|
}
|
|
}
|
|
|
|
function Normalize-Arch {
|
|
switch ($env:PROCESSOR_ARCHITECTURE) {
|
|
"AMD64" { "x86_64"; break }
|
|
"ARM64" { "aarch64"; break }
|
|
default { $env:PROCESSOR_ARCHITECTURE.ToLowerInvariant() }
|
|
}
|
|
}
|
|
|
|
function Release-ArtifactName {
|
|
if ($BinaryName) {
|
|
return $BinaryName
|
|
}
|
|
"dosh-windows-$(Normalize-Arch).zip"
|
|
}
|
|
|
|
function Repo-WebBase($Value) {
|
|
if (-not $Value) {
|
|
return $null
|
|
}
|
|
$base = $Value.TrimEnd("/")
|
|
if ($base.EndsWith(".git")) {
|
|
$base = $base.Substring(0, $base.Length - 4)
|
|
}
|
|
if ($base -notmatch "^https?://") {
|
|
return $null
|
|
}
|
|
$base
|
|
}
|
|
|
|
function Release-DownloadUrl {
|
|
if ($BinaryUrl) {
|
|
return $BinaryUrl
|
|
}
|
|
$name = Release-ArtifactName
|
|
if ($BinaryBase) {
|
|
return "$($BinaryBase.TrimEnd('/'))/$name"
|
|
}
|
|
$web = Repo-WebBase $Repo
|
|
if (-not $web) {
|
|
return $null
|
|
}
|
|
if ($BinaryVersion -eq "latest") {
|
|
return "$web/releases/latest/download/$name"
|
|
}
|
|
"$web/releases/download/$BinaryVersion/$name"
|
|
}
|
|
|
|
function Release-LatestTagDownloadUrl {
|
|
if ($BinaryUrl -or $BinaryBase -or $BinaryVersion -ne "latest") {
|
|
return $null
|
|
}
|
|
$web = Repo-WebBase $Repo
|
|
if (-not $web) {
|
|
return $null
|
|
}
|
|
try {
|
|
$response = Invoke-WebRequest -UseBasicParsing -Uri "$web/releases/latest" -MaximumRedirection 5
|
|
$effective = $null
|
|
if ($response.BaseResponse.ResponseUri) {
|
|
$effective = $response.BaseResponse.ResponseUri.AbsoluteUri
|
|
} elseif ($response.BaseResponse.RequestMessage -and $response.BaseResponse.RequestMessage.RequestUri) {
|
|
$effective = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
|
|
}
|
|
$prefix = "$web/releases/tag/"
|
|
if ($effective -and $effective.StartsWith($prefix)) {
|
|
$tag = $effective.Substring($prefix.Length)
|
|
if ($tag) {
|
|
return "$web/releases/download/$tag/$(Release-ArtifactName)"
|
|
}
|
|
}
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Version-Core($Value) {
|
|
$base = $Value.TrimStart("v").Split("+")[0]
|
|
$base.Split("-")[0]
|
|
}
|
|
|
|
function Version-Prerelease($Value) {
|
|
$base = $Value.TrimStart("v").Split("+")[0]
|
|
$dash = $base.IndexOf("-")
|
|
if ($dash -lt 0) {
|
|
return ""
|
|
}
|
|
$base.Substring($dash + 1).ToLowerInvariant()
|
|
}
|
|
|
|
function Version-Parts($Value) {
|
|
[regex]::Matches((Version-Core $Value), "\d+") | ForEach-Object { [int64]$_.Value }
|
|
}
|
|
|
|
function Compare-Prerelease($Left, $Right) {
|
|
if (-not $Left -and -not $Right) {
|
|
return 0
|
|
}
|
|
if (-not $Left) {
|
|
return 1
|
|
}
|
|
if (-not $Right) {
|
|
return -1
|
|
}
|
|
if ($Left -eq $Right) {
|
|
return 0
|
|
}
|
|
if ($Left -match "(?i)^rc(\d+)$" -and $Right -match "(?i)^rc(\d+)$") {
|
|
$leftRc = [int64]([regex]::Match($Left, "(?i)^rc(\d+)$").Groups[1].Value)
|
|
$rightRc = [int64]([regex]::Match($Right, "(?i)^rc(\d+)$").Groups[1].Value)
|
|
return $leftRc.CompareTo($rightRc)
|
|
}
|
|
return [string]::CompareOrdinal($Left, $Right)
|
|
}
|
|
|
|
function Compare-DoshVersion($Left, $Right) {
|
|
$leftParts = @(Version-Parts $Left)
|
|
$rightParts = @(Version-Parts $Right)
|
|
$width = [Math]::Max($leftParts.Count, $rightParts.Count)
|
|
for ($i = 0; $i -lt $width; $i++) {
|
|
$l = if ($i -lt $leftParts.Count) { $leftParts[$i] } else { 0 }
|
|
$r = if ($i -lt $rightParts.Count) { $rightParts[$i] } else { 0 }
|
|
if ($l -lt $r) { return -1 }
|
|
if ($l -gt $r) { return 1 }
|
|
}
|
|
return Compare-Prerelease (Version-Prerelease $Left) (Version-Prerelease $Right)
|
|
}
|
|
|
|
function Current-SourceVersion {
|
|
if (Test-Path "Cargo.toml") {
|
|
$raw = Get-Content "Cargo.toml" -Raw
|
|
} else {
|
|
$web = Repo-WebBase $Repo
|
|
if (-not $web) {
|
|
return $null
|
|
}
|
|
try {
|
|
$raw = (Invoke-WebRequest -UseBasicParsing -Uri "$web/raw/branch/main/Cargo.toml").Content
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
}
|
|
if ($raw -match '(?m)^version = "([^"]+)"') {
|
|
return $Matches[1]
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Latest-ReleaseTag {
|
|
$web = Repo-WebBase $Repo
|
|
if (-not $web) {
|
|
return $null
|
|
}
|
|
try {
|
|
$response = Invoke-WebRequest -UseBasicParsing -Uri "$web/releases/latest" -MaximumRedirection 5
|
|
$effective = $null
|
|
if ($response.BaseResponse.ResponseUri) {
|
|
$effective = $response.BaseResponse.ResponseUri.AbsoluteUri
|
|
} elseif ($response.BaseResponse.RequestMessage -and $response.BaseResponse.RequestMessage.RequestUri) {
|
|
$effective = $response.BaseResponse.RequestMessage.RequestUri.AbsoluteUri
|
|
}
|
|
$prefix = "$web/releases/tag/"
|
|
if ($effective -and $effective.StartsWith($prefix)) {
|
|
return $effective.Substring($prefix.Length)
|
|
}
|
|
}
|
|
catch {
|
|
return $null
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Latest-ReleaseIsStale {
|
|
if ($BinaryUrl -or $BinaryBase -or $BinaryVersion -ne "latest") {
|
|
return $false
|
|
}
|
|
$latest = Latest-ReleaseTag
|
|
$current = Current-SourceVersion
|
|
if (-not $latest -or -not $current) {
|
|
return $false
|
|
}
|
|
$latestVersion = $latest.TrimStart("v")
|
|
if ((Compare-DoshVersion $latestVersion $current) -lt 0) {
|
|
Write-Warning "latest release $latestVersion is older than source $current; skipping stale prebuilt"
|
|
return $true
|
|
}
|
|
return $false
|
|
}
|
|
|
|
function Verify-ArchiveChecksum($Url, $Archive) {
|
|
$checksumPath = "$Archive.sha256"
|
|
try {
|
|
Invoke-WebRequest -UseBasicParsing -Uri "$Url.sha256" -OutFile $checksumPath
|
|
}
|
|
catch {
|
|
throw "prebuilt checksum unavailable; refusing unverified binary for $Url"
|
|
}
|
|
$expected = ((Get-Content $checksumPath -Raw).Trim() -split "\s+")[0].ToLowerInvariant()
|
|
$actual = (Get-FileHash -Algorithm SHA256 $Archive).Hash.ToLowerInvariant()
|
|
if ($expected -ne $actual) {
|
|
throw "prebuilt checksum mismatch for $Url"
|
|
}
|
|
}
|
|
|
|
function Expected-ArchiveVersion($Url) {
|
|
if ($Url -match "/releases/download/v([^/]+)/") {
|
|
return $Matches[1]
|
|
}
|
|
if (-not $BinaryUrl -and -not $BinaryBase -and $BinaryVersion -ne "latest") {
|
|
return $BinaryVersion.TrimStart("v")
|
|
}
|
|
return $null
|
|
}
|
|
|
|
function Verify-ArchiveVersion($ExtractDir, $Url) {
|
|
$expected = Expected-ArchiveVersion $Url
|
|
if (-not $expected) {
|
|
return
|
|
}
|
|
$versionFile = Get-ChildItem -Path $ExtractDir -Recurse -File -Filter VERSION | Select-Object -First 1
|
|
if (-not $versionFile) {
|
|
throw "prebuilt archive missing VERSION for $Url"
|
|
}
|
|
$actual = (Get-Content $versionFile.FullName -Raw).Trim()
|
|
if ($actual -ne $expected) {
|
|
throw "prebuilt archive version mismatch for ${Url}: expected $expected, got $actual"
|
|
}
|
|
}
|
|
|
|
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
|
|
try {
|
|
Move-Item $tmp $Destination -Force
|
|
}
|
|
catch {
|
|
Remove-Item $tmp -Force -ErrorAction SilentlyContinue
|
|
throw
|
|
}
|
|
}
|
|
|
|
function Normalize-PathForCompare($Path) {
|
|
[System.IO.Path]::GetFullPath($Path).TrimEnd(
|
|
[System.IO.Path]::DirectorySeparatorChar,
|
|
[System.IO.Path]::AltDirectorySeparatorChar
|
|
)
|
|
}
|
|
|
|
function Assert-NoRelativePathSegments($Path) {
|
|
foreach ($segment in ($Path -split '[\\/]')) {
|
|
if ($segment -eq "." -or $segment -eq "..") {
|
|
throw "refusing unsafe update cache path: $Path"
|
|
}
|
|
}
|
|
}
|
|
|
|
function Assert-SafeUpdateCache($Path) {
|
|
if (-not $Path) {
|
|
throw "refusing unsafe update cache path: $Path"
|
|
}
|
|
Assert-NoRelativePathSegments $Path
|
|
$full = Normalize-PathForCompare $Path
|
|
$root = Normalize-PathForCompare ([System.IO.Path]::GetPathRoot($full))
|
|
$homePath = Normalize-PathForCompare $HOME
|
|
$homeCache = Normalize-PathForCompare (Join-Path $HOME ".cache")
|
|
if ($full -eq $root -or $full -eq $homePath -or $full -eq $homeCache) {
|
|
throw "refusing unsafe update cache path: $Path"
|
|
}
|
|
$full
|
|
}
|
|
|
|
$bindir = Join-Path $Prefix "bin"
|
|
$configDir = Join-Path $HOME ".config\dosh"
|
|
New-Item -ItemType Directory -Force -Path $bindir, $configDir | Out-Null
|
|
|
|
function Install-Prebuilt {
|
|
if (Latest-ReleaseIsStale) {
|
|
return $false
|
|
}
|
|
$url = Release-LatestTagDownloadUrl
|
|
if (-not $url) {
|
|
$url = Release-DownloadUrl
|
|
}
|
|
if (-not $url) {
|
|
return $false
|
|
}
|
|
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("dosh-bin-" + [guid]::NewGuid())
|
|
$zip = Join-Path $tmp (Release-ArtifactName)
|
|
$extract = Join-Path $tmp "extract"
|
|
try {
|
|
New-Item -ItemType Directory -Force -Path $tmp, $extract | Out-Null
|
|
Write-Host "Trying Dosh prebuilt $(Release-ArtifactName)"
|
|
Invoke-WebRequest -UseBasicParsing -Uri $url -OutFile $zip
|
|
Verify-ArchiveChecksum $url $zip
|
|
Expand-Archive -Force -Path $zip -DestinationPath $extract
|
|
Verify-ArchiveVersion $extract $url
|
|
foreach ($bin in @("dosh-client.exe", "dosh-bench.exe")) {
|
|
$found = Get-ChildItem -Path $extract -Recurse -File -Filter $bin | Select-Object -First 1
|
|
if (-not $found) {
|
|
throw "prebuilt archive missing $bin"
|
|
}
|
|
Install-Binary $found.FullName (Join-Path $bindir $bin)
|
|
}
|
|
Install-Binary (Join-Path $bindir "dosh-client.exe") (Join-Path $bindir "dosh.exe")
|
|
return $true
|
|
}
|
|
catch {
|
|
Write-Warning "prebuilt install failed: $_"
|
|
return $false
|
|
}
|
|
finally {
|
|
if (Test-Path $tmp) {
|
|
Remove-Item -Recurse -Force $tmp
|
|
}
|
|
}
|
|
}
|
|
|
|
function Install-FromSource {
|
|
Require-Command cargo
|
|
if (Test-Path "Cargo.toml") {
|
|
$src = (Get-Location).Path
|
|
} else {
|
|
if (-not $Repo) {
|
|
throw "DOSH_REPO is required when running the installer from irm/iex"
|
|
}
|
|
Require-Command git
|
|
$sourceCache = Assert-SafeUpdateCache $UpdateCache
|
|
$parent = Split-Path -Parent $sourceCache
|
|
New-Item -ItemType Directory -Force -Path $parent | Out-Null
|
|
if (Test-Path (Join-Path $sourceCache ".git")) {
|
|
git -C $sourceCache remote set-url origin $Repo
|
|
git -C $sourceCache fetch --depth 1 origin main
|
|
git -C $sourceCache checkout -q -B main FETCH_HEAD
|
|
} else {
|
|
if (Test-Path $sourceCache) {
|
|
Remove-Item -Recurse -Force $sourceCache
|
|
}
|
|
git clone --depth 1 --branch main $Repo $sourceCache | Out-Null
|
|
}
|
|
$src = $sourceCache
|
|
}
|
|
|
|
try {
|
|
Push-Location $src
|
|
cargo build --release --bin dosh-client --bin dosh-bench
|
|
Install-Binary "target\release\dosh-client.exe" (Join-Path $bindir "dosh-client.exe")
|
|
Install-Binary "target\release\dosh-client.exe" (Join-Path $bindir "dosh.exe")
|
|
Install-Binary "target\release\dosh-bench.exe" (Join-Path $bindir "dosh-bench.exe")
|
|
}
|
|
finally {
|
|
Pop-Location
|
|
}
|
|
}
|
|
|
|
if ($UsePrebuilt) {
|
|
$ok = Install-Prebuilt
|
|
if (-not $ok) {
|
|
if ($BinaryRequired) {
|
|
throw "prebuilt install failed and DOSH_BINARY_REQUIRED=1"
|
|
}
|
|
Write-Host "Falling back to source build"
|
|
Install-FromSource
|
|
}
|
|
} else {
|
|
Install-FromSource
|
|
}
|
|
|
|
$clientConfig = Join-Path $configDir "client.toml"
|
|
if ($ForceConfig -or -not (Test-Path $clientConfig)) {
|
|
$defaultServer = if ($Server) { $Server } else { "user@example.com" }
|
|
$updateRepo = if ($Repo) { $Repo } else { "https://git.palav.dev/Palav/dosh.git" }
|
|
$doshHostLine = if ($DoshHost) { "dosh_host = `"$DoshHost`"" } else { "# dosh_host = `"public.example.com`"" }
|
|
@"
|
|
update_repo = "$updateRepo"
|
|
update_port = $Port
|
|
server = "$defaultServer"
|
|
$doshHostLine
|
|
ssh_auth_command = "~/.local/bin/dosh-auth"
|
|
# ssh_port = 22
|
|
dosh_port = $Port
|
|
default_session = "new"
|
|
reconnect_timeout_secs = 5
|
|
view_only = false
|
|
predict = true
|
|
predict_mode = "experimental"
|
|
cache_attach_tickets = true
|
|
credential_cache = "~/.local/share/dosh/credentials"
|
|
auth_preference = "native,ssh"
|
|
trust_on_first_use = false
|
|
native_auth_timeout_ms = 700
|
|
known_hosts = "~/.config/dosh/known_hosts"
|
|
identity_files = ["~/.ssh/id_ed25519"]
|
|
use_ssh_agent = true
|
|
forward_agent = false
|
|
escape_key = "^]"
|
|
"@ | Set-Content -NoNewline -Encoding utf8 $clientConfig
|
|
}
|
|
|
|
$hostsConfig = Join-Path $configDir "hosts.toml"
|
|
if ($ForceConfig -or -not (Test-Path $hostsConfig)) {
|
|
$defaultServer = if ($Server) { $Server } else { "user@example.com" }
|
|
$hostUdp = if ($DoshHost) { $DoshHost } else { $defaultServer }
|
|
@"
|
|
# Example:
|
|
# [server]
|
|
# ssh = "server"
|
|
# dosh_host = "server.example.com"
|
|
# port = 50000
|
|
# default_command = "tm"
|
|
# predict = true
|
|
|
|
[default]
|
|
ssh = "$defaultServer"
|
|
dosh_host = "$hostUdp"
|
|
port = $Port
|
|
predict = true
|
|
"@ | Set-Content -NoNewline -Encoding utf8 $hostsConfig
|
|
}
|
|
|
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
|
if (-not (($userPath -split ';') -contains $bindir)) {
|
|
[Environment]::SetEnvironmentVariable("Path", "$userPath;$bindir", "User")
|
|
}
|
|
|
|
Write-Host "Installed Dosh client to $bindir"
|
|
Write-Host "Configured UDP port $Port"
|
|
Write-Host ""
|
|
$displayServer = if ($Server) { $Server } else { "user@host" }
|
|
Write-Host "Client commands:"
|
|
Write-Host " $bindir\dosh.exe $displayServer"
|
|
Write-Host " $bindir\dosh.exe setup <ssh-alias>"
|
|
Write-Host " $bindir\dosh.exe update --check"
|
|
Write-Host ""
|
|
Write-Host "Client config:"
|
|
Write-Host " $configDir\client.toml"
|
|
Write-Host " $configDir\hosts.toml"
|
|
Write-Host ""
|
|
Write-Host "Open a new terminal for PATH changes to apply."
|