Improve release and benchmark tooling
This commit is contained in:
@@ -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:
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+151
-32
@@ -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,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
|
||||
if (Test-Path "Cargo.toml") {
|
||||
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 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 {
|
||||
} else {
|
||||
if (-not $Repo) {
|
||||
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())
|
||||
git clone --depth 1 $Repo $tmp | Out-Null
|
||||
$src = $tmp
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
try {
|
||||
Push-Location $src
|
||||
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.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 ($ForceConfig -or -not (Test-Path $clientConfig)) {
|
||||
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`"" }
|
||||
@@ -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)) {
|
||||
$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
|
||||
}
|
||||
}
|
||||
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
@@ -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
|
||||
|
||||
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 ;;
|
||||
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
|
||||
|
||||
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 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
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user