4 Commits
15 changed files with 1353 additions and 1091 deletions

No files matched your search

+131 -48
View File
@@ -8,20 +8,64 @@ on:
workflow_dispatch:
permissions:
contents: write
contents: read
env:
SQLX_OFFLINE: "true"
RELEASE_VERSION: ${{ github.ref_name }}
jobs:
build:
runs-on: ubuntu-latest
router:
name: Router (ARMv7 musl)
runs-on: ubuntu-24.04
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
with:
targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu
path: |
~/.cargo/registry
~/.cargo/git
target
key: release-router-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: release-router-${{ runner.os }}-${{ runner.arch }}-
- name: Cache cross executable
id: cross
uses: actions/cache@v4
with:
path: ~/.cargo/bin/cross
key: cross-${{ runner.os }}-${{ runner.arch }}-0.2.5
- name: Install cross
if: steps.cross.outputs.cache-hit != 'true'
run: cargo install cross --version 0.2.5 --locked
- name: Build router binaries
run: cross build --release --locked --target armv7-unknown-linux-musleabihf -p wakey -p wakey-agent
- name: Package rootfs tarball
shell: pwsh
run: ./scripts/package_rootfs.ps1 -Version $env:RELEASE_VERSION -Target armv7-unknown-linux-musleabihf
- name: Upload rootfs
uses: actions/upload-artifact@v4
with:
name: wakey-rootfs
path: dist/wakey-rootfs-*armv7-unknown-linux-musleabihf.tgz
if-no-files-found: error
compression-level: 0
ui:
name: UI
runs-on: ubuntu-24.04
steps:
- uses: actions/checkout@v4
- uses: pnpm/action-setup@v4
with:
@@ -33,8 +77,36 @@ jobs:
cache: pnpm
cache-dependency-path: ui/pnpm-lock.yaml
- name: Install cross
run: cargo install cross --locked
- name: Build UI dist
working-directory: ui
run: |
pnpm install --frozen-lockfile
pnpm build
- name: Upload UI
uses: actions/upload-artifact@v4
with:
name: wakey-ui
path: ui/dist/
if-no-files-found: error
cc:
name: CC (${{ matrix.target }})
strategy:
fail-fast: false
matrix:
include:
- target: x86_64-unknown-linux-gnu
runner: ubuntu-24.04
- target: aarch64-unknown-linux-gnu
runner: ubuntu-24.04-arm
runs-on: ${{ matrix.runner }}
steps:
- uses: actions/checkout@v4
with:
submodules: recursive
- uses: dtolnay/rust-toolchain@stable
- name: Cache cargo
uses: actions/cache@v4
@@ -43,57 +115,67 @@ jobs:
~/.cargo/registry
~/.cargo/git
target
key: linux-cargo-${{ hashFiles('**/Cargo.lock') }}
key: release-cc-${{ matrix.target }}-${{ hashFiles('**/Cargo.lock') }}
restore-keys: release-cc-${{ matrix.target }}-
- name: Build (armv7-musl with cross)
run: |
cross build --release --target armv7-unknown-linux-musleabihf -p wakey -p wakey-agent
# Native GNU builds use the runner's glibc. Gitea remains the separate
# older-glibc release path for deployment hosts that need it.
- name: Build control-plane natively
run: cargo build --release --locked --target ${{ matrix.target }} -p wakey-control-plane
- name: Build UI dist
shell: bash
run: |
set -euo pipefail
cd ui
pnpm install --frozen-lockfile
pnpm build
- name: Build control-plane (linux-gnu)
shell: bash
run: |
set -euo pipefail
# x86_64 builds natively; aarch64 needs cross
echo "Building control-plane x86_64-unknown-linux-gnu (native)"
cargo build --release --target x86_64-unknown-linux-gnu -p wakey-control-plane
echo "Building control-plane aarch64-unknown-linux-gnu (cross)"
cross build --release --target aarch64-unknown-linux-gnu -p wakey-control-plane
- name: Package rootfs tarball
shell: pwsh
run: |
./scripts/package_rootfs.ps1 -Version "${{ github.ref_name }}" -Target armv7-unknown-linux-musleabihf
- name: Package control-plane bundles
shell: bash
run: |
set -euo pipefail
chmod +x scripts/update_wakey_cc.sh scripts/package_wakey_cc_bundle.sh
for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
./scripts/package_wakey_cc_bundle.sh --version "${{ github.ref_name }}" --target "$target"
done
- name: Upload artifact
- name: Upload control-plane binary
uses: actions/upload-artifact@v4
with:
name: wakey-cc-bin-${{ matrix.target }}
path: target/${{ matrix.target }}/release/wakey-control-plane
if-no-files-found: error
release:
name: Package and publish
needs: [router, ui, cc]
runs-on: ubuntu-24.04
permissions:
contents: write
steps:
- uses: actions/checkout@v4
- name: Download rootfs
uses: actions/download-artifact@v4
with:
name: wakey-rootfs
path: dist/wakey-rootfs-*armv7-unknown-linux-musleabihf.tgz
path: dist
- name: Download UI
uses: actions/download-artifact@v4
with:
name: wakey-ui
path: ui/dist
- name: Download x86-64 CC
uses: actions/download-artifact@v4
with:
name: wakey-cc-bin-x86_64-unknown-linux-gnu
path: target/x86_64-unknown-linux-gnu/release
- name: Download ARM64 CC
uses: actions/download-artifact@v4
with:
name: wakey-cc-bin-aarch64-unknown-linux-gnu
path: target/aarch64-unknown-linux-gnu/release
- name: Package control-plane bundles
run: |
for target in x86_64-unknown-linux-gnu aarch64-unknown-linux-gnu; do
sh ./scripts/package_wakey_cc_bundle.sh --version "$RELEASE_VERSION" --target "$target"
done
- name: Upload control-plane bundles
uses: actions/upload-artifact@v4
with:
name: wakey-cc-bundles
path: dist/wakey-cc-*unknown-linux-gnu.tgz
if-no-files-found: error
compression-level: 0
- name: Publish Release to GitHub
if: startsWith(github.ref, 'refs/tags/')
@@ -101,6 +183,7 @@ jobs:
with:
token: ${{ secrets.GITHUB_TOKEN }}
tag_name: ${{ github.ref_name }}
fail_on_unmatched_files: true
files: |
dist/wakey-rootfs-*armv7-unknown-linux-musleabihf.tgz
dist/wakey-cc-*unknown-linux-gnu.tgz
Generated
+699 -900
View File
File diff suppressed because it is too large. Load diff
+9
View File
@@ -155,6 +155,15 @@ This needs classic histogram bucket series. Keep `le` in the grouping: it is
the bucket boundary, in seconds. p95 is an estimate from those boundaries,
not an exact sort of recorded durations. Few operations make percentiles noisy.
Operation-duration buckets span 100 microseconds to 60 seconds, with an overflow
bucket above that. Older binaries used SDK defaults whose first positive boundary
was 5 seconds: fast operations could therefore report an estimated p95 of 4.75
seconds. Updating both CC and agents fixes future measurements, not historical
data. Wait until the query's window contains only new-version measurements after
rollout; mixing old and new bucket layouts makes the estimate unreliable.
With roughly one snapshot per minute, a five-minute percentile is based on only
a handful of observations and can still look stepped. Use the mean above alongside it.
CC relay duration includes waiting for the agent; agent execution duration
measures its dispatch separately. Do not sum both and call that user latency.
+1 -1
View File
@@ -75,7 +75,7 @@ pub async fn get_with_handle(
NeighbourAddress::Inet6(v6) => ip = Some(IpAddr::from(v6)),
_ => continue 'row,
},
NeighbourAttribute::LinkLocalAddress(bytes) => {
NeighbourAttribute::LinkLayerAddress(bytes) => {
mac = match bytes.len() {
6 => bytes.first_chunk::<6>().map(|&b| MacAddr::from(b)),
8 => bytes.first_chunk::<8>().map(|&b| MacAddr::from(b)),
+12
View File
@@ -43,6 +43,10 @@ This folder has small helpers for build, CI, and router install. Keep it simple;
- `dev_push.ps1` — Fast dev loop: build + upload to router. Usage:
- `./scripts/dev_push.ps1 -Pass <password> [-HostName <ip>] [-RemotePath </root/.bin/wakey>] [-Restart] [-Quiet]`
- Uploads to `<RemotePath>.tmp` then atomically moves into place; `-Restart` restarts the service; `-Quiet` silences MOTD.
- Defaults to both binaries plus init/hooks, with restart enabled. Use `-Restart:$false` to leave the service running, or `-Quiet:$false` for deployment output. Config files are untouched.
- Binary-only agent update: `./scripts/dev_push.ps1 -HostName router -BinariesOnly -Package wakey-agent -Quiet:$false`
- Use `-BinariesOnly` without `-Package` for both binaries, or `-Package wakey` for only the CLI. Cargo still builds the selected package's dependencies.
- The checked-in `remote_deploy_wakey.sh` is streamed over SSH to `sh -s -- --batch ...` in all modes, with the deployment plan passed as quoted arguments. No remote helper is uploaded or required. Its original single-binary invocation remains supported. The shared `Invoke-Ssh -Cmd 'sh -s' -Stdin $script` supports stdin scripts for other callers.
- `package_rootfs.ps1` — Produces `dist/wakey-rootfs-<version>-<target>.tgz` with `/root/.bin/wakey` and `/etc/init.d/*`.
- `update_wakey_cc.sh` — Linux VPS updater for control-plane bundles; extracts into the selected directory (default: current directory) and restarts `wakey-cc.service`.
- `update_wakey_cc_from_repo.sh` — Linux VPS source updater; clones repo to temp dir, builds UI + control-plane locally, packages bundle tgz, installs into selected directory, and restarts `wakey-cc.service`.
@@ -144,6 +148,14 @@ Implementation notes:
## CI (GitHub)
- `.github/workflows/release.yml` builds router binaries, UI, x86-64 CC and
ARM64 CC in parallel. CC uses matching Ubuntu 24.04 runners and native Cargo;
Cross is used only for the ARMv7-musl router binaries. A final job downloads
their artifacts, packages CC bundles and publishes after every build succeeds.
- GitHub's native GNU binaries inherit its runner's glibc requirements. Use the
Gitea bundles for older deployment hosts, checking that its build actually
used Zig's configured baseline. Gitea retains one job for `--once` runners.
- Defined in `.github/workflows/ci.yml` on pushes/PRs to `main` or `master`:
- **`rust` job:** `cargo fmt --check`, `clippy -D warnings`, `cargo test --workspace` on `ubuntu-latest`.
- **`ui` job:** under `ui/`, `pnpm install --frozen-lockfile`, then `pnpm run typecheck`, `format:check`, and `build` (mirrors what you need before packaging the control-plane bundle). GitHub sets `CI=true` for the runner; pnpm uses lockfile `ui/pnpm-lock.yaml` for cache.
+6
View File
@@ -67,6 +67,12 @@ From Windows PowerShell, using the WSL wrapper:
Path control (binary and config are independent):
When called from PowerShell inside WSL, absolute POSIX paths pass through
unchanged. Windows paths are converted using the selected distro's `wslpath`.
Relative runner home, binary and config paths resolve from the current PowerShell
directory. Conversion also supports paths that have not been created yet.
The helper, runner home and config paths must be accessible in that distro.
```powershell
./scripts/act_runner_wsl.ps1 -Action status `
-RunnerBin /home/lda/gitea-runner/act_runner `
+8 -10
View File
@@ -2,7 +2,7 @@
param(
[ValidateSet("update", "register", "start", "stop", "status")]
[string]$Action = "start",
[string]$Distro = $(if ($env:WAKEY_WSL_DISTRO) { $env:WAKEY_WSL_DISTRO } else { "FedoraLinux-43" }),
[string]$Distro = $( $env:WAKEY_WSL_DISTRO ?? "FedoraLinux-43" ),
[string]$RunnerHome,
[string]$RunnerBin = "/home/lda/gitea-runner/act_runner",
[string]$ConfigPath,
@@ -22,7 +22,7 @@ if (-not (Test-Path $scriptWin)) {
throw "Missing Fedora runner helper: $scriptWin"
}
$scriptLinux = (& wsl.exe -d $Distro -e wslpath -a "$scriptWin").Trim()
$scriptLinux = ConvertTo-WslPath -WindowsPath $scriptWin -Distro $Distro
if (-not $scriptLinux) {
throw "Failed to resolve WSL path for $scriptWin"
}
@@ -30,21 +30,19 @@ if (-not $scriptLinux) {
if (-not $RunnerHome) {
$repoRootWin = Split-Path -Parent $PSScriptRoot
$defaultRunnerHomeWin = Join-Path $repoRootWin "ar_data/wsl_runner"
$RunnerHome = (& wsl.exe -d $Distro -e wslpath -a "$defaultRunnerHomeWin").Trim()
if (-not $RunnerHome) {
throw "Failed to resolve WSL runner home for $defaultRunnerHomeWin"
}
$RunnerHome = $defaultRunnerHomeWin
}
if (-not $ConfigPath) {
$repoRootWin = Split-Path -Parent $PSScriptRoot
$defaultConfigWin = Join-Path $repoRootWin "ar_data/wsl_runner/config.yaml"
$ConfigPath = (& wsl.exe -d $Distro -e wslpath -a "$defaultConfigWin").Trim()
if (-not $ConfigPath) {
throw "Failed to resolve WSL config path for $defaultConfigWin"
}
$ConfigPath = $defaultConfigWin
}
$RunnerHome = ConvertTo-WslPath -Path $RunnerHome -Distro $Distro
$RunnerBin = ConvertTo-WslPath -Path $RunnerBin -Distro $Distro
$ConfigPath = ConvertTo-WslPath -Path $ConfigPath -Distro $Distro
$parts = @("bash", (Quote-ShArg $scriptLinux), (Quote-ShArg $Action), "--runner-home", (Quote-ShArg $RunnerHome))
$parts += @("--runner-bin", (Quote-ShArg $RunnerBin), "--config-path", (Quote-ShArg $ConfigPath))
if ($ServerUrl) { $parts += @("--server-url", (Quote-ShArg $ServerUrl)) }
+80 -95
View File
@@ -4,6 +4,9 @@
param(
[ValidateSet("cargo", "cross")]
[string]$Cargo = "cargo",
[ValidateSet("wakey", "wakey-agent")]
[ValidateNotNullOrEmpty()]
[string[]]$Package = @("wakey", "wakey-agent"),
[string]$Pass,
[string]$HostName = "192.168.100.1",
[int]$Port = 22,
@@ -17,6 +20,7 @@ param(
[string]$HostKey,
[switch]$ForcePassword,
[switch]$SkipInitScript,
[switch]$BinariesOnly,
[switch]$Restart = $true,
[switch]$Quiet = $true
)
@@ -26,109 +30,90 @@ try { $PSStyle.OutputRendering = 'Host' } catch {}
. "$PSScriptRoot/lib.ps1"
$Pass = Get-DefaultPassword $Pass
function Build-Packages {
param([string[]]$Packages)
$buildArgs = @('build', '--release', '--target', $Target)
foreach ($pkg in $Packages) { $buildArgs += @('-p', $pkg) }
Write-Host "[build] $Cargo $($buildArgs -join ' ')" -ForegroundColor Cyan
& $Cargo @buildArgs
if ($LASTEXITCODE -ne 0) { throw "$Cargo build failed ($LASTEXITCODE)" }
}
function New-DeploymentPlan {
param([string[]]$Packages)
# Keep CLI first so replacing both binaries restarts the service only once.
$binaries = @(
@{ Package = 'wakey'; Name = $BinName; Destination = $RemotePath }
@{ Package = 'wakey-agent'; Name = $AgentBinName; Destination = $AgentRemotePath }
) | Where-Object { $_.Package -in $Packages } | ForEach-Object {
[pscustomobject]@{
Local = Join-Path $repoRoot "target/$Target/release/$($_.Name)"
Temp = "$($_.Destination).tmp"
Destination = $_.Destination
}
}
$support = @(
if (-not $BinariesOnly) {
if (-not $SkipInitScript) {
@{ Source = 'init/openwrt/wakey'; Temp = '/var/tmp/wakey.init.tmp'; Destination = $RemoteInitPath }
}
@{ Source = 'hotplug/openwrt/dhcp/95-wakey'; Temp = '/var/tmp/wakey.hotplug.dhcp.tmp'; Destination = '/etc/hotplug.d/dhcp/95-wakey' }
@{ Source = 'hotplug/openwrt/neigh/95-wakey'; Temp = '/var/tmp/wakey.hotplug.neigh.tmp'; Destination = '/etc/hotplug.d/neigh/95-wakey' }
}
) | ForEach-Object {
$local = Join-Path $PSScriptRoot $_.Source
if (Test-Path -LiteralPath $local) {
[pscustomobject]@{ Local = $local; Temp = $_.Temp; Destination = $_.Destination }
}
}
foreach ($binary in $binaries) {
if (-not (Test-Path -LiteralPath $binary.Local)) { throw "binary not found: $($binary.Local)" }
}
[pscustomobject]@{ Binaries = @($binaries); Support = @($support) }
}
function Get-RemoteDeployment {
param($Plan)
$deployArgs = @('--batch')
for ($i = 0; $i -lt $Plan.Binaries.Count; $i++) {
$binary = $Plan.Binaries[$i]
$restartFlag = if ($Restart -and $i -eq $Plan.Binaries.Count - 1) { '1' } else { '0' }
$deployArgs += @('--binary', $binary.Temp, $binary.Destination, $restartFlag)
}
foreach ($file in $Plan.Support) {
$deployArgs += @('--install', $file.Temp, $file.Destination)
}
# Only data needs shell quoting; the checked-in shell program travels as stdin.
$command = 'sh -s -- ' + (($deployArgs | ForEach-Object { Quote-ShArg $_ }) -join ' ')
if ($Quiet) { $command += ' >/dev/null 2>&1' }
[pscustomobject]@{
Script = Get-Content -Raw -LiteralPath "$PSScriptRoot/remote_deploy_wakey.sh"
Command = $command
}
}
function Send-DeploymentFiles {
param($Plan)
foreach ($file in @($Plan.Binaries) + @($Plan.Support)) {
Invoke-Scp -Local $file.Local -Dest "$User@${HostName}:$($file.Temp)" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
}
}
$Pass = Get-DefaultPassword $Pass
# Backward compatibility: allow HostName in the form host:port.
$hostPort = Split-HostPort -HostName $HostName -DefaultPort $Port
$HostName = $hostPort.Host
$Port = $hostPort.Port
function Get-DeployScript {
param($DeployPreferred, $DeployTmp, $RemoteTmp, $RemotePath, $RestartFlag)
return @"
DEPLOY=$DeployPreferred;
if [ ! -x "`$DEPLOY" ] && [ -f $DeployTmp ]; then
sed -i "s/\r$//" $DeployTmp 2>/dev/null || true
chmod +x $DeployTmp
DEPLOY=$DeployTmp
fi
sh "`$DEPLOY" $RemoteTmp $RemotePath $RestartFlag
"@
}
#---- Main Flow ----
$repoRoot = Split-Path -Parent $PSScriptRoot
Push-Location $repoRoot
try {
Write-Host "[build] $Cargo build --release --target $Target -p wakey -p wakey-agent" -ForegroundColor Cyan
. "$Cargo" build --release --target $Target -p wakey -p wakey-agent
if ($LASTEXITCODE -ne 0) { throw "$Cargo build failed ($LASTEXITCODE)" }
$localBin = Join-Path $repoRoot "target/$Target/release/$BinName"
$localAgentBin = Join-Path $repoRoot "target/$Target/release/$AgentBinName"
if (-not (Test-Path $localBin)) { throw "binary not found: $localBin" }
if (-not (Test-Path $localAgentBin)) { throw "binary not found: $localAgentBin" }
$remoteTmp = "$RemotePath.tmp"
$agentRemoteTmp = "$AgentRemotePath.tmp"
$destTmp = "$User@${HostName}:$remoteTmp"
$agentDestTmp = "$User@${HostName}:$agentRemoteTmp"
$initTmp = '/var/tmp/wakey.init.tmp'
$initDestTmp = "$User@${HostName}:$initTmp"
$localDeploy = Join-Path $repoRoot 'scripts/remote_deploy_wakey.sh'
$localInit = Join-Path $repoRoot 'scripts/init/openwrt/wakey'
$localDhcpHotplug = Join-Path $repoRoot 'scripts/hotplug/openwrt/dhcp/95-wakey'
$localNeighHotplug = Join-Path $repoRoot 'scripts/hotplug/openwrt/neigh/95-wakey'
$deployTmp = '/var/tmp/remote_deploy_wakey.sh'
$deployPreferred = '/root/.bin/remote_deploy_wakey.sh'
$dhcpHotplugTmp = '/var/tmp/wakey.hotplug.dhcp.tmp'
$neighHotplugTmp = '/var/tmp/wakey.hotplug.neigh.tmp'
# Push binaries
Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
Invoke-Scp -Local $localAgentBin -Dest $agentDestTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
# Push OpenWrt init script unless explicitly skipped
if (-not $SkipInitScript -and (Test-Path $localInit)) {
Invoke-Scp -Local $localInit -Dest $initDestTmp -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
}
if (Test-Path $localDhcpHotplug) {
Invoke-Scp -Local $localDhcpHotplug -Dest "$User@${HostName}:$dhcpHotplugTmp" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
}
if (Test-Path $localNeighHotplug) {
Invoke-Scp -Local $localNeighHotplug -Dest "$User@${HostName}:$neighHotplugTmp" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
}
# Push deploy helper if exists
if (Test-Path $localDeploy) {
Invoke-Scp -Local $localDeploy -Dest "$User@${HostName}:$deployTmp" -Pass $Pass -HostKey $HostKey -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
}
# Build and run remote deploy command
$restartFlag = $(if ($Restart) { '1' } else { '0' })
$script = @"
$(Get-DeployScript $deployPreferred $deployTmp $remoteTmp $RemotePath 0)
$(Get-DeployScript $deployPreferred $deployTmp $agentRemoteTmp $AgentRemotePath $restartFlag)
$(if (-not $SkipInitScript) {
@"
if [ -f $initTmp ]; then
sed -i "s/\r$//" $initTmp 2>/dev/null || true
if command -v install >/dev/null 2>&1; then
install -m 0755 $initTmp $RemoteInitPath
else
cp -f $initTmp $RemoteInitPath
chmod 0755 $RemoteInitPath
fi
fi
"@
})
if [ -f $dhcpHotplugTmp ]; then
sed -i "s/\r$//" $dhcpHotplugTmp 2>/dev/null || true
mkdir -p /etc/hotplug.d/dhcp
cp -f $dhcpHotplugTmp /etc/hotplug.d/dhcp/95-wakey
fi
if [ -f $neighHotplugTmp ]; then
sed -i "s/\r$//" $neighHotplugTmp 2>/dev/null || true
mkdir -p /etc/hotplug.d/neigh
cp -f $neighHotplugTmp /etc/hotplug.d/neigh/95-wakey
fi
"@
$remoteCmd = "sh -lc '$($script -replace "`r",'')'"
if ($Quiet) { $remoteCmd += " >/dev/null 2>&1" }
Invoke-Ssh -Cmd $remoteCmd -User $User -Remote $HostName -Pass $Pass -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
Build-Packages ($Package | Select-Object -Unique)
$plan = New-DeploymentPlan $Package
# Prepare before uploading so missing local helpers fail without remote changes.
$deployment = Get-RemoteDeployment $plan
Send-DeploymentFiles $plan
Invoke-Ssh -Cmd $deployment.Command -Stdin $deployment.Script -User $User -Remote $HostName -Pass $Pass -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
Write-Host "done ✔" -ForegroundColor Green
}
finally {
+55 -7
View File
@@ -27,8 +27,15 @@ function Test-IsWsl {
return $false
}
# With -Distro, resolve local relative paths from the PowerShell working
# directory and convert Windows paths with the distro's mount configuration.
# Without it, keep the existing mapping used by SCP callers, including remote destinations.
function ConvertTo-WslPath {
param([string]$WindowsPath)
param(
[Alias('Path')]
[string]$WindowsPath,
[string]$Distro
)
if ([string]::IsNullOrWhiteSpace($WindowsPath)) {
return $WindowsPath
@@ -39,6 +46,36 @@ function ConvertTo-WslPath {
return $WindowsPath
}
# wslpath expects a Windows input here. Use the selected distro's mount
# configuration when requested, while keeping existing POSIX paths intact.
if ($Distro) {
# Absolute Windows paths can be supplied even from Linux PowerShell.
# Relative inputs use the filesystem provider without requiring a file
# to exist, which is important for a new runner home/config.
if ($WindowsPath -notmatch '^[A-Za-z]:[\\/]|^\\\\') {
$provider = $null
$drive = $null
$WindowsPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath(
$WindowsPath, [ref]$provider, [ref]$drive)
if ($provider.Name -ne 'FileSystem') {
throw "WSL paths must use the filesystem provider"
}
if ($WindowsPath.StartsWith('/')) {
return $WindowsPath
}
}
$output = & wsl.exe -d $Distro -e wslpath -a "$WindowsPath"
$exitCode = $LASTEXITCODE
if ($exitCode -ne 0) {
throw "wslpath failed for '$WindowsPath' in '$Distro' (exit $exitCode)"
}
$converted = ($output -join "`n").Trim()
if (-not $converted) {
throw "wslpath returned no path for '$WindowsPath' in '$Distro'"
}
return $converted
}
# Handle Windows path (e.g., C:\path\to\file -> /mnt/c/path/to/file)
if ($WindowsPath -match '^([A-Z]):(.*)$') {
$drive = $matches[1].ToLower()
@@ -154,7 +191,7 @@ function Get-SshpassCommand {
}
function Invoke-Ext {
param($Exe, $Arguments, $Label)
param($Exe, $Arguments, $Label, [string]$Stdin)
$displayArgs = $Arguments.Clone()
for ($i = 0; $i -lt $displayArgs.Count; $i++) {
if ($displayArgs[$i] -eq '-pw' -and ($i + 1) -lt $displayArgs.Count) {
@@ -166,7 +203,12 @@ function Invoke-Ext {
}
Write-Host ("[{0}] {1} {2}" -f $Label, $Exe, ($displayArgs -join ' ')) -ForegroundColor Cyan
$out = & $Exe @Arguments 2>&1
# Keep stdin optional so interactive callers retain their existing behavior.
if ($PSBoundParameters.ContainsKey('Stdin')) {
$out = $Stdin | & $Exe @Arguments 2>&1
} else {
$out = & $Exe @Arguments 2>&1
}
if ($LASTEXITCODE -ne 0) {
if ($Exe -like '*sshpass*' -and $LASTEXITCODE -eq 6) {
Write-Error ("{0} failed: sshpass could not confirm host key automatically (code 6). Use ssh once manually, or enable StrictHostKeyChecking=accept-new." -f $Label)
@@ -247,8 +289,14 @@ function Invoke-Scp {
function Invoke-Ssh {
param($Cmd, $User, $Remote, $Pass, [int]$Port = 22, [switch]$Quiet, [switch]$ForcePassword)
# Supply Stdin for scripts, e.g. -Cmd 'sh -s' -Stdin $script. This avoids
# embedding a whole script in a remote shell command's quoting layer.
param($Cmd, $User, $Remote, $Pass, [int]$Port = 22, [switch]$Quiet, [switch]$ForcePassword, [string]$Stdin)
$Cmd = Normalize-LineEndings $Cmd
$inputArgs = @{}
if ($PSBoundParameters.ContainsKey('Stdin')) {
$inputArgs.Stdin = Normalize-LineEndings $Stdin
}
$isWin = [bool]$IsWindows
$isWsl = Test-IsWsl
@@ -260,7 +308,7 @@ function Invoke-Ssh {
if ($Pass) { $arguments += @('-pw', $Pass) }
$dest = if ($User) { "$User@$Remote" } else { $Remote }
$arguments += $dest, $Cmd
Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh'
Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh' @inputArgs
}
else {
$arguments = @()
@@ -282,7 +330,7 @@ function Invoke-Ssh {
$sshpassExe = Get-SshpassCommand
if ($sshpassExe) {
$wrapped = @('-p', $Pass, 'ssh') + $arguments
Invoke-Ext -Exe $sshpassExe -Arguments $wrapped -Label 'ssh'
Invoke-Ext -Exe $sshpassExe -Arguments $wrapped -Label 'ssh' @inputArgs
return
}
if (-not $script:WarnedNoSshpass) {
@@ -301,7 +349,7 @@ function Invoke-Ssh {
throw 'ForcePassword requires sshpass'
}
Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh'
Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh' @inputArgs
}
}
+69 -30
View File
@@ -1,41 +1,80 @@
#!/bin/sh
# Usage: remote_deploy_wakey.sh <bin_tmp> <dest_path> [restart_flag]
# restart_flag: 1 (default) to stop/start, 0 to only replace file.
# Or stream over SSH: sh -s -- --batch [--binary <tmp> <dest> <restart> |
# --install <tmp> <dest>]...
set -e
BIN_TMP="$1"
DEST="$2"
RESTART="${3:-1}"
INIT="/etc/init.d/wakey"
KILL="/root/.bin/kill_wakey.sh"
if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
exit 2
fi
# A subshell keeps per-binary variables and exit statements local, including
# when several replacements are requested by one dev_push invocation.
deploy_binary() (
BIN_TMP="$1"
DEST="$2"
RESTART="${3:-1}"
INIT="/etc/init.d/wakey"
KILL="/root/.bin/kill_wakey.sh"
[ -f "$BIN_TMP" ] || {
echo "tmp binary not found: $BIN_TMP" >&2
exit 1
if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
exit 2
fi
[ -f "$BIN_TMP" ] || {
echo "tmp binary not found: $BIN_TMP" >&2
exit 1
}
chmod +x "$BIN_TMP" || true
if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then
"$INIT" stop || true
elif [ -x "$KILL" ]; then
sh "$KILL" || true
fi
fi
mv -f "$BIN_TMP" "$DEST"
if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then
"$INIT" start || "$INIT" restart || true
else
nohup "$DEST" >/dev/null 2>&1 &
fi
fi
exit 0
)
install_support_file() {
sed -i 's/\r$//' "$1" 2>/dev/null || true
mkdir -p "$(dirname "$2")"
cp -f "$1" "$2"
chmod 0755 "$2"
}
chmod +x "$BIN_TMP" || true
if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then
"$INIT" stop || true
elif [ -x "$KILL" ]; then
sh "$KILL" || true
fi
if [ "${1:-}" != '--batch' ]; then
deploy_binary "$@"
exit
fi
mv -f "$BIN_TMP" "$DEST"
if [ "$RESTART" = "1" ]; then
if [ -x "$INIT" ]; then
"$INIT" start || "$INIT" restart || true
else
nohup "$DEST" >/dev/null 2>&1 &
fi
fi
exit 0
shift
while [ "$#" -gt 0 ]; do
case "$1" in
--binary)
[ "$#" -ge 4 ] || { echo '--binary requires tmp, destination, restart' >&2; exit 2; }
deploy_binary "$2" "$3" "$4"
shift 4
;;
--install)
[ "$#" -ge 3 ] || { echo '--install requires tmp and destination' >&2; exit 2; }
install_support_file "$2" "$3"
shift 3
;;
*)
echo "unknown deployment operation: $1" >&2
exit 2
;;
esac
done
+115
View File
@@ -0,0 +1,115 @@
#!/usr/bin/env pwsh
# Run with pwsh -NoProfile -File scripts/tests/dev_push.ps1. No router is contacted.
$ErrorActionPreference = 'Stop'
$repo = Split-Path -Parent (Split-Path -Parent $PSScriptRoot)
$fixture = Join-Path ([IO.Path]::GetTempPath()) "wakey-dev-push-$([guid]::NewGuid())"
function Assert($condition, $message) {
if (-not $condition) { throw $message }
}
try {
New-Item -ItemType Directory -Path "$fixture/scripts" -Force | Out-Null
Copy-Item "$repo/scripts/dev_push.ps1" "$fixture/scripts/dev_push.ps1"
Copy-Item "$repo/scripts/lib.ps1" "$fixture/scripts/actual-lib.ps1"
foreach ($file in @('remote_deploy_wakey.sh', 'init/openwrt/wakey', 'hotplug/openwrt/dhcp/95-wakey', 'hotplug/openwrt/neigh/95-wakey')) {
$dest = "$fixture/scripts/$file"
New-Item -ItemType Directory -Path (Split-Path -Parent $dest) -Force | Out-Null
Copy-Item "$repo/scripts/$file" $dest
}
# Override only external operations; keep path resolution and quoting real.
@'
. "$PSScriptRoot/actual-lib.ps1"
function cargo {
$global:DeployTest.Build = @($args)
$global:LASTEXITCODE = $global:DeployTest.BuildExit
}
function Invoke-Scp {
param($Local, $Dest, $Pass, $HostKey, $Port, [switch]$Quiet, [switch]$ForcePassword)
$global:DeployTest.Uploads.Add([pscustomobject]@{ Local = $Local; Dest = $Dest })
if ($global:DeployTest.Execute) {
Copy-Item -LiteralPath $Local -Destination ($Dest -split ':', 2)[1]
}
}
function Invoke-Ssh {
param($Cmd, $User, $Remote, $Pass, $Port, [switch]$Quiet, [switch]$ForcePassword, $Stdin)
$global:DeployTest.Command = $Cmd
$global:DeployTest.Body = $Stdin
if (-not $Cmd.StartsWith('sh -s -- ')) { throw 'Deployment must use SSH stdin with sh -s' }
# Parse exactly what would reach the remote shell, without executing it.
$Stdin | & sh -n
if ($LASTEXITCODE -ne 0) { throw 'Generated deployment shell is invalid' }
if ($global:DeployTest.Execute) {
$Stdin | & sh -c $Cmd
if ($LASTEXITCODE -ne 0) { throw 'Local fixture deployment failed' }
}
}
'@ | Set-Content "$fixture/scripts/lib.ps1"
function Run-Deploy($options = @{}, $binaries = @('wakey', 'wakey-agent'), $buildExit = 0, $execute = $false) {
$global:DeployTest = @{
Build = @(); BuildExit = $buildExit; Execute = $execute
Uploads = [System.Collections.Generic.List[object]]::new()
Command = $null
}
$output = "$fixture/target/test/release"
if (Test-Path $output) { Remove-Item -Recurse -Force $output }
New-Item -ItemType Directory -Path $output -Force | Out-Null
foreach ($binary in $binaries) { New-Item -ItemType File -Path "$output/$binary" | Out-Null }
& "$fixture/scripts/dev_push.ps1" -Pass test-only -HostName router -Target test -Quiet:$false @options
}
Run-Deploy
Assert ($global:DeployTest.Uploads.Count -eq 5) 'Default deploy must upload both binaries and three support files'
Assert (($global:DeployTest.Build -join ' ') -eq 'build --release --target test -p wakey -p wakey-agent') 'Default build changed'
Assert ($global:DeployTest.Command.Contains("'/root/.bin/wakey.tmp' '/root/.bin/wakey' '0'")) 'CLI must not restart before agent replacement'
Assert ($global:DeployTest.Command.Contains("'/root/.bin/wakey-agent.tmp' '/root/.bin/wakey-agent' '1'")) 'Agent must restart by default'
Assert ($global:DeployTest.Body -ceq (Get-Content -Raw "$repo/scripts/remote_deploy_wakey.sh")) 'Deployment must stream the checked-in file unchanged'
Run-Deploy @{ BinariesOnly = $true }
Assert ($global:DeployTest.Uploads.Count -eq 2) 'Binary-only deploy uploaded support files'
Assert (-not $global:DeployTest.Command.Contains('--install')) 'Binary-only deploy contains support installation'
foreach ($package in @('wakey', 'wakey-agent')) {
Run-Deploy @{ BinariesOnly = $true; Package = @($package, $package); Restart = $false } @($package)
Assert ($global:DeployTest.Uploads.Count -eq 1) 'Single-package deploy uploaded another file'
Assert (($global:DeployTest.Build -join ' ') -eq "build --release --target test -p $package") 'Package selection or deduplication failed'
Assert ($global:DeployTest.Command.Contains("'/root/.bin/$package.tmp' '/root/.bin/$package' '0'")) 'Restart override lost'
}
Run-Deploy @{ SkipInitScript = $true }
Assert ($global:DeployTest.Uploads.Count -eq 4) 'SkipInitScript must still upload hooks'
Run-Deploy @{ BinariesOnly = $true; Package = @('wakey-agent'); AgentRemotePath = "/tmp/owner's bin/agent" } @('wakey-agent')
Assert ($global:DeployTest.Command.Contains('owner')) 'Custom path lost'
# Execute only binary replacement inside the temporary fixture, with restart
# disabled. This also checks that the helper's exit doesn't skip binary two.
$cliPath = "$fixture/installed cli"
$agentPath = "$fixture/owner's agent"
Run-Deploy @{ BinariesOnly = $true; Restart = $false; RemotePath = $cliPath; AgentRemotePath = $agentPath } @('wakey', 'wakey-agent') 0 $true
Assert ((Test-Path -LiteralPath $cliPath) -and (Test-Path -LiteralPath $agentPath)) 'Streamed helper did not install both binaries'
Assert (-not (Test-Path -LiteralPath "$agentPath.tmp")) 'Temporary upload was not moved'
$supportTemp = "$fixture/support.tmp"
$supportDest = "$fixture/hook dir/owner's hook"
[IO.File]::WriteAllText($supportTemp, "#!/bin/sh`r`necho hook`r`n")
& sh "$fixture/scripts/remote_deploy_wakey.sh" --batch --install $supportTemp $supportDest
Assert ($LASTEXITCODE -eq 0) 'Support installation failed'
Assert (-not (Get-Content -Raw -LiteralPath $supportDest).Contains("`r")) 'Support CRLF was not normalized'
& sh -c 'test -x "$1"' sh $supportDest
Assert ($LASTEXITCODE -eq 0) 'Installed support file is not executable'
New-Item -ItemType File -Path "$fixture/legacy.tmp" | Out-Null
& sh "$fixture/scripts/remote_deploy_wakey.sh" "$fixture/legacy.tmp" "$fixture/legacy" 0
Assert ($LASTEXITCODE -eq 0 -and (Test-Path "$fixture/legacy")) 'Original helper invocation broke'
$failed = $false
try { Run-Deploy @{} @() 1 } catch { $failed = $_.Exception.Message -like '*build failed*' }
Assert $failed 'Build failure did not abort deployment'
Assert ($global:DeployTest.Uploads.Count -eq 0 -and $null -eq $global:DeployTest.Command) 'Failed build attempted deployment'
Write-Host 'dev_push tests passed'
} finally {
Remove-Variable DeployTest -Scope Global -ErrorAction SilentlyContinue
Remove-Item -Recurse -Force $fixture
}
+34
View File
@@ -0,0 +1,34 @@
#!/usr/bin/env pwsh
# Run with pwsh -NoProfile -File scripts/tests/ssh_stdin.ps1 on Linux/WSL.
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/../lib.ps1"
# Exercise the real native-command pipeline without contacting a server.
$body = "printf '%s\n' `"owner's directory`"`nprintf '%s\n' 'second line'"
$output = @(Invoke-Ext -Exe sh -Arguments @('-s') -Label test -Stdin $body)
if (($output -join '|') -cne "owner's directory|second line") { throw 'stdin script changed in transit' }
$output = @(Invoke-Ext -Exe sh -Arguments @('-c', 'printf command-only') -Label test)
if (($output -join '') -cne 'command-only') { throw 'Command-only execution changed' }
$failed = $false
try { Invoke-Ext -Exe sh -Arguments @('-s') -Label test -Stdin 'exit 7' } catch { $failed = $true }
if (-not $failed) { throw 'stdin execution swallowed a failure' }
# Test SSH argument and stdin forwarding independently of native SSH availability.
function Test-IsWsl { return $true }
function Get-SshpassCommand { return $script:Sshpass }
function Invoke-Ext {
param($Exe, $Arguments, $Label, [string]$Stdin)
$script:Call = @{ Exe = $Exe; Arguments = $Arguments; Stdin = $Stdin; HasStdin = $PSBoundParameters.ContainsKey('Stdin') }
}
$script:Sshpass = $null
Invoke-Ssh -Cmd 'sh -s' -User root -Remote test -Stdin "printf ok`r`n"
if ($script:Call.Exe -ne 'ssh' -or $script:Call.Stdin.Contains("`r") -or -not $script:Call.HasStdin) {
throw 'SSH stdin was not normalized and forwarded'
}
if (($script:Call.Arguments[-2..-1] -join '|') -ne 'root@test|sh -s') { throw 'SSH command changed' }
Invoke-Ssh -Cmd 'true' -User root -Remote test
if ($script:Call.HasStdin) { throw 'Command-only SSH unexpectedly redirects stdin' }
$script:Sshpass = '/fake/sshpass'
Invoke-Ssh -Cmd 'sh -s' -User root -Remote test -Pass test-only -Stdin $body
if ($script:Call.Exe -ne '/fake/sshpass' -or $script:Call.Stdin -cne $body) { throw 'sshpass lost stdin' }
Write-Host 'SSH stdin tests passed'
+97
View File
@@ -0,0 +1,97 @@
#!/usr/bin/env pwsh
# Run with pwsh -NoProfile -File scripts/tests/wsl_paths.ps1.
$ErrorActionPreference = 'Stop'
. "$PSScriptRoot/../lib.ps1"
# Intercept WSL so these tests never start or register a real runner.
$global:WakeyWslTest = @{
Calls = [System.Collections.Generic.List[object]]::new()
ExitCode = 0
# Fake conversion response, chosen to exercise a path containing spaces.
Output = '/mnt/c/runner home/config.yaml'
}
function global:wsl.exe {
$global:WakeyWslTest.Calls.Add(@($args))
$global:LASTEXITCODE = $global:WakeyWslTest.ExitCode
if ($args[3] -eq 'wslpath') { $global:WakeyWslTest.Output }
}
foreach ($path in @('/home/lda/repo/scripts/helper.sh', '/mnt/c/runner home/config.yaml')) {
$result = ConvertTo-WslPath -WindowsPath $path -Distro TestDistro
if ($result -cne $path) { throw "POSIX path changed: $result" }
}
if ($global:WakeyWslTest.Calls.Count -ne 0) { throw 'POSIX paths invoked wslpath' }
foreach ($path in @('C:\runner home\config.yaml', 'C:/runner home/config.yaml', '\\server\share\runner home\config.yaml')) {
$global:WakeyWslTest.Calls.Clear()
$result = ConvertTo-WslPath -WindowsPath $path -Distro TestDistro
if ($result -cne '/mnt/c/runner home/config.yaml') { throw 'Mock Windows conversion failed' }
$call = $global:WakeyWslTest.Calls[0]
if (($call[0..4] -join ' ') -cne '-d TestDistro -e wslpath -a' -or $call[5] -cne $path) {
throw 'Distro or Windows path was not passed intact'
}
}
if ((ConvertTo-WslPath 'D:\runner\config.yaml') -cne '/mnt/d/runner/config.yaml') {
throw 'Existing drive conversion changed'
}
foreach ($path in @('root@router:/root/.bin/', 'relative/file')) {
if ((ConvertTo-WslPath $path) -cne $path) { throw 'Existing SCP path handling changed' }
}
# Resolve missing relative paths from the PS location, including dot segments.
$temp = Join-Path ([System.IO.Path]::GetTempPath()) "wakey-wsl-paths-$([guid]::NewGuid())"
New-Item -ItemType Directory -Path $temp | Out-Null
Push-Location $temp
try {
foreach ($relative in @('new folder/config.yaml', '.\new folder\config.yaml', 'nested/../new folder/config.yaml')) {
$expected = Join-Path $temp 'new folder/config.yaml'
$global:WakeyWslTest.Calls.Clear()
$result = ConvertTo-WslPath -Path $relative -Distro TestDistro
if ($IsWindows) {
if ($global:WakeyWslTest.Calls[0][5] -cne $expected) { throw 'Relative Windows path was not resolved before conversion' }
} elseif ($result -cne $expected -or $global:WakeyWslTest.Calls.Count -ne 0) {
throw "Relative POSIX path was not resolved: $result"
}
}
} finally {
Pop-Location
Remove-Item -Recurse -Force $temp
}
foreach ($case in @(
@{ ExitCode = 1; Output = '' },
@{ ExitCode = 1; Output = '/mnt/c/bad' },
@{ ExitCode = 0; Output = '' }
)) {
$global:WakeyWslTest.ExitCode = $case.ExitCode
$global:WakeyWslTest.Output = $case.Output
$caught = $false
try { ConvertTo-WslPath -WindowsPath 'C:\bad' -Distro TestDistro | Out-Null }
catch { $caught = $_.Exception.Message -like 'wslpath *' }
if (-not $caught) { throw 'Failed/empty wslpath result was accepted' }
}
if (-not $IsWindows) {
$global:WakeyWslTest.Calls.Clear()
$global:WakeyWslTest.ExitCode = 0
& "$PSScriptRoot/../act_runner_wsl.ps1" -Action status -Distro TestDistro
if ($global:WakeyWslTest.Calls.Count -ne 1 -or $global:WakeyWslTest.Calls[0][3] -ne 'bash') {
throw 'POSIX wrapper paths should invoke only the runner command'
}
$command = $global:WakeyWslTest.Calls[0][5]
if ($command -notlike '*act_runner_fedora.sh*' -or $command -notlike '*ar_data/wsl_runner/config.yaml*') {
throw 'Wrapper did not resolve default paths'
}
$global:WakeyWslTest.Calls.Clear()
$global:WakeyWslTest.Output = '/mnt/c/runner home/config.yaml'
& "$PSScriptRoot/../act_runner_wsl.ps1" -Action status -Distro TestDistro `
-RunnerHome 'new runner home' -RunnerBin 'bin/act_runner' -ConfigPath 'C:\runner home\config.yaml'
if ($global:WakeyWslTest.Calls.Count -ne 2 -or $global:WakeyWslTest.Calls[0][3] -ne 'wslpath') {
throw 'Explicit Windows config should be converted once before running bash'
}
$command = $global:WakeyWslTest.Calls[1][5]
foreach ($path in @((Join-Path (Get-Location).Path 'new runner home'), (Join-Path (Get-Location).Path 'bin/act_runner'), '/mnt/c/runner home/config.yaml')) {
if (-not $command.Contains((Quote-ShArg $path))) { throw "Explicit path missing from runner command: $path" }
}
}
Write-Host 'WSL path tests passed'
+6
View File
@@ -338,6 +338,12 @@ impl Instruments {
duration: meter
.f64_histogram("wakey.operation.duration")
.with_unit("s")
// Measurements are seconds; SDK defaults start at 5, which
// collapses millisecond operations into one unusably wide bucket.
.with_boundaries(vec![
0.0001, 0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0,
10.0, 30.0, 60.0,
])
.build(),
snapshot_devices: meter.u64_counter("wakey.snapshot.devices").build(),
terminal_active: meter.i64_up_down_counter("wakey.terminal.active").build(),
+31
View File
@@ -409,6 +409,37 @@ async fn exports_all_signals_and_preserves_remote_command_parent() {
assert!(names.contains(&name), "missing {name}");
}
let latest = metrics.last().unwrap();
{
let duration = latest
.resource_metrics
.iter()
.flat_map(|r| &r.scope_metrics)
.flat_map(|s| &s.metrics)
.find(|m| m.name == "wakey.operation.duration")
.unwrap();
assert_eq!(duration.unit, "s");
let Some(metric::Data::Histogram(histogram)) = &duration.data else {
panic!("operation duration must export a histogram");
};
let point = histogram.data_points.first().unwrap();
assert_eq!(
point.explicit_bounds,
vec![
0.0001, 0.0005, 0.001, 0.005, 0.01, 0.025, 0.05, 0.1, 0.25, 0.5, 1.0, 2.5, 5.0,
10.0, 30.0, 60.0,
]
);
// One inventory operation is recorded above. Derive its expected bucket
// from the exported sum, avoiding elapsed-time thresholds or sleeps.
assert_eq!(point.count, 1);
let sample = point.sum.unwrap();
let bucket = point
.explicit_bounds
.partition_point(|bound| *bound < sample);
let mut expected = vec![0; point.explicit_bounds.len() + 1];
expected[bucket] = 1;
assert_eq!(point.bucket_counts, expected);
}
{
for (name, unit, expected) in [
(