Improve release and benchmark tooling

This commit is contained in:
DuProcess
2026-06-20 10:37:26 -04:00
parent 7884ea2796
commit b44ff8e773
12 changed files with 608 additions and 73 deletions
+27
View File
@@ -48,6 +48,33 @@ jobs:
if: steps.nightly.outcome != 'success' || steps.install.outcome != 'success'
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:
runs-on: ubuntu-latest
env:
+4 -1
View File
@@ -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:
cargo build --release
@@ -15,6 +15,9 @@ install:
package-release:
sh scripts/package-release.sh
bench-report:
sh scripts/bench-report.sh
# 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
# a temp HOME. Never touches the production server or UDP port 50000.
+16 -2
View File
@@ -117,12 +117,15 @@ Update an installed client later:
```bash
dosh update
dosh update --check
dosh --version
```
`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
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:
@@ -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
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:
```bash
@@ -275,6 +287,7 @@ Build release tarballs for upload to Gitea/GitHub releases:
```bash
make package-release
GITEA_TOKEN=... scripts/upload-gitea-release.sh v0.1.0 target/dosh-release/dosh-*
```
## Performance Rules
@@ -311,7 +324,8 @@ Hot-path rules:
- Replacing SSH as the first public-key trust mechanism.
- 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.
- Perfect predictive local echo in the first MVP.
+3 -1
View File
@@ -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-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-report # Markdown report wrapper around dosh-bench
```
`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
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
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
+2 -2
View File
@@ -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. |
| 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. |
| 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. |
| 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. |
@@ -206,7 +206,7 @@ Additional security hardening tracked outside the section 16 list:
## Before Public Launch
- 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
publish the target durations.
- Keep scripted TUI tests green and add a broader app matrix for real alternate-screen
+166 -47
View File
@@ -6,6 +6,12 @@ param(
[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 = $($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
)
@@ -17,39 +23,157 @@ function Require-Command($Name) {
}
}
Require-Command cargo
$tmp = $null
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"
function Normalize-Arch {
switch ($env:PROCESSOR_ARCHITECTURE) {
"AMD64" { "x86_64"; break }
"ARM64" { "aarch64"; break }
default { $env:PROCESSOR_ARCHITECTURE.ToLowerInvariant() }
}
Require-Command git
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("dosh-" + [guid]::NewGuid())
git clone --depth 1 $Repo $tmp | Out-Null
$src = $tmp
}
try {
Push-Location $src
cargo build --release --bin dosh-client --bin dosh-bench
function Release-ArtifactName {
if ($BinaryName) {
return $BinaryName
}
"dosh-windows-$(Normalize-Arch).zip"
}
$bindir = Join-Path $Prefix "bin"
$configDir = Join-Path $HOME ".config\dosh"
New-Item -ItemType Directory -Force -Path $bindir, $configDir | Out-Null
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
}
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-bench.exe" (Join-Path $bindir "dosh-bench.exe") -Force
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"
}
$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`"" }
@"
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
} else {
if (-not $Repo) {
throw "DOSH_REPO is required when running the installer from irm/iex"
}
Require-Command git
$tmp = Join-Path ([System.IO.Path]::GetTempPath()) ("dosh-" + [guid]::NewGuid())
git clone --depth 1 $Repo $tmp | Out-Null
$src = $tmp
}
try {
Push-Location $src
cargo build --release --bin dosh-client --bin dosh-bench
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-bench.exe" (Join-Path $bindir "dosh-bench.exe") -Force
}
finally {
Pop-Location
if ($tmp) {
Remove-Item -Recurse -Force $tmp
}
}
}
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"
@@ -60,28 +184,23 @@ 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"
"@ | Set-Content -NoNewline -Encoding utf8 $clientConfig
}
$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 command:"
Write-Host " $bindir\dosh.exe $displayServer"
Write-Host ""
Write-Host "Open a new terminal for PATH changes to apply."
}
finally {
Pop-Location
if ($tmp) {
Remove-Item -Recurse -Force $tmp
}
$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 command:"
Write-Host " $bindir\dosh.exe $displayServer"
Write-Host ""
Write-Host "Open a new terminal for PATH changes to apply."
+31
View File
@@ -217,10 +217,40 @@ install_extracted_binary() {
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() {
download_url="$(release_download_url)" || return 1
tmpdir="$(mktemp -d)"
archive="$tmpdir/$(release_artifact_name)"
checksum_file="$archive.sha256"
[ "$quiet" = "1" ] && echo "Trying Dosh prebuilt $(release_artifact_name)"
need curl
need tar
@@ -228,6 +258,7 @@ try_install_prebuilt() {
echo "prebuilt unavailable: $download_url" >&2
return 1
fi
verify_archive_checksum "$download_url" "$archive" "$checksum_file" || return 1
mkdir -p "$tmpdir/extract"
if ! tar -xzf "$archive" -C "$tmpdir/extract"; then
echo "prebuilt archive could not be extracted: $download_url" >&2
+21
View File
@@ -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:-}
+36 -3
View File
@@ -12,6 +12,7 @@ normalize_os() {
Darwin) printf '%s\n' macos ;;
Linux) printf '%s\n' linux ;;
FreeBSD) printf '%s\n' freebsd ;;
MINGW*|MSYS*|CYGWIN*) printf '%s\n' windows ;;
*) uname -s | tr '[:upper:]' '[:lower:]' ;;
esac
}
@@ -27,8 +28,13 @@ normalize_arch() {
os="$(normalize_os)"
arch="$(normalize_arch)"
artifact="dosh-$os-$arch.tar.gz"
versioned_artifact="dosh-$version-$os-$arch.tar.gz"
if [ "$os" = "windows" ]; then
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"
cargo build --release
@@ -42,11 +48,38 @@ for bin in dosh-client dosh-server dosh-auth dosh-bench; do
done
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"
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
Wrote:
$out_dir/$artifact
$out_dir/$artifact.sha256
$out_dir/$versioned_artifact
$out_dir/$versioned_artifact.sha256
EOF
+71
View File
@@ -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
View File
@@ -2,6 +2,8 @@ use anyhow::{Context, Result, anyhow};
use clap::Parser;
use portable_pty::{CommandBuilder, NativePtySystem, PtySize, PtySystem};
use std::ffi::OsStr;
use std::fmt::Write as _;
use std::fs;
use std::io::Read;
use std::path::PathBuf;
use std::process::Command;
@@ -63,6 +65,12 @@ struct Args {
/// Emit machine-readable JSON (one object per metric, with raw samples).
#[arg(long)]
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).
#[arg(long)]
label: Option<String>,
@@ -179,11 +187,14 @@ fn main() -> Result<()> {
results.push(MetricSamples::new("mosh_start_true_ms", mosh_times));
}
if args.json {
print_json(&args, &results);
let report = if args.json {
render_json(&args, &results)
} else if args.markdown {
render_markdown(&args, &results)
} else {
print_table(&args, &results);
}
render_table(&args, &results)
};
write_report(&args, &report)?;
run_assertions(&args, &results)?;
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 {
println!("# label: {label}");
let _ = writeln!(out, "# label: {label}");
}
println!(
let _ = writeln!(
out,
"{:<24} {:>6} {:>9} {:>9} {:>9} {:>9} {:>9}",
"metric", "n", "min", "median", "p95", "mean", "max"
);
for metric in results {
let s = metric.stats();
println!(
let _ = writeln!(
out,
"{:<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
);
@@ -578,11 +592,12 @@ fn print_table(args: &Args, results: &[MetricSamples]) {
// Raw per-iteration samples, so published numbers can include raw data.
for metric in results {
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();
for metric in results {
let s = metric.stats();
@@ -603,11 +618,58 @@ fn print_json(args: &Args, results: &[MetricSamples]) {
Some(label) => format!("\"{}\"", label.replace('"', "\\\"")),
None => "null".to_string(),
};
println!(
format!(
"{{\"label\":{label},\"iterations\":{},\"metrics\":[{}]}}",
args.iterations.max(1),
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> {
@@ -677,3 +739,69 @@ fn default_client_path() -> PathBuf {
.and_then(|path| path.parent().map(|parent| parent.join("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
View File
@@ -67,11 +67,11 @@ fn terminal_size() -> (u16, u16) {
}
#[derive(Debug, Parser)]
#[command(name = "dosh-client")]
#[command(name = "dosh-client", version)]
struct Args {
#[arg()]
server: Option<String>,
#[arg()]
#[arg(trailing_var_arg = true, allow_hyphen_values = true)]
command: Vec<String>,
#[arg(long)]
session: Option<String>,
@@ -192,7 +192,7 @@ async fn main() -> Result<()> {
let args = Args::parse();
let config = load_client_config(None).unwrap_or_default();
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") {
return run_import_ssh(&config, &args.command);
@@ -1079,13 +1079,75 @@ fn run_sessions_command(
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
.update_repo
.clone()
.unwrap_or_else(|| "https://git.palav.dev/Palav/dosh.git".to_string());
let raw_base = raw_base_from_repo(&repo);
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()
.unwrap_or_else(|| expand_tilde("~/.cache"))
.join("dosh")
@@ -4286,12 +4348,12 @@ const TERMINAL_CLEANUP: &[u8] = concat!(
mod tests {
use super::{
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,
parse_remote_forward, parse_ssh_config, raw_contains_host_table, recv_response_until,
render_status_clear, render_status_overlay, requested_env, resolved_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::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]
fn resolved_startup_command_expands_global_extension() {
let mut config = ClientConfig::default();