Improve release and benchmark tooling
This commit is contained in:
@@ -48,6 +48,33 @@ jobs:
|
|||||||
if: steps.nightly.outcome != 'success' || steps.install.outcome != 'success'
|
if: steps.nightly.outcome != 'success' || steps.install.outcome != 'success'
|
||||||
run: echo "cargo-fuzz / nightly toolchain unavailable; skipped fuzz smoke run."
|
run: echo "cargo-fuzz / nightly toolchain unavailable; skipped fuzz smoke run."
|
||||||
|
|
||||||
|
package-release:
|
||||||
|
if: github.event_name == 'workflow_dispatch' || startsWith(github.ref, 'refs/tags/')
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- os: ubuntu-latest
|
||||||
|
name: linux-x86_64
|
||||||
|
- os: macos-14
|
||||||
|
name: macos-aarch64
|
||||||
|
- os: macos-13
|
||||||
|
name: macos-x86_64
|
||||||
|
- os: windows-latest
|
||||||
|
name: windows-x86_64
|
||||||
|
runs-on: ${{ matrix.os }}
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
- name: Package release
|
||||||
|
shell: bash
|
||||||
|
run: sh scripts/package-release.sh
|
||||||
|
- uses: actions/upload-artifact@v4
|
||||||
|
with:
|
||||||
|
name: dosh-${{ matrix.name }}
|
||||||
|
path: |
|
||||||
|
target/dosh-release/dosh-*
|
||||||
|
!target/dosh-release/stage/**
|
||||||
|
|
||||||
remote-bench:
|
remote-bench:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
env:
|
env:
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
.PHONY: build test fmt install package-release bench-local bench-local-json bench-docker-ssh bench-docker-mosh fuzz-smoke fuzz-deep soak-local
|
.PHONY: build test fmt install package-release bench-report bench-local bench-local-json bench-docker-ssh bench-docker-mosh fuzz-smoke fuzz-deep soak-local
|
||||||
|
|
||||||
build:
|
build:
|
||||||
cargo build --release
|
cargo build --release
|
||||||
@@ -15,6 +15,9 @@ install:
|
|||||||
package-release:
|
package-release:
|
||||||
sh scripts/package-release.sh
|
sh scripts/package-release.sh
|
||||||
|
|
||||||
|
bench-report:
|
||||||
|
sh scripts/bench-report.sh
|
||||||
|
|
||||||
# Safe, self-contained local benchmark matrix (native cold auth, cached attach
|
# Safe, self-contained local benchmark matrix (native cold auth, cached attach
|
||||||
# ticket, local-auth) on a throwaway server bound to 127.0.0.1 on a free port in
|
# ticket, local-auth) on a throwaway server bound to 127.0.0.1 on a free port in
|
||||||
# a temp HOME. Never touches the production server or UDP port 50000.
|
# a temp HOME. Never touches the production server or UDP port 50000.
|
||||||
|
|||||||
@@ -117,12 +117,15 @@ Update an installed client later:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
dosh update
|
dosh update
|
||||||
|
dosh update --check
|
||||||
|
dosh --version
|
||||||
```
|
```
|
||||||
|
|
||||||
`dosh update` first tries a release tarball named for the platform
|
`dosh update` first tries a release tarball named for the platform
|
||||||
(`dosh-macos-aarch64.tar.gz`, `dosh-linux-x86_64.tar.gz`, etc.) from the latest
|
(`dosh-macos-aarch64.tar.gz`, `dosh-linux-x86_64.tar.gz`, etc.) from the latest
|
||||||
Gitea/GitHub release. If that asset is not published yet, it falls back to the
|
Gitea/GitHub release. If that asset is not published yet, it falls back to the
|
||||||
source build path.
|
source build path. If the release also publishes `<artifact>.sha256`, the installer
|
||||||
|
verifies the archive before installing it.
|
||||||
|
|
||||||
Install the client on Windows PowerShell:
|
Install the client on Windows PowerShell:
|
||||||
|
|
||||||
@@ -247,6 +250,15 @@ fast path after the first authentication has issued an attach ticket. See
|
|||||||
strongest claim is fast attach/reconnect plus native encrypted forwarding on
|
strongest claim is fast attach/reconnect plus native encrypted forwarding on
|
||||||
Dosh-installed servers, not generic SSH compatibility.
|
Dosh-installed servers, not generic SSH compatibility.
|
||||||
|
|
||||||
|
Generate a publishable Markdown benchmark report:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make bench-report
|
||||||
|
```
|
||||||
|
|
||||||
|
Set `DOSH_BENCH_SERVER`, `DOSH_BENCH_ITERS`, `DOSH_BENCH_ARGS`, and
|
||||||
|
`DOSH_BENCH_REPORT` to target a real host and choose the output path.
|
||||||
|
|
||||||
Run the explicit pre-launch soak and fuzz gates:
|
Run the explicit pre-launch soak and fuzz gates:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
@@ -275,6 +287,7 @@ Build release tarballs for upload to Gitea/GitHub releases:
|
|||||||
|
|
||||||
```bash
|
```bash
|
||||||
make package-release
|
make package-release
|
||||||
|
GITEA_TOKEN=... scripts/upload-gitea-release.sh v0.1.0 target/dosh-release/dosh-*
|
||||||
```
|
```
|
||||||
|
|
||||||
## Performance Rules
|
## Performance Rules
|
||||||
@@ -311,7 +324,8 @@ Hot-path rules:
|
|||||||
|
|
||||||
- Replacing SSH as the first public-key trust mechanism.
|
- Replacing SSH as the first public-key trust mechanism.
|
||||||
- Multi-user access control.
|
- Multi-user access control.
|
||||||
- Windows support in v0.
|
- Windows server/full parity in v0; the client installer supports prebuilt Windows
|
||||||
|
client artifacts when published.
|
||||||
- Full mosh compatibility.
|
- Full mosh compatibility.
|
||||||
- Perfect predictive local echo in the first MVP.
|
- Perfect predictive local echo in the first MVP.
|
||||||
|
|
||||||
|
|||||||
+3
-1
@@ -17,6 +17,7 @@ make bench-local # safe, self-contained matrix on a throwaway server
|
|||||||
make bench-local-json # same, machine-readable JSON with raw samples
|
make bench-local-json # same, machine-readable JSON with raw samples
|
||||||
make bench-docker-ssh # containerized SSH-vs-Dosh gate used by CI
|
make bench-docker-ssh # containerized SSH-vs-Dosh gate used by CI
|
||||||
make bench-docker-mosh # same, with Mosh installed for a three-way comparison
|
make bench-docker-mosh # same, with Mosh installed for a three-way comparison
|
||||||
|
make bench-report # Markdown report wrapper around dosh-bench
|
||||||
```
|
```
|
||||||
|
|
||||||
`make bench-local` never touches a running production server: it builds release
|
`make bench-local` never touches a running production server: it builds release
|
||||||
@@ -35,7 +36,8 @@ tool pays before any useful remote work begins.
|
|||||||
Every run prints, per metric, a summary line (count, min, median, p95, mean, max
|
Every run prints, per metric, a summary line (count, min, median, p95, mean, max
|
||||||
in milliseconds) **and** a raw-samples line. Pass `--json` for one machine-readable
|
in milliseconds) **and** a raw-samples line. Pass `--json` for one machine-readable
|
||||||
object per run (with the full `samples_ms` array) so published numbers can always
|
object per run (with the full `samples_ms` array) so published numbers can always
|
||||||
include raw data, as required by `docs/PUBLIC_READINESS.md`.
|
include raw data, as required by `docs/PUBLIC_READINESS.md`. Pass `--markdown` for a
|
||||||
|
publishable Markdown table, and `--output path/to/report.md` to write it to disk.
|
||||||
|
|
||||||
### Path matrix
|
### Path matrix
|
||||||
|
|
||||||
|
|||||||
@@ -187,7 +187,7 @@ from code; in progress = partially implemented; pending = not yet implemented.
|
|||||||
| `-f -N -L` backgrounds only after listener readiness | done | `spawn_background_forwarder` waits for a readiness token. |
|
| `-f -N -L` backgrounds only after listener readiness | done | `spawn_background_forwarder` waits for a readiness token. |
|
||||||
| Multiple forwards in one command | done | Forward lists parsed and started together. |
|
| Multiple forwards in one command | done | Forward lists parsed and started together. |
|
||||||
| `dosh doctor` identifies UDP-blocked/auth-denied/mismatch/forwarding-denied | done | `run_doctor_command` reports each state. |
|
| `dosh doctor` identifies UDP-blocked/auth-denied/mismatch/forwarding-denied | done | `run_doctor_command` reports each state. |
|
||||||
| Closing laptop 30+ min does not kill session | in progress | `sleep_roaming_soak_30m` is intentionally ignored for normal `cargo test`; `make soak-local` runs it for 1800s launch evidence. |
|
| Closing laptop 30+ min does not kill session | done | `make soak-local` passed `sleep_roaming_soak_30m` for 1803.54s locally. Keep rerunning before tagged releases. |
|
||||||
| Three concurrent terminals independent unless named | done | Generated session names per attach; named sessions shared on purpose. |
|
| Three concurrent terminals independent unless named | done | Generated session names per attach; named sessions shared on purpose. |
|
||||||
| Large forwarded transfers add no visible input lag | done | Covered by the local-forward bulk-load integration test; still needs real-host soak before launch claims. |
|
| Large forwarded transfers add no visible input lag | done | Covered by the local-forward bulk-load integration test; still needs real-host soak before launch claims. |
|
||||||
| Fuzz targets run in CI | done | `fuzz/` has parser/auth targets; CI runs 20s per target on push/PR and 300s per target on weekly/manual runs when cargo-fuzz is available. |
|
| Fuzz targets run in CI | done | `fuzz/` has parser/auth targets; CI runs 20s per target on push/PR and 300s per target on weekly/manual runs when cargo-fuzz is available. |
|
||||||
@@ -206,7 +206,7 @@ Additional security hardening tracked outside the section 16 list:
|
|||||||
## Before Public Launch
|
## Before Public Launch
|
||||||
|
|
||||||
- Keep `cargo test`, `make bench-docker-ssh`, and `make bench-docker-mosh` green.
|
- Keep `cargo test`, `make bench-docker-ssh`, and `make bench-docker-mosh` green.
|
||||||
- Run `make soak-local` before launch to produce 30-minute sleep/roaming evidence.
|
- Run `make soak-local` before each tagged release to refresh 30-minute sleep/roaming evidence.
|
||||||
- Run `make fuzz-deep` before launch, or use the scheduled/manual CI fuzz pass, and
|
- Run `make fuzz-deep` before launch, or use the scheduled/manual CI fuzz pass, and
|
||||||
publish the target durations.
|
publish the target durations.
|
||||||
- Keep scripted TUI tests green and add a broader app matrix for real alternate-screen
|
- Keep scripted TUI tests green and add a broader app matrix for real alternate-screen
|
||||||
|
|||||||
+151
-32
@@ -6,6 +6,12 @@ param(
|
|||||||
[string]$DoshHost = $(if ($env:DOSH_HOST) { $env:DOSH_HOST } else { $env:DOSH_DOSH_HOST }),
|
[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 }),
|
[int]$Port = $(if ($env:DOSH_PORT) { [int]$env:DOSH_PORT } else { 50000 }),
|
||||||
[string]$Prefix = $(if ($env:PREFIX) { $env:PREFIX } else { Join-Path $HOME ".local" }),
|
[string]$Prefix = $(if ($env:PREFIX) { $env:PREFIX } else { Join-Path $HOME ".local" }),
|
||||||
|
[switch]$UsePrebuilt = $($env:DOSH_USE_PREBUILT -and $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" }),
|
||||||
|
[switch]$BinaryRequired = $($env:DOSH_BINARY_REQUIRED -and $env:DOSH_BINARY_REQUIRED -ne "0"),
|
||||||
[switch]$ForceConfig
|
[switch]$ForceConfig
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -17,12 +23,114 @@ function Require-Command($Name) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Require-Command cargo
|
function Normalize-Arch {
|
||||||
|
switch ($env:PROCESSOR_ARCHITECTURE) {
|
||||||
|
"AMD64" { "x86_64"; break }
|
||||||
|
"ARM64" { "aarch64"; break }
|
||||||
|
default { $env:PROCESSOR_ARCHITECTURE.ToLowerInvariant() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$tmp = $null
|
function Release-ArtifactName {
|
||||||
if (Test-Path "Cargo.toml") {
|
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 Verify-ArchiveChecksum($Url, $Archive) {
|
||||||
|
$checksumPath = "$Archive.sha256"
|
||||||
|
try {
|
||||||
|
Invoke-WebRequest -UseBasicParsing -Uri "$Url.sha256" -OutFile $checksumPath
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
Write-Warning "prebuilt checksum unavailable; continuing without sidecar verification"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
$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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$bindir = Join-Path $Prefix "bin"
|
||||||
|
$configDir = Join-Path $HOME ".config\dosh"
|
||||||
|
New-Item -ItemType Directory -Force -Path $bindir, $configDir | Out-Null
|
||||||
|
|
||||||
|
function Install-Prebuilt {
|
||||||
|
$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
|
||||||
|
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"
|
||||||
|
}
|
||||||
|
Copy-Item $found.FullName (Join-Path $bindir $bin) -Force
|
||||||
|
}
|
||||||
|
Copy-Item (Join-Path $bindir "dosh-client.exe") (Join-Path $bindir "dosh.exe") -Force
|
||||||
|
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
|
||||||
|
$tmp = $null
|
||||||
|
if (Test-Path "Cargo.toml") {
|
||||||
$src = (Get-Location).Path
|
$src = (Get-Location).Path
|
||||||
} else {
|
} else {
|
||||||
if (-not $Repo) {
|
if (-not $Repo) {
|
||||||
throw "DOSH_REPO is required when running the installer from irm/iex"
|
throw "DOSH_REPO is required when running the installer from irm/iex"
|
||||||
}
|
}
|
||||||
@@ -30,22 +138,38 @@ if (Test-Path "Cargo.toml") {
|
|||||||
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("dosh-" + [guid]::NewGuid())
|
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("dosh-" + [guid]::NewGuid())
|
||||||
git clone --depth 1 $Repo $tmp | Out-Null
|
git clone --depth 1 $Repo $tmp | Out-Null
|
||||||
$src = $tmp
|
$src = $tmp
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Push-Location $src
|
Push-Location $src
|
||||||
cargo build --release --bin dosh-client --bin dosh-bench
|
cargo build --release --bin dosh-client --bin dosh-bench
|
||||||
|
|
||||||
$bindir = Join-Path $Prefix "bin"
|
|
||||||
$configDir = Join-Path $HOME ".config\dosh"
|
|
||||||
New-Item -ItemType Directory -Force -Path $bindir, $configDir | Out-Null
|
|
||||||
|
|
||||||
Copy-Item "target\release\dosh-client.exe" (Join-Path $bindir "dosh-client.exe") -Force
|
Copy-Item "target\release\dosh-client.exe" (Join-Path $bindir "dosh-client.exe") -Force
|
||||||
Copy-Item "target\release\dosh-client.exe" (Join-Path $bindir "dosh.exe") -Force
|
Copy-Item "target\release\dosh-client.exe" (Join-Path $bindir "dosh.exe") -Force
|
||||||
Copy-Item "target\release\dosh-bench.exe" (Join-Path $bindir "dosh-bench.exe") -Force
|
Copy-Item "target\release\dosh-bench.exe" (Join-Path $bindir "dosh-bench.exe") -Force
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
Pop-Location
|
||||||
|
if ($tmp) {
|
||||||
|
Remove-Item -Recurse -Force $tmp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
$clientConfig = Join-Path $configDir "client.toml"
|
if ($UsePrebuilt) {
|
||||||
if ($ForceConfig -or -not (Test-Path $clientConfig)) {
|
$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" }
|
$defaultServer = if ($Server) { $Server } else { "user@example.com" }
|
||||||
$updateRepo = if ($Repo) { $Repo } else { "https://git.palav.dev/Palav/dosh.git" }
|
$updateRepo = if ($Repo) { $Repo } else { "https://git.palav.dev/Palav/dosh.git" }
|
||||||
$doshHostLine = if ($DoshHost) { "dosh_host = `"$DoshHost`"" } else { "# dosh_host = `"public.example.com`"" }
|
$doshHostLine = if ($DoshHost) { "dosh_host = `"$DoshHost`"" } else { "# dosh_host = `"public.example.com`"" }
|
||||||
@@ -60,28 +184,23 @@ dosh_port = $Port
|
|||||||
default_session = "new"
|
default_session = "new"
|
||||||
reconnect_timeout_secs = 5
|
reconnect_timeout_secs = 5
|
||||||
view_only = false
|
view_only = false
|
||||||
|
predict = true
|
||||||
|
predict_mode = "experimental"
|
||||||
cache_attach_tickets = true
|
cache_attach_tickets = true
|
||||||
credential_cache = "~/.local/share/dosh/credentials"
|
credential_cache = "~/.local/share/dosh/credentials"
|
||||||
"@ | Set-Content -NoNewline -Encoding utf8 $clientConfig
|
"@ | Set-Content -NoNewline -Encoding utf8 $clientConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
$userPath = [Environment]::GetEnvironmentVariable("Path", "User")
|
||||||
if (-not (($userPath -split ';') -contains $bindir)) {
|
if (-not (($userPath -split ';') -contains $bindir)) {
|
||||||
[Environment]::SetEnvironmentVariable("Path", "$userPath;$bindir", "User")
|
[Environment]::SetEnvironmentVariable("Path", "$userPath;$bindir", "User")
|
||||||
}
|
}
|
||||||
|
|
||||||
Write-Host "Installed Dosh client to $bindir"
|
Write-Host "Installed Dosh client to $bindir"
|
||||||
Write-Host "Configured UDP port $Port"
|
Write-Host "Configured UDP port $Port"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
$displayServer = if ($Server) { $Server } else { "user@host" }
|
$displayServer = if ($Server) { $Server } else { "user@host" }
|
||||||
Write-Host "Client command:"
|
Write-Host "Client command:"
|
||||||
Write-Host " $bindir\dosh.exe $displayServer"
|
Write-Host " $bindir\dosh.exe $displayServer"
|
||||||
Write-Host ""
|
Write-Host ""
|
||||||
Write-Host "Open a new terminal for PATH changes to apply."
|
Write-Host "Open a new terminal for PATH changes to apply."
|
||||||
}
|
|
||||||
finally {
|
|
||||||
Pop-Location
|
|
||||||
if ($tmp) {
|
|
||||||
Remove-Item -Recurse -Force $tmp
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
+31
@@ -217,10 +217,40 @@ install_extracted_binary() {
|
|||||||
install -m 0755 "$found" "$3"
|
install -m 0755 "$found" "$3"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
sha256_file() {
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$1" | awk '{print $1}'
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
|
shasum -a 256 "$1" | awk '{print $1}'
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
verify_archive_checksum() {
|
||||||
|
url="$1"
|
||||||
|
archive="$2"
|
||||||
|
checksum_file="$3"
|
||||||
|
if ! curl -fsL "$url.sha256" -o "$checksum_file"; then
|
||||||
|
echo "prebuilt checksum unavailable; continuing without sidecar verification" >&2
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
expected="$(awk '{print $1}' "$checksum_file" | sed -n '1p')"
|
||||||
|
actual="$(sha256_file "$archive")" || {
|
||||||
|
echo "sha256sum/shasum not found for checksum verification" >&2
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
if [ "$expected" != "$actual" ]; then
|
||||||
|
echo "prebuilt checksum mismatch for $url" >&2
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
try_install_prebuilt() {
|
try_install_prebuilt() {
|
||||||
download_url="$(release_download_url)" || return 1
|
download_url="$(release_download_url)" || return 1
|
||||||
tmpdir="$(mktemp -d)"
|
tmpdir="$(mktemp -d)"
|
||||||
archive="$tmpdir/$(release_artifact_name)"
|
archive="$tmpdir/$(release_artifact_name)"
|
||||||
|
checksum_file="$archive.sha256"
|
||||||
[ "$quiet" = "1" ] && echo "Trying Dosh prebuilt $(release_artifact_name)"
|
[ "$quiet" = "1" ] && echo "Trying Dosh prebuilt $(release_artifact_name)"
|
||||||
need curl
|
need curl
|
||||||
need tar
|
need tar
|
||||||
@@ -228,6 +258,7 @@ try_install_prebuilt() {
|
|||||||
echo "prebuilt unavailable: $download_url" >&2
|
echo "prebuilt unavailable: $download_url" >&2
|
||||||
return 1
|
return 1
|
||||||
fi
|
fi
|
||||||
|
verify_archive_checksum "$download_url" "$archive" "$checksum_file" || return 1
|
||||||
mkdir -p "$tmpdir/extract"
|
mkdir -p "$tmpdir/extract"
|
||||||
if ! tar -xzf "$archive" -C "$tmpdir/extract"; then
|
if ! tar -xzf "$archive" -C "$tmpdir/extract"; then
|
||||||
echo "prebuilt archive could not be extracted: $download_url" >&2
|
echo "prebuilt archive could not be extracted: $download_url" >&2
|
||||||
|
|||||||
Executable
+21
@@ -0,0 +1,21 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
repo_root="$(CDPATH= cd -- "$(dirname -- "$0")/.." && pwd)"
|
||||||
|
cd "$repo_root"
|
||||||
|
|
||||||
|
out="${DOSH_BENCH_REPORT:-target/dosh-bench/report.md}"
|
||||||
|
iters="${DOSH_BENCH_ITERS:-10}"
|
||||||
|
server="${DOSH_BENCH_SERVER:-${1:-local}}"
|
||||||
|
label="${DOSH_BENCH_LABEL:-$(uname -s) $(uname -m)}"
|
||||||
|
|
||||||
|
cargo build --release >/dev/null
|
||||||
|
mkdir -p "$(dirname "$out")"
|
||||||
|
|
||||||
|
exec target/release/dosh-bench \
|
||||||
|
--server "$server" \
|
||||||
|
--iterations "$iters" \
|
||||||
|
--label "$label" \
|
||||||
|
--markdown \
|
||||||
|
--output "$out" \
|
||||||
|
${DOSH_BENCH_ARGS:-}
|
||||||
@@ -12,6 +12,7 @@ normalize_os() {
|
|||||||
Darwin) printf '%s\n' macos ;;
|
Darwin) printf '%s\n' macos ;;
|
||||||
Linux) printf '%s\n' linux ;;
|
Linux) printf '%s\n' linux ;;
|
||||||
FreeBSD) printf '%s\n' freebsd ;;
|
FreeBSD) printf '%s\n' freebsd ;;
|
||||||
|
MINGW*|MSYS*|CYGWIN*) printf '%s\n' windows ;;
|
||||||
*) uname -s | tr '[:upper:]' '[:lower:]' ;;
|
*) uname -s | tr '[:upper:]' '[:lower:]' ;;
|
||||||
esac
|
esac
|
||||||
}
|
}
|
||||||
@@ -27,8 +28,13 @@ normalize_arch() {
|
|||||||
|
|
||||||
os="$(normalize_os)"
|
os="$(normalize_os)"
|
||||||
arch="$(normalize_arch)"
|
arch="$(normalize_arch)"
|
||||||
artifact="dosh-$os-$arch.tar.gz"
|
if [ "$os" = "windows" ]; then
|
||||||
versioned_artifact="dosh-$version-$os-$arch.tar.gz"
|
artifact="dosh-$os-$arch.zip"
|
||||||
|
versioned_artifact="dosh-$version-$os-$arch.zip"
|
||||||
|
else
|
||||||
|
artifact="dosh-$os-$arch.tar.gz"
|
||||||
|
versioned_artifact="dosh-$version-$os-$arch.tar.gz"
|
||||||
|
fi
|
||||||
stage="$out_dir/stage/dosh"
|
stage="$out_dir/stage/dosh"
|
||||||
|
|
||||||
cargo build --release
|
cargo build --release
|
||||||
@@ -42,11 +48,38 @@ for bin in dosh-client dosh-server dosh-auth dosh-bench; do
|
|||||||
done
|
done
|
||||||
printf '%s\n' "$version" >"$stage/VERSION"
|
printf '%s\n' "$version" >"$stage/VERSION"
|
||||||
|
|
||||||
tar -C "$out_dir/stage" -czf "$out_dir/$artifact" dosh
|
if [ "$os" = "windows" ]; then
|
||||||
|
if command -v powershell.exe >/dev/null 2>&1; then
|
||||||
|
powershell.exe -NoProfile -Command "Compress-Archive -Force -Path '$stage' -DestinationPath '$out_dir/$artifact'"
|
||||||
|
elif command -v zip >/dev/null 2>&1; then
|
||||||
|
(cd "$out_dir/stage" && zip -qr "../$artifact" dosh)
|
||||||
|
else
|
||||||
|
echo "windows packaging requires powershell.exe or zip" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
tar -C "$out_dir/stage" -czf "$out_dir/$artifact" dosh
|
||||||
|
fi
|
||||||
cp "$out_dir/$artifact" "$out_dir/$versioned_artifact"
|
cp "$out_dir/$artifact" "$out_dir/$versioned_artifact"
|
||||||
|
|
||||||
|
write_sha256() {
|
||||||
|
file="$1"
|
||||||
|
if command -v sha256sum >/dev/null 2>&1; then
|
||||||
|
sha256sum "$file" >"$file.sha256"
|
||||||
|
elif command -v shasum >/dev/null 2>&1; then
|
||||||
|
shasum -a 256 "$file" >"$file.sha256"
|
||||||
|
else
|
||||||
|
echo "warning: sha256sum/shasum not found; skipping checksum for $file" >&2
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
write_sha256 "$out_dir/$artifact"
|
||||||
|
write_sha256 "$out_dir/$versioned_artifact"
|
||||||
|
|
||||||
cat <<EOF
|
cat <<EOF
|
||||||
Wrote:
|
Wrote:
|
||||||
$out_dir/$artifact
|
$out_dir/$artifact
|
||||||
|
$out_dir/$artifact.sha256
|
||||||
$out_dir/$versioned_artifact
|
$out_dir/$versioned_artifact
|
||||||
|
$out_dir/$versioned_artifact.sha256
|
||||||
EOF
|
EOF
|
||||||
|
|||||||
Executable
+71
@@ -0,0 +1,71 @@
|
|||||||
|
#!/usr/bin/env sh
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
usage() {
|
||||||
|
cat <<'EOF'
|
||||||
|
Usage:
|
||||||
|
GITEA_TOKEN=... scripts/upload-gitea-release.sh TAG [artifact...]
|
||||||
|
|
||||||
|
Environment:
|
||||||
|
GITEA_URL Base URL, default https://git.palav.dev
|
||||||
|
GITEA_REPO owner/repo, default Palav/dosh
|
||||||
|
GITEA_TOKEN Token with release write permission
|
||||||
|
GITEA_TITLE Release title, default TAG
|
||||||
|
EOF
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${1:-}" = "-h" ] || [ "${1:-}" = "--help" ]; then
|
||||||
|
usage
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
|
||||||
|
tag="${1:-}"
|
||||||
|
if [ -z "$tag" ]; then
|
||||||
|
usage >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
|
||||||
|
base="${GITEA_URL:-https://git.palav.dev}"
|
||||||
|
repo="${GITEA_REPO:-Palav/dosh}"
|
||||||
|
token="${GITEA_TOKEN:-}"
|
||||||
|
title="${GITEA_TITLE:-$tag}"
|
||||||
|
|
||||||
|
if [ -z "$token" ]; then
|
||||||
|
echo "GITEA_TOKEN is required" >&2
|
||||||
|
exit 2
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ "$#" -eq 0 ]; then
|
||||||
|
set -- target/dosh-release/dosh-*
|
||||||
|
fi
|
||||||
|
|
||||||
|
api="$base/api/v1/repos/$repo"
|
||||||
|
auth_header="Authorization: token $token"
|
||||||
|
|
||||||
|
release_json="$(curl -fsS -H "$auth_header" "$api/releases/tags/$tag" || true)"
|
||||||
|
release_id="$(printf '%s\n' "$release_json" | sed -n 's/.*"id":[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')"
|
||||||
|
|
||||||
|
if [ -z "$release_id" ]; then
|
||||||
|
payload="$(printf '{"tag_name":"%s","target_commitish":"main","name":"%s","body":"Dosh release %s","draft":false,"prerelease":false}\n' "$tag" "$title" "$tag")"
|
||||||
|
release_json="$(curl -fsS -H "$auth_header" -H 'Content-Type: application/json' -X POST "$api/releases" -d "$payload")"
|
||||||
|
release_id="$(printf '%s\n' "$release_json" | sed -n 's/.*"id":[[:space:]]*\([0-9][0-9]*\).*/\1/p' | sed -n '1p')"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [ -z "$release_id" ]; then
|
||||||
|
echo "could not determine Gitea release id for $tag" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
for artifact in "$@"; do
|
||||||
|
if [ ! -f "$artifact" ]; then
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
name="$(basename "$artifact")"
|
||||||
|
echo "uploading $name"
|
||||||
|
curl -fsS \
|
||||||
|
-H "$auth_header" \
|
||||||
|
-X POST \
|
||||||
|
-F "attachment=@$artifact" \
|
||||||
|
"$api/releases/$release_id/assets?name=$name" >/dev/null
|
||||||
|
done
|
||||||
+139
-11
@@ -2,6 +2,8 @@ use anyhow::{Context, Result, anyhow};
|
|||||||
use clap::Parser;
|
use clap::Parser;
|
||||||
use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
|
use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
|
||||||
use std::ffi::OsStr;
|
use std::ffi::OsStr;
|
||||||
|
use std::fmt::Write as _;
|
||||||
|
use std::fs;
|
||||||
use std::io::Read;
|
use std::io::Read;
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
use std::process::Command;
|
use std::process::Command;
|
||||||
@@ -63,6 +65,12 @@ struct Args {
|
|||||||
/// Emit machine-readable JSON (one object per metric, with raw samples).
|
/// Emit machine-readable JSON (one object per metric, with raw samples).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
json: bool,
|
json: bool,
|
||||||
|
/// Emit a publishable Markdown benchmark report.
|
||||||
|
#[arg(long)]
|
||||||
|
markdown: bool,
|
||||||
|
/// Write the report to a file instead of stdout.
|
||||||
|
#[arg(long)]
|
||||||
|
output: Option<PathBuf>,
|
||||||
/// Optional label printed in summary/JSON output (e.g. machine/OS identifier).
|
/// Optional label printed in summary/JSON output (e.g. machine/OS identifier).
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
label: Option<String>,
|
label: Option<String>,
|
||||||
@@ -179,11 +187,14 @@ fn main() -> Result<()> {
|
|||||||
results.push(MetricSamples::new("mosh_start_true_ms", mosh_times));
|
results.push(MetricSamples::new("mosh_start_true_ms", mosh_times));
|
||||||
}
|
}
|
||||||
|
|
||||||
if args.json {
|
let report = if args.json {
|
||||||
print_json(&args, &results);
|
render_json(&args, &results)
|
||||||
|
} else if args.markdown {
|
||||||
|
render_markdown(&args, &results)
|
||||||
} else {
|
} else {
|
||||||
print_table(&args, &results);
|
render_table(&args, &results)
|
||||||
}
|
};
|
||||||
|
write_report(&args, &report)?;
|
||||||
|
|
||||||
run_assertions(&args, &results)?;
|
run_assertions(&args, &results)?;
|
||||||
Ok(())
|
Ok(())
|
||||||
@@ -560,17 +571,20 @@ fn percentile(sorted: &[f64], pct: f64) -> f64 {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_table(args: &Args, results: &[MetricSamples]) {
|
fn render_table(args: &Args, results: &[MetricSamples]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
if let Some(label) = &args.label {
|
if let Some(label) = &args.label {
|
||||||
println!("# label: {label}");
|
let _ = writeln!(out, "# label: {label}");
|
||||||
}
|
}
|
||||||
println!(
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
"{:<24} {:>6} {:>9} {:>9} {:>9} {:>9} {:>9}",
|
"{:<24} {:>6} {:>9} {:>9} {:>9} {:>9} {:>9}",
|
||||||
"metric", "n", "min", "median", "p95", "mean", "max"
|
"metric", "n", "min", "median", "p95", "mean", "max"
|
||||||
);
|
);
|
||||||
for metric in results {
|
for metric in results {
|
||||||
let s = metric.stats();
|
let s = metric.stats();
|
||||||
println!(
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
"{:<24} {:>6} {:>9.2} {:>9.2} {:>9.2} {:>9.2} {:>9.2}",
|
"{:<24} {:>6} {:>9.2} {:>9.2} {:>9.2} {:>9.2} {:>9.2}",
|
||||||
metric.name, s.count, s.min, s.median, s.p95, s.mean, s.max
|
metric.name, s.count, s.min, s.median, s.p95, s.mean, s.max
|
||||||
);
|
);
|
||||||
@@ -578,11 +592,12 @@ fn print_table(args: &Args, results: &[MetricSamples]) {
|
|||||||
// Raw per-iteration samples, so published numbers can include raw data.
|
// Raw per-iteration samples, so published numbers can include raw data.
|
||||||
for metric in results {
|
for metric in results {
|
||||||
let ms: Vec<String> = metric.ms().iter().map(|v| format!("{v:.2}")).collect();
|
let ms: Vec<String> = metric.ms().iter().map(|v| format!("{v:.2}")).collect();
|
||||||
println!("{} samples_ms=[{}]", metric.name, ms.join(", "));
|
let _ = writeln!(out, "{} samples_ms=[{}]", metric.name, ms.join(", "));
|
||||||
}
|
}
|
||||||
|
out
|
||||||
}
|
}
|
||||||
|
|
||||||
fn print_json(args: &Args, results: &[MetricSamples]) {
|
fn render_json(args: &Args, results: &[MetricSamples]) -> String {
|
||||||
let mut entries = Vec::new();
|
let mut entries = Vec::new();
|
||||||
for metric in results {
|
for metric in results {
|
||||||
let s = metric.stats();
|
let s = metric.stats();
|
||||||
@@ -603,11 +618,58 @@ fn print_json(args: &Args, results: &[MetricSamples]) {
|
|||||||
Some(label) => format!("\"{}\"", label.replace('"', "\\\"")),
|
Some(label) => format!("\"{}\"", label.replace('"', "\\\"")),
|
||||||
None => "null".to_string(),
|
None => "null".to_string(),
|
||||||
};
|
};
|
||||||
println!(
|
format!(
|
||||||
"{{\"label\":{label},\"iterations\":{},\"metrics\":[{}]}}",
|
"{{\"label\":{label},\"iterations\":{},\"metrics\":[{}]}}",
|
||||||
args.iterations.max(1),
|
args.iterations.max(1),
|
||||||
entries.join(",")
|
entries.join(",")
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn render_markdown(args: &Args, results: &[MetricSamples]) -> String {
|
||||||
|
let mut out = String::new();
|
||||||
|
let label = args.label.as_deref().unwrap_or("Dosh benchmark");
|
||||||
|
let _ = writeln!(out, "# {label}\n");
|
||||||
|
let _ = writeln!(out, "- server: `{}`", args.server);
|
||||||
|
let _ = writeln!(out, "- iterations: `{}`", args.iterations.max(1));
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"- generated_by: `dosh-bench {}`\n",
|
||||||
|
env!("CARGO_PKG_VERSION")
|
||||||
);
|
);
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"| metric | n | min ms | median ms | p95 ms | mean ms | max ms |"
|
||||||
|
);
|
||||||
|
let _ = writeln!(out, "|---|---:|---:|---:|---:|---:|---:|");
|
||||||
|
for metric in results {
|
||||||
|
let s = metric.stats();
|
||||||
|
let _ = writeln!(
|
||||||
|
out,
|
||||||
|
"| `{}` | {} | {:.2} | {:.2} | {:.2} | {:.2} | {:.2} |",
|
||||||
|
metric.name, s.count, s.min, s.median, s.p95, s.mean, s.max
|
||||||
|
);
|
||||||
|
}
|
||||||
|
let _ = writeln!(out, "\n## Raw Samples\n");
|
||||||
|
for metric in results {
|
||||||
|
let ms: Vec<String> = metric.ms().iter().map(|v| format!("{v:.2}")).collect();
|
||||||
|
let _ = writeln!(out, "- `{}`: [{}]", metric.name, ms.join(", "));
|
||||||
|
}
|
||||||
|
out
|
||||||
|
}
|
||||||
|
|
||||||
|
fn write_report(args: &Args, report: &str) -> Result<()> {
|
||||||
|
if let Some(path) = &args.output {
|
||||||
|
if let Some(parent) = path.parent()
|
||||||
|
&& !parent.as_os_str().is_empty()
|
||||||
|
{
|
||||||
|
fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||||
|
}
|
||||||
|
fs::write(path, report).with_context(|| format!("write {}", path.display()))?;
|
||||||
|
println!("wrote {}", path.display());
|
||||||
|
} else {
|
||||||
|
println!("{report}");
|
||||||
|
}
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn metric_mean(results: &[MetricSamples], name: &str) -> Option<f64> {
|
fn metric_mean(results: &[MetricSamples], name: &str) -> Option<f64> {
|
||||||
@@ -677,3 +739,69 @@ fn default_client_path() -> PathBuf {
|
|||||||
.and_then(|path| path.parent().map(|parent| parent.join("dosh-client")))
|
.and_then(|path| path.parent().map(|parent| parent.join("dosh-client")))
|
||||||
.unwrap_or_else(|| PathBuf::from("dosh-client"))
|
.unwrap_or_else(|| PathBuf::from("dosh-client"))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
fn args_for_render() -> Args {
|
||||||
|
Args {
|
||||||
|
server: "local".to_string(),
|
||||||
|
session: "default".to_string(),
|
||||||
|
ssh_port: 22,
|
||||||
|
dosh_port: 50000,
|
||||||
|
dosh_host: None,
|
||||||
|
iterations: 2,
|
||||||
|
local_auth: true,
|
||||||
|
cold_native: false,
|
||||||
|
cached_ticket: false,
|
||||||
|
resume: false,
|
||||||
|
client: None,
|
||||||
|
ssh_auth_command: "~/.local/bin/dosh-auth".to_string(),
|
||||||
|
ssh_key: None,
|
||||||
|
ssh_known_hosts: None,
|
||||||
|
ssh_control_path: None,
|
||||||
|
controlmaster: false,
|
||||||
|
no_cache: false,
|
||||||
|
warm_cache: false,
|
||||||
|
skip_ssh_baseline: true,
|
||||||
|
include_mosh: false,
|
||||||
|
mosh: PathBuf::from("mosh"),
|
||||||
|
mosh_server_command: "mosh-server".to_string(),
|
||||||
|
mosh_port: None,
|
||||||
|
json: false,
|
||||||
|
markdown: true,
|
||||||
|
output: None,
|
||||||
|
label: Some("test host".to_string()),
|
||||||
|
assert_ssh_plus_ms: None,
|
||||||
|
assert_mosh_minus_ms: None,
|
||||||
|
assert_dosh_max_ms: None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn markdown_report_contains_summary_and_samples() {
|
||||||
|
let args = args_for_render();
|
||||||
|
let metrics = vec![MetricSamples::new(
|
||||||
|
"dosh_cached_attach_ms",
|
||||||
|
vec![Duration::from_millis(3), Duration::from_millis(5)],
|
||||||
|
)];
|
||||||
|
let report = render_markdown(&args, &metrics);
|
||||||
|
assert!(report.contains("# test host"));
|
||||||
|
assert!(report.contains("| `dosh_cached_attach_ms` | 2 | 3.00"));
|
||||||
|
assert!(report.contains("- `dosh_cached_attach_ms`: [3.00, 5.00]"));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn json_report_contains_metrics() {
|
||||||
|
let args = args_for_render();
|
||||||
|
let metrics = vec![MetricSamples::new(
|
||||||
|
"dosh_local_attach_ms",
|
||||||
|
vec![Duration::from_millis(7)],
|
||||||
|
)];
|
||||||
|
let report = render_json(&args, &metrics);
|
||||||
|
assert!(report.contains("\"label\":\"test host\""));
|
||||||
|
assert!(report.contains("\"metric\":\"dosh_local_attach_ms\""));
|
||||||
|
assert!(report.contains("\"samples_ms\":[7.0000]"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
+92
-6
@@ -67,11 +67,11 @@ fn terminal_size() -> (u16, u16) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Parser)]
|
#[derive(Debug, Parser)]
|
||||||
#[command(name = "dosh-client")]
|
#[command(name = "dosh-client", version)]
|
||||||
struct Args {
|
struct Args {
|
||||||
#[arg()]
|
#[arg()]
|
||||||
server: Option<String>,
|
server: Option<String>,
|
||||||
#[arg()]
|
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
|
||||||
command: Vec<String>,
|
command: Vec<String>,
|
||||||
#[arg(long)]
|
#[arg(long)]
|
||||||
session: Option<String>,
|
session: Option<String>,
|
||||||
@@ -192,7 +192,7 @@ async fn main() -> Result<()> {
|
|||||||
let args = Args::parse();
|
let args = Args::parse();
|
||||||
let config = load_client_config(None).unwrap_or_default();
|
let config = load_client_config(None).unwrap_or_default();
|
||||||
if args.server.as_deref() == Some("update") {
|
if args.server.as_deref() == Some("update") {
|
||||||
return run_update(&config);
|
return run_update(&config, update_check_requested(&args.command)?);
|
||||||
}
|
}
|
||||||
if args.server.as_deref() == Some("import-ssh") {
|
if args.server.as_deref() == Some("import-ssh") {
|
||||||
return run_import_ssh(&config, &args.command);
|
return run_import_ssh(&config, &args.command);
|
||||||
@@ -1079,13 +1079,75 @@ fn run_sessions_command(
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
fn run_update(config: &dosh::config::ClientConfig) -> Result<()> {
|
fn update_check_requested(command: &[String]) -> Result<bool> {
|
||||||
|
match command {
|
||||||
|
[] => Ok(false),
|
||||||
|
[flag] if flag == "--check" || flag == "-n" => Ok(true),
|
||||||
|
_ => Err(anyhow!("usage: dosh update [--check]")),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn release_artifact_name() -> &'static str {
|
||||||
|
match (std::env::consts::OS, std::env::consts::ARCH) {
|
||||||
|
("macos", "aarch64") => "dosh-macos-aarch64.tar.gz",
|
||||||
|
("macos", "x86_64") => "dosh-macos-x86_64.tar.gz",
|
||||||
|
("linux", "aarch64") => "dosh-linux-aarch64.tar.gz",
|
||||||
|
("linux", "x86_64") => "dosh-linux-x86_64.tar.gz",
|
||||||
|
("windows", "x86_64") => "dosh-windows-x86_64.zip",
|
||||||
|
("windows", "aarch64") => "dosh-windows-aarch64.zip",
|
||||||
|
_ => "dosh-unknown.tar.gz",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn latest_release_download_url(repo: &str, artifact: &str) -> Option<String> {
|
||||||
|
let web = repo
|
||||||
|
.strip_suffix(".git")
|
||||||
|
.unwrap_or(repo)
|
||||||
|
.trim_end_matches('/');
|
||||||
|
if !web.starts_with("http://") && !web.starts_with("https://") {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
Some(format!("{web}/releases/latest/download/{artifact}"))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn url_reachable(url: &str) -> Result<bool> {
|
||||||
|
let status = Command::new("curl")
|
||||||
|
.arg("-fsIL")
|
||||||
|
.arg(url)
|
||||||
|
.stdin(Stdio::null())
|
||||||
|
.stdout(Stdio::null())
|
||||||
|
.stderr(Stdio::null())
|
||||||
|
.status()
|
||||||
|
.with_context(|| format!("check {url}"))?;
|
||||||
|
Ok(status.success())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn run_update(config: &dosh::config::ClientConfig, check_only: bool) -> Result<()> {
|
||||||
let repo = config
|
let repo = config
|
||||||
.update_repo
|
.update_repo
|
||||||
.clone()
|
.clone()
|
||||||
.unwrap_or_else(|| "https://git.palav.dev/Palav/dosh.git".to_string());
|
.unwrap_or_else(|| "https://git.palav.dev/Palav/dosh.git".to_string());
|
||||||
let raw_base = raw_base_from_repo(&repo);
|
let raw_base = raw_base_from_repo(&repo);
|
||||||
let installer = format!("{raw_base}/raw/branch/main/install.sh");
|
let installer = format!("{raw_base}/raw/branch/main/install.sh");
|
||||||
|
let artifact = release_artifact_name();
|
||||||
|
let binary_url = latest_release_download_url(&repo, artifact);
|
||||||
|
if check_only {
|
||||||
|
println!("dosh {}", env!("CARGO_PKG_VERSION"));
|
||||||
|
println!("repo: {repo}");
|
||||||
|
println!("installer: {installer}");
|
||||||
|
if let Some(binary_url) = &binary_url {
|
||||||
|
let status = if url_reachable(binary_url)? {
|
||||||
|
"available"
|
||||||
|
} else {
|
||||||
|
"missing"
|
||||||
|
};
|
||||||
|
println!("prebuilt: {status} ({artifact})");
|
||||||
|
println!("prebuilt_url: {binary_url}");
|
||||||
|
} else {
|
||||||
|
println!("prebuilt: unavailable for non-HTTP repo");
|
||||||
|
}
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
let update_cache = dirs::cache_dir()
|
let update_cache = dirs::cache_dir()
|
||||||
.unwrap_or_else(|| expand_tilde("~/.cache"))
|
.unwrap_or_else(|| expand_tilde("~/.cache"))
|
||||||
.join("dosh")
|
.join("dosh")
|
||||||
@@ -4286,12 +4348,12 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
|
|||||||
mod tests {
|
mod tests {
|
||||||
use super::{
|
use super::{
|
||||||
DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, PredictMode, Predictor,
|
DisconnectStatus, DynamicForward, FrameBuffer, LocalForward, PredictMode, Predictor,
|
||||||
RemoteForward, SshConfig, StatusAction, auth_allows,
|
RemoteForward, SshConfig, StatusAction, auth_allows, latest_release_download_url,
|
||||||
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_local_forward,
|
load_first_native_identity_with_prompt, parse_dynamic_forward, parse_local_forward,
|
||||||
parse_remote_forward, parse_ssh_config, raw_contains_host_table, recv_response_until,
|
parse_remote_forward, parse_ssh_config, raw_contains_host_table, recv_response_until,
|
||||||
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
render_status_clear, render_status_overlay, requested_env, resolved_startup_command,
|
||||||
ssh_destination_host, ssh_username, ssh_with_user, startup_command,
|
ssh_destination_host, ssh_username, ssh_with_user, startup_command,
|
||||||
toml_bare_key_or_quoted,
|
toml_bare_key_or_quoted, update_check_requested,
|
||||||
};
|
};
|
||||||
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
use dosh::config::{ClientConfig, CommandExtension, HostConfig};
|
||||||
use dosh::native::EnvVar;
|
use dosh::native::EnvVar;
|
||||||
@@ -4839,6 +4901,30 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn update_check_accepts_only_check_flag() {
|
||||||
|
assert!(!update_check_requested(&[]).unwrap());
|
||||||
|
assert!(update_check_requested(&["--check".to_string()]).unwrap());
|
||||||
|
assert!(update_check_requested(&["-n".to_string()]).unwrap());
|
||||||
|
assert!(update_check_requested(&["--wat".to_string()]).is_err());
|
||||||
|
assert!(update_check_requested(&["--check".to_string(), "extra".to_string()]).is_err());
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn release_download_url_uses_latest_release_asset() {
|
||||||
|
assert_eq!(
|
||||||
|
latest_release_download_url(
|
||||||
|
"https://git.palav.dev/Palav/dosh.git",
|
||||||
|
"dosh-linux-x86_64.tar.gz"
|
||||||
|
)
|
||||||
|
.unwrap(),
|
||||||
|
"https://git.palav.dev/Palav/dosh/releases/latest/download/dosh-linux-x86_64.tar.gz"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
latest_release_download_url("git@git.palav.dev:Palav/dosh.git", "artifact").is_none()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn resolved_startup_command_expands_global_extension() {
|
fn resolved_startup_command_expands_global_extension() {
|
||||||
let mut config = ClientConfig::default();
|
let mut config = ClientConfig::default();
|
||||||
|
|||||||
Reference in New Issue
Block a user