Compare commits
6
Commits
v0.3.7
...
4239886b1a
No files matched your search
+131
-48
@@ -8,20 +8,64 @@ on:
|
|||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: read
|
||||||
|
|
||||||
|
env:
|
||||||
|
SQLX_OFFLINE: "true"
|
||||||
|
RELEASE_VERSION: ${{ github.ref_name }}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
router:
|
||||||
runs-on: ubuntu-latest
|
name: Router (ARMv7 musl)
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
with:
|
||||||
submodules: recursive
|
submodules: recursive
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@stable
|
||||||
|
|
||||||
|
- name: Cache cargo
|
||||||
|
uses: actions/cache@v4
|
||||||
with:
|
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
|
- uses: pnpm/action-setup@v4
|
||||||
with:
|
with:
|
||||||
@@ -33,8 +77,36 @@ jobs:
|
|||||||
cache: pnpm
|
cache: pnpm
|
||||||
cache-dependency-path: ui/pnpm-lock.yaml
|
cache-dependency-path: ui/pnpm-lock.yaml
|
||||||
|
|
||||||
- name: Install cross
|
- name: Build UI dist
|
||||||
run: cargo install cross --locked
|
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
|
- name: Cache cargo
|
||||||
uses: actions/cache@v4
|
uses: actions/cache@v4
|
||||||
@@ -43,57 +115,67 @@ jobs:
|
|||||||
~/.cargo/registry
|
~/.cargo/registry
|
||||||
~/.cargo/git
|
~/.cargo/git
|
||||||
target
|
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)
|
# Native GNU builds use the runner's glibc. Gitea remains the separate
|
||||||
run: |
|
# older-glibc release path for deployment hosts that need it.
|
||||||
cross build --release --target armv7-unknown-linux-musleabihf -p wakey -p wakey-agent
|
- name: Build control-plane natively
|
||||||
|
run: cargo build --release --locked --target ${{ matrix.target }} -p wakey-control-plane
|
||||||
|
|
||||||
- name: Build UI dist
|
- name: Upload control-plane binary
|
||||||
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
|
|
||||||
uses: actions/upload-artifact@v4
|
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:
|
with:
|
||||||
name: wakey-rootfs
|
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
|
- name: Upload control-plane bundles
|
||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: wakey-cc-bundles
|
name: wakey-cc-bundles
|
||||||
path: dist/wakey-cc-*unknown-linux-gnu.tgz
|
path: dist/wakey-cc-*unknown-linux-gnu.tgz
|
||||||
|
if-no-files-found: error
|
||||||
|
compression-level: 0
|
||||||
|
|
||||||
- name: Publish Release to GitHub
|
- name: Publish Release to GitHub
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
if: startsWith(github.ref, 'refs/tags/')
|
||||||
@@ -101,6 +183,7 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
tag_name: ${{ github.ref_name }}
|
tag_name: ${{ github.ref_name }}
|
||||||
|
fail_on_unmatched_files: true
|
||||||
files: |
|
files: |
|
||||||
dist/wakey-rootfs-*armv7-unknown-linux-musleabihf.tgz
|
dist/wakey-rootfs-*armv7-unknown-linux-musleabihf.tgz
|
||||||
dist/wakey-cc-*unknown-linux-gnu.tgz
|
dist/wakey-cc-*unknown-linux-gnu.tgz
|
||||||
Generated
+699
-900
File diff suppressed because it is too large.
Load diff
@@ -269,6 +269,26 @@ pnpm build
|
|||||||
|
|
||||||
Then start control-plane and open `/ui/` on the same host/port.
|
Then start control-plane and open `/ui/` on the same host/port.
|
||||||
|
|
||||||
|
For UI development with hot reload, run `pnpm --dir ui dev` and open
|
||||||
|
`http://127.0.0.1:5173/ui/` (Vite chooses another port if occupied). API requests
|
||||||
|
and terminal/event WebSockets proxy to `http://127.0.0.1:8080` by default.
|
||||||
|
Override the backend with `WAKEY_DEV_API_TARGET=http://127.0.0.1:18080 pnpm --dir ui dev`,
|
||||||
|
or set it in `ui/.env` using `ui/.env.example`. Restart Vite after changing it.
|
||||||
|
This setting is dev-only; built assets still use the serving CC's origin.
|
||||||
|
|
||||||
|
For the remote CC, first open an SSH tunnel:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
ssh -N -L 127.0.0.1:18080:127.0.0.1:23501 cloud
|
||||||
|
```
|
||||||
|
|
||||||
|
Use `http://127.0.0.1:18080` as the dev target. This connects directly to CC
|
||||||
|
through SSH, avoiding Cloudflare Access browser redirects/cookie handling in the
|
||||||
|
proxy. The dev server binds to loopback; do not expose this authenticated tunnel
|
||||||
|
through a public Vite server. Changes made in the dev UI affect the selected
|
||||||
|
backend, including production if that is what you tunnel to. HTTPS targets retain
|
||||||
|
certificate verification; the proxy does not add Cloudflare credentials.
|
||||||
|
|
||||||
## VPS Deploy (Manual Updates)
|
## VPS Deploy (Manual Updates)
|
||||||
|
|
||||||
Suggested layout on VPS:
|
Suggested layout on VPS:
|
||||||
|
|||||||
@@ -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,
|
the bucket boundary, in seconds. p95 is an estimate from those boundaries,
|
||||||
not an exact sort of recorded durations. Few operations make percentiles noisy.
|
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
|
CC relay duration includes waiting for the agent; agent execution duration
|
||||||
measures its dispatch separately. Do not sum both and call that user latency.
|
measures its dispatch separately. Do not sum both and call that user latency.
|
||||||
|
|
||||||
|
|||||||
@@ -75,7 +75,7 @@ pub async fn get_with_handle(
|
|||||||
NeighbourAddress::Inet6(v6) => ip = Some(IpAddr::from(v6)),
|
NeighbourAddress::Inet6(v6) => ip = Some(IpAddr::from(v6)),
|
||||||
_ => continue 'row,
|
_ => continue 'row,
|
||||||
},
|
},
|
||||||
NeighbourAttribute::LinkLocalAddress(bytes) => {
|
NeighbourAttribute::LinkLayerAddress(bytes) => {
|
||||||
mac = match bytes.len() {
|
mac = match bytes.len() {
|
||||||
6 => bytes.first_chunk::<6>().map(|&b| MacAddr::from(b)),
|
6 => bytes.first_chunk::<6>().map(|&b| MacAddr::from(b)),
|
||||||
8 => bytes.first_chunk::<8>().map(|&b| MacAddr::from(b)),
|
8 => bytes.first_chunk::<8>().map(|&b| MacAddr::from(b)),
|
||||||
|
|||||||
@@ -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:
|
- `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]`
|
- `./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.
|
- 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/*`.
|
- `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.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`.
|
- `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)
|
## 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`:
|
- 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`.
|
- **`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.
|
- **`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.
|
||||||
|
|||||||
@@ -67,6 +67,12 @@ From Windows PowerShell, using the WSL wrapper:
|
|||||||
|
|
||||||
Path control (binary and config are independent):
|
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
|
```powershell
|
||||||
./scripts/act_runner_wsl.ps1 -Action status `
|
./scripts/act_runner_wsl.ps1 -Action status `
|
||||||
-RunnerBin /home/lda/gitea-runner/act_runner `
|
-RunnerBin /home/lda/gitea-runner/act_runner `
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
param(
|
param(
|
||||||
[ValidateSet("update", "register", "start", "stop", "status")]
|
[ValidateSet("update", "register", "start", "stop", "status")]
|
||||||
[string]$Action = "start",
|
[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]$RunnerHome,
|
||||||
[string]$RunnerBin = "/home/lda/gitea-runner/act_runner",
|
[string]$RunnerBin = "/home/lda/gitea-runner/act_runner",
|
||||||
[string]$ConfigPath,
|
[string]$ConfigPath,
|
||||||
@@ -22,7 +22,7 @@ if (-not (Test-Path $scriptWin)) {
|
|||||||
throw "Missing Fedora runner helper: $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) {
|
if (-not $scriptLinux) {
|
||||||
throw "Failed to resolve WSL path for $scriptWin"
|
throw "Failed to resolve WSL path for $scriptWin"
|
||||||
}
|
}
|
||||||
@@ -30,21 +30,19 @@ if (-not $scriptLinux) {
|
|||||||
if (-not $RunnerHome) {
|
if (-not $RunnerHome) {
|
||||||
$repoRootWin = Split-Path -Parent $PSScriptRoot
|
$repoRootWin = Split-Path -Parent $PSScriptRoot
|
||||||
$defaultRunnerHomeWin = Join-Path $repoRootWin "ar_data/wsl_runner"
|
$defaultRunnerHomeWin = Join-Path $repoRootWin "ar_data/wsl_runner"
|
||||||
$RunnerHome = (& wsl.exe -d $Distro -e wslpath -a "$defaultRunnerHomeWin").Trim()
|
$RunnerHome = $defaultRunnerHomeWin
|
||||||
if (-not $RunnerHome) {
|
|
||||||
throw "Failed to resolve WSL runner home for $defaultRunnerHomeWin"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (-not $ConfigPath) {
|
if (-not $ConfigPath) {
|
||||||
$repoRootWin = Split-Path -Parent $PSScriptRoot
|
$repoRootWin = Split-Path -Parent $PSScriptRoot
|
||||||
$defaultConfigWin = Join-Path $repoRootWin "ar_data/wsl_runner/config.yaml"
|
$defaultConfigWin = Join-Path $repoRootWin "ar_data/wsl_runner/config.yaml"
|
||||||
$ConfigPath = (& wsl.exe -d $Distro -e wslpath -a "$defaultConfigWin").Trim()
|
$ConfigPath = $defaultConfigWin
|
||||||
if (-not $ConfigPath) {
|
|
||||||
throw "Failed to resolve WSL config path for $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 = @("bash", (Quote-ShArg $scriptLinux), (Quote-ShArg $Action), "--runner-home", (Quote-ShArg $RunnerHome))
|
||||||
$parts += @("--runner-bin", (Quote-ShArg $RunnerBin), "--config-path", (Quote-ShArg $ConfigPath))
|
$parts += @("--runner-bin", (Quote-ShArg $RunnerBin), "--config-path", (Quote-ShArg $ConfigPath))
|
||||||
if ($ServerUrl) { $parts += @("--server-url", (Quote-ShArg $ServerUrl)) }
|
if ($ServerUrl) { $parts += @("--server-url", (Quote-ShArg $ServerUrl)) }
|
||||||
|
|||||||
+80
-95
@@ -4,6 +4,9 @@
|
|||||||
param(
|
param(
|
||||||
[ValidateSet("cargo", "cross")]
|
[ValidateSet("cargo", "cross")]
|
||||||
[string]$Cargo = "cargo",
|
[string]$Cargo = "cargo",
|
||||||
|
[ValidateSet("wakey", "wakey-agent")]
|
||||||
|
[ValidateNotNullOrEmpty()]
|
||||||
|
[string[]]$Package = @("wakey", "wakey-agent"),
|
||||||
[string]$Pass,
|
[string]$Pass,
|
||||||
[string]$HostName = "192.168.100.1",
|
[string]$HostName = "192.168.100.1",
|
||||||
[int]$Port = 22,
|
[int]$Port = 22,
|
||||||
@@ -17,6 +20,7 @@ param(
|
|||||||
[string]$HostKey,
|
[string]$HostKey,
|
||||||
[switch]$ForcePassword,
|
[switch]$ForcePassword,
|
||||||
[switch]$SkipInitScript,
|
[switch]$SkipInitScript,
|
||||||
|
[switch]$BinariesOnly,
|
||||||
[switch]$Restart = $true,
|
[switch]$Restart = $true,
|
||||||
[switch]$Quiet = $true
|
[switch]$Quiet = $true
|
||||||
)
|
)
|
||||||
@@ -26,109 +30,90 @@ try { $PSStyle.OutputRendering = 'Host' } catch {}
|
|||||||
|
|
||||||
. "$PSScriptRoot/lib.ps1"
|
. "$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.
|
# Backward compatibility: allow HostName in the form host:port.
|
||||||
$hostPort = Split-HostPort -HostName $HostName -DefaultPort $Port
|
$hostPort = Split-HostPort -HostName $HostName -DefaultPort $Port
|
||||||
$HostName = $hostPort.Host
|
$HostName = $hostPort.Host
|
||||||
$Port = $hostPort.Port
|
$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
|
$repoRoot = Split-Path -Parent $PSScriptRoot
|
||||||
Push-Location $repoRoot
|
Push-Location $repoRoot
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Write-Host "[build] $Cargo build --release --target $Target -p wakey -p wakey-agent" -ForegroundColor Cyan
|
Build-Packages ($Package | Select-Object -Unique)
|
||||||
. "$Cargo" build --release --target $Target -p wakey -p wakey-agent
|
$plan = New-DeploymentPlan $Package
|
||||||
if ($LASTEXITCODE -ne 0) { throw "$Cargo build failed ($LASTEXITCODE)" }
|
# Prepare before uploading so missing local helpers fail without remote changes.
|
||||||
|
$deployment = Get-RemoteDeployment $plan
|
||||||
$localBin = Join-Path $repoRoot "target/$Target/release/$BinName"
|
Send-DeploymentFiles $plan
|
||||||
$localAgentBin = Join-Path $repoRoot "target/$Target/release/$AgentBinName"
|
Invoke-Ssh -Cmd $deployment.Command -Stdin $deployment.Script -User $User -Remote $HostName -Pass $Pass -Port $Port -Quiet:$Quiet -ForcePassword:$ForcePassword
|
||||||
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
|
|
||||||
|
|
||||||
Write-Host "done ✔" -ForegroundColor Green
|
Write-Host "done ✔" -ForegroundColor Green
|
||||||
}
|
}
|
||||||
finally {
|
finally {
|
||||||
|
|||||||
+54
-6
@@ -27,8 +27,15 @@ function Test-IsWsl {
|
|||||||
return $false
|
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 {
|
function ConvertTo-WslPath {
|
||||||
param([string]$WindowsPath)
|
param(
|
||||||
|
[Alias('Path')]
|
||||||
|
[string]$WindowsPath,
|
||||||
|
[string]$Distro
|
||||||
|
)
|
||||||
|
|
||||||
if ([string]::IsNullOrWhiteSpace($WindowsPath)) {
|
if ([string]::IsNullOrWhiteSpace($WindowsPath)) {
|
||||||
return $WindowsPath
|
return $WindowsPath
|
||||||
@@ -39,6 +46,36 @@ function ConvertTo-WslPath {
|
|||||||
return $WindowsPath
|
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)
|
# Handle Windows path (e.g., C:\path\to\file -> /mnt/c/path/to/file)
|
||||||
if ($WindowsPath -match '^([A-Z]):(.*)$') {
|
if ($WindowsPath -match '^([A-Z]):(.*)$') {
|
||||||
$drive = $matches[1].ToLower()
|
$drive = $matches[1].ToLower()
|
||||||
@@ -154,7 +191,7 @@ function Get-SshpassCommand {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function Invoke-Ext {
|
function Invoke-Ext {
|
||||||
param($Exe, $Arguments, $Label)
|
param($Exe, $Arguments, $Label, [string]$Stdin)
|
||||||
$displayArgs = $Arguments.Clone()
|
$displayArgs = $Arguments.Clone()
|
||||||
for ($i = 0; $i -lt $displayArgs.Count; $i++) {
|
for ($i = 0; $i -lt $displayArgs.Count; $i++) {
|
||||||
if ($displayArgs[$i] -eq '-pw' -and ($i + 1) -lt $displayArgs.Count) {
|
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
|
Write-Host ("[{0}] {1} {2}" -f $Label, $Exe, ($displayArgs -join ' ')) -ForegroundColor Cyan
|
||||||
|
|
||||||
|
# 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
|
$out = & $Exe @Arguments 2>&1
|
||||||
|
}
|
||||||
if ($LASTEXITCODE -ne 0) {
|
if ($LASTEXITCODE -ne 0) {
|
||||||
if ($Exe -like '*sshpass*' -and $LASTEXITCODE -eq 6) {
|
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)
|
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 {
|
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
|
$Cmd = Normalize-LineEndings $Cmd
|
||||||
|
$inputArgs = @{}
|
||||||
|
if ($PSBoundParameters.ContainsKey('Stdin')) {
|
||||||
|
$inputArgs.Stdin = Normalize-LineEndings $Stdin
|
||||||
|
}
|
||||||
|
|
||||||
$isWin = [bool]$IsWindows
|
$isWin = [bool]$IsWindows
|
||||||
$isWsl = Test-IsWsl
|
$isWsl = Test-IsWsl
|
||||||
@@ -260,7 +308,7 @@ function Invoke-Ssh {
|
|||||||
if ($Pass) { $arguments += @('-pw', $Pass) }
|
if ($Pass) { $arguments += @('-pw', $Pass) }
|
||||||
$dest = if ($User) { "$User@$Remote" } else { $Remote }
|
$dest = if ($User) { "$User@$Remote" } else { $Remote }
|
||||||
$arguments += $dest, $Cmd
|
$arguments += $dest, $Cmd
|
||||||
Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh'
|
Invoke-Ext -Exe $plink.Path -Arguments $arguments -Label 'ssh' @inputArgs
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
$arguments = @()
|
$arguments = @()
|
||||||
@@ -282,7 +330,7 @@ function Invoke-Ssh {
|
|||||||
$sshpassExe = Get-SshpassCommand
|
$sshpassExe = Get-SshpassCommand
|
||||||
if ($sshpassExe) {
|
if ($sshpassExe) {
|
||||||
$wrapped = @('-p', $Pass, 'ssh') + $arguments
|
$wrapped = @('-p', $Pass, 'ssh') + $arguments
|
||||||
Invoke-Ext -Exe $sshpassExe -Arguments $wrapped -Label 'ssh'
|
Invoke-Ext -Exe $sshpassExe -Arguments $wrapped -Label 'ssh' @inputArgs
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if (-not $script:WarnedNoSshpass) {
|
if (-not $script:WarnedNoSshpass) {
|
||||||
@@ -301,7 +349,7 @@ function Invoke-Ssh {
|
|||||||
throw 'ForcePassword requires sshpass'
|
throw 'ForcePassword requires sshpass'
|
||||||
}
|
}
|
||||||
|
|
||||||
Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh'
|
Invoke-Ext -Exe 'ssh' -Arguments $arguments -Label 'ssh' @inputArgs
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1,41 +1,80 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# Usage: remote_deploy_wakey.sh <bin_tmp> <dest_path> [restart_flag]
|
# Usage: remote_deploy_wakey.sh <bin_tmp> <dest_path> [restart_flag]
|
||||||
# restart_flag: 1 (default) to stop/start, 0 to only replace file.
|
# 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
|
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
|
# 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"
|
||||||
|
|
||||||
|
if [ -z "$BIN_TMP" ] || [ -z "$DEST" ]; then
|
||||||
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
|
echo "usage: $0 <bin_tmp> <dest_path> [restart_flag]" >&2
|
||||||
exit 2
|
exit 2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
[ -f "$BIN_TMP" ] || {
|
[ -f "$BIN_TMP" ] || {
|
||||||
echo "tmp binary not found: $BIN_TMP" >&2
|
echo "tmp binary not found: $BIN_TMP" >&2
|
||||||
exit 1
|
exit 1
|
||||||
}
|
}
|
||||||
chmod +x "$BIN_TMP" || true
|
chmod +x "$BIN_TMP" || true
|
||||||
|
|
||||||
if [ "$RESTART" = "1" ]; then
|
if [ "$RESTART" = "1" ]; then
|
||||||
if [ -x "$INIT" ]; then
|
if [ -x "$INIT" ]; then
|
||||||
"$INIT" stop || true
|
"$INIT" stop || true
|
||||||
elif [ -x "$KILL" ]; then
|
elif [ -x "$KILL" ]; then
|
||||||
sh "$KILL" || true
|
sh "$KILL" || true
|
||||||
fi
|
fi
|
||||||
fi
|
fi
|
||||||
|
|
||||||
mv -f "$BIN_TMP" "$DEST"
|
mv -f "$BIN_TMP" "$DEST"
|
||||||
|
|
||||||
if [ "$RESTART" = "1" ]; then
|
if [ "$RESTART" = "1" ]; then
|
||||||
if [ -x "$INIT" ]; then
|
if [ -x "$INIT" ]; then
|
||||||
"$INIT" start || "$INIT" restart || true
|
"$INIT" start || "$INIT" restart || true
|
||||||
else
|
else
|
||||||
nohup "$DEST" >/dev/null 2>&1 &
|
nohup "$DEST" >/dev/null 2>&1 &
|
||||||
fi
|
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"
|
||||||
|
}
|
||||||
|
|
||||||
|
if [ "${1:-}" != '--batch' ]; then
|
||||||
|
deploy_binary "$@"
|
||||||
|
exit
|
||||||
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
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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'
|
||||||
@@ -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'
|
||||||
@@ -0,0 +1,2 @@
|
|||||||
|
# Vite dev server only. Use an origin, without /ui or /api suffixes.
|
||||||
|
WAKEY_DEV_API_TARGET=http://127.0.0.1:8080
|
||||||
+7
-7
@@ -15,13 +15,13 @@
|
|||||||
"@base-ui/react": "^1.4.0",
|
"@base-ui/react": "^1.4.0",
|
||||||
"@fontsource-variable/inter": "^5.2.8",
|
"@fontsource-variable/inter": "^5.2.8",
|
||||||
"@tailwindcss/vite": "^4.2.2",
|
"@tailwindcss/vite": "^4.2.2",
|
||||||
"@xterm/addon-clipboard": "0.3.0-beta.301",
|
"@xterm/addon-clipboard": "0.3.0-beta.313",
|
||||||
"@xterm/addon-fit": "0.12.0-beta.301",
|
"@xterm/addon-fit": "0.12.0-beta.311",
|
||||||
"@xterm/addon-image": "0.10.0-beta.301",
|
"@xterm/addon-image": "0.10.0-beta.310",
|
||||||
"@xterm/addon-unicode-graphemes": "0.5.0-beta.301",
|
"@xterm/addon-unicode-graphemes": "0.5.0-beta.310",
|
||||||
"@xterm/addon-unicode11": "0.10.0-beta.301",
|
"@xterm/addon-unicode11": "0.10.0-beta.310",
|
||||||
"@xterm/addon-web-links": "0.13.0-beta.301",
|
"@xterm/addon-web-links": "0.13.0-beta.310",
|
||||||
"@xterm/xterm": "6.1.0-beta.301",
|
"@xterm/xterm": "6.1.0-beta.315",
|
||||||
"class-variance-authority": "^0.7.1",
|
"class-variance-authority": "^0.7.1",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"lucide-react": "^1.8.0",
|
"lucide-react": "^1.8.0",
|
||||||
|
|||||||
Generated
+47
-47
@@ -18,26 +18,26 @@ importers:
|
|||||||
specifier: ^4.2.2
|
specifier: ^4.2.2
|
||||||
version: 4.2.2([email protected](@types/[email protected])([email protected]))
|
version: 4.2.2([email protected](@types/[email protected])([email protected]))
|
||||||
'@xterm/addon-clipboard':
|
'@xterm/addon-clipboard':
|
||||||
specifier: 0.3.0-beta.301
|
specifier: 0.3.0-beta.313
|
||||||
version: 0.3.0-beta.301(@xterm/[email protected]01)
|
version: 0.3.0-beta.313(@xterm/[email protected]5)
|
||||||
'@xterm/addon-fit':
|
'@xterm/addon-fit':
|
||||||
specifier: 0.12.0-beta.301
|
specifier: 0.12.0-beta.311
|
||||||
version: 0.12.0-beta.301(@xterm/[email protected]01)
|
version: 0.12.0-beta.311(@xterm/[email protected]5)
|
||||||
'@xterm/addon-image':
|
'@xterm/addon-image':
|
||||||
specifier: 0.10.0-beta.301
|
specifier: 0.10.0-beta.310
|
||||||
version: 0.10.0-beta.301(@xterm/[email protected]01)
|
version: 0.10.0-beta.310(@xterm/[email protected]5)
|
||||||
'@xterm/addon-unicode-graphemes':
|
'@xterm/addon-unicode-graphemes':
|
||||||
specifier: 0.5.0-beta.301
|
specifier: 0.5.0-beta.310
|
||||||
version: 0.5.0-beta.301(@xterm/[email protected]01)
|
version: 0.5.0-beta.310(@xterm/[email protected]5)
|
||||||
'@xterm/addon-unicode11':
|
'@xterm/addon-unicode11':
|
||||||
specifier: 0.10.0-beta.301
|
specifier: 0.10.0-beta.310
|
||||||
version: 0.10.0-beta.301(@xterm/[email protected]01)
|
version: 0.10.0-beta.310(@xterm/[email protected]5)
|
||||||
'@xterm/addon-web-links':
|
'@xterm/addon-web-links':
|
||||||
specifier: 0.13.0-beta.301
|
specifier: 0.13.0-beta.310
|
||||||
version: 0.13.0-beta.301(@xterm/[email protected]01)
|
version: 0.13.0-beta.310(@xterm/[email protected]5)
|
||||||
'@xterm/xterm':
|
'@xterm/xterm':
|
||||||
specifier: 6.1.0-beta.301
|
specifier: 6.1.0-beta.315
|
||||||
version: 6.1.0-beta.301
|
version: 6.1.0-beta.315
|
||||||
class-variance-authority:
|
class-variance-authority:
|
||||||
specifier: ^0.7.1
|
specifier: ^0.7.1
|
||||||
version: 0.7.1
|
version: 0.7.1
|
||||||
@@ -836,38 +836,38 @@ packages:
|
|||||||
peerDependencies:
|
peerDependencies:
|
||||||
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
vite: ^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0
|
||||||
|
|
||||||
'@xterm/[email protected]01':
|
'@xterm/[email protected]3':
|
||||||
resolution: {integrity: sha512-DRL0EybPhWzgndaTeakVbh/HvJF7yzxRtfNCnIudFZQaGugFYsEUZNOaV2NZev8z5kLwor6miH1j3DNMI8Paxg==}
|
resolution: {integrity: sha512-Jvt9vzccZ1vQ0VHsQsvW0j9TGThsCeY9T3x2TP+pHhYB5NVgOIlKhNiRRGTdMxsJd8eW3fxEpTGVhf88aeb8Ug==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@xterm/xterm': ^6.1.0-beta.301
|
'@xterm/xterm': ^6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01':
|
'@xterm/[email protected]11':
|
||||||
resolution: {integrity: sha512-MukRyJLAFQrW/++aM209jBZ8G9J/Ioe4SGS6c3ba87v+zYuX/I66JrDB7sVfykQ9t7DlT+c1MWl1jTlX2kyx1w==}
|
resolution: {integrity: sha512-M40mAWDoAVKz+0zs5ipmBJ+tNilw8CzUbbGIX05NANmTiw0MS47FYeJ4d2El4zdvFphDJjtVLrxTKXuz2wUM6Q==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@xterm/xterm': ^6.1.0-beta.304
|
'@xterm/xterm': ^6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01':
|
'@xterm/[email protected]0':
|
||||||
resolution: {integrity: sha512-PJZL4GlQ1ReIgbffFMaOAFxc3wOit21/G2UEM82RpH3hNHjn6MH8ilz3ToDeBjYuOqU7ienBaeIuMbNktX4/HA==}
|
resolution: {integrity: sha512-8fzgYV7HC9iWlw8ChZgnPmCXeBoYkC3Kyo9C0vyOQWD7Cm/bebKNNWhzNlDt5Qxbv+UBPbgkcO4i3dTYnggkRw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@xterm/xterm': ^6.1.0-beta.304
|
'@xterm/xterm': ^6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01':
|
'@xterm/[email protected]0':
|
||||||
resolution: {integrity: sha512-EBZIZYS6RItWTGO/KpLqV6y3IKGYyugbP07lK0x6G7185FVA61L2t0/XVtVEQNpczsbPo8zAxBhpoV72TP8CYw==}
|
resolution: {integrity: sha512-oCvQV7rKNekf7KcNdVicPhOo47W160QS6tIhXG7VpQ9OKGKTrAnMAxD+fr/nTlbd3h86+zvMqJpfd7NKEm/fYA==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@xterm/xterm': ^6.1.0-beta.304
|
'@xterm/xterm': ^6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01':
|
'@xterm/[email protected]0':
|
||||||
resolution: {integrity: sha512-SS5BzfbvfFIikE4xZFAtHu6n0Kl5R1ZCPx41RL2WULLSE+HFo/WmYee53tMxNK/QUjpcQWepeVB7HebCPhHnig==}
|
resolution: {integrity: sha512-kQQIWMaQX7ViV1ofrYQnydR4/yNqCgOXYSYXG+x9pvNFU5/3KzZbqeQOXuM1y3cZYzR+Mw2cy3oRp/ssnK9cww==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@xterm/xterm': ^6.1.0-beta.304
|
'@xterm/xterm': ^6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01':
|
'@xterm/[email protected]0':
|
||||||
resolution: {integrity: sha512-BpY297K+FGLsTQemnA6uOg6hk85DwVuUvWCqE9rIAcDxOOC7fP9GKAZTIfaEos/EZk2Ei23vD6w0P5Br6+RU7Q==}
|
resolution: {integrity: sha512-sxY3n5N3BCafchHLpqZSP44SncYhgt7rYSqk797naAeTJgymL3kG7fNIRob6SeV3nvf3MH0EIB6B3xxATtHYsQ==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
'@xterm/xterm': ^6.1.0-beta.304
|
'@xterm/xterm': ^6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01':
|
'@xterm/[email protected]5':
|
||||||
resolution: {integrity: sha512-y1pH42+Mx+h5E2xDgyilYXBKYkn8/6+anToiTmIt8gVww/2gHFqheK0vW+m9LADQsZ5AFs+SQvcK+tOyxWfRJA==}
|
resolution: {integrity: sha512-w7MeKxi6vbxE3IOFRiwgugkM6FZonE9gF9KyJ0OiE5HbYZOm38bz+CTGZg3+wqA6porJnHfjzX1KaihInP+iWg==}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
resolution: {integrity: sha512-5cvg6CtKwfgdmVqY1WIiXKc3Q1bkRqGLi+2W/6ao+6Y7gu/RCwRuAhGEzh5B4KlszSuTLgZYuqFqo5bImjNKng==}
|
||||||
@@ -2862,31 +2862,31 @@ snapshots:
|
|||||||
transitivePeerDependencies:
|
transitivePeerDependencies:
|
||||||
- supports-color
|
- supports-color
|
||||||
|
|
||||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
'@xterm/[email protected]3(@xterm/[email protected]5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xterm/xterm': 6.1.0-beta.301
|
'@xterm/xterm': 6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
'@xterm/[email protected]11(@xterm/[email protected]5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xterm/xterm': 6.1.0-beta.301
|
'@xterm/xterm': 6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
'@xterm/[email protected]0(@xterm/[email protected]5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xterm/xterm': 6.1.0-beta.301
|
'@xterm/xterm': 6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
'@xterm/[email protected]0(@xterm/[email protected]5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xterm/xterm': 6.1.0-beta.301
|
'@xterm/xterm': 6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
'@xterm/[email protected]0(@xterm/[email protected]5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xterm/xterm': 6.1.0-beta.301
|
'@xterm/xterm': 6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01(@xterm/[email protected]01)':
|
'@xterm/[email protected]0(@xterm/[email protected]5)':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@xterm/xterm': 6.1.0-beta.301
|
'@xterm/xterm': 6.1.0-beta.315
|
||||||
|
|
||||||
'@xterm/[email protected]01': {}
|
'@xterm/[email protected]5': {}
|
||||||
|
|
||||||
[email protected]:
|
[email protected]:
|
||||||
dependencies:
|
dependencies:
|
||||||
|
|||||||
@@ -0,0 +1,97 @@
|
|||||||
|
import assert from "node:assert/strict";
|
||||||
|
import { createServer as createHttpServer, request } from "node:http";
|
||||||
|
import { once } from "node:events";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
import test from "node:test";
|
||||||
|
import { createServer } from "vite";
|
||||||
|
|
||||||
|
test("dev proxy preserves API requests and WebSocket upgrades under /ui base", async () => {
|
||||||
|
const sockets = new Set();
|
||||||
|
const backend = createHttpServer(async (req, res) => {
|
||||||
|
const chunks = [];
|
||||||
|
for await (const chunk of req) chunks.push(chunk);
|
||||||
|
res.setHeader("Content-Type", "application/json");
|
||||||
|
res.end(
|
||||||
|
JSON.stringify({
|
||||||
|
method: req.method,
|
||||||
|
url: req.url,
|
||||||
|
body: Buffer.concat(chunks).toString(),
|
||||||
|
host: req.headers.host,
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
});
|
||||||
|
backend.on("connection", (socket) => {
|
||||||
|
sockets.add(socket);
|
||||||
|
socket.on("close", () => sockets.delete(socket));
|
||||||
|
});
|
||||||
|
backend.on("upgrade", (req, socket) => {
|
||||||
|
socket.write(
|
||||||
|
"HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n",
|
||||||
|
);
|
||||||
|
});
|
||||||
|
backend.listen(0, "127.0.0.1");
|
||||||
|
await once(backend, "listening");
|
||||||
|
const target = `http://127.0.0.1:${backend.address().port}`;
|
||||||
|
const previous = process.env.WAKEY_DEV_API_TARGET;
|
||||||
|
process.env.WAKEY_DEV_API_TARGET = target;
|
||||||
|
let vite;
|
||||||
|
try {
|
||||||
|
vite = await createServer({
|
||||||
|
root: fileURLToPath(new URL("../", import.meta.url)),
|
||||||
|
server: { port: 0 },
|
||||||
|
});
|
||||||
|
await vite.listen();
|
||||||
|
const origin = `http://127.0.0.1:${vite.httpServer.address().port}`;
|
||||||
|
const response = await fetch(
|
||||||
|
`${origin}/api/v1/control/fleet/devices/refresh?test=1`,
|
||||||
|
{
|
||||||
|
method: "POST",
|
||||||
|
headers: { "Content-Type": "application/json" },
|
||||||
|
body: '{"agent_ids":[]}',
|
||||||
|
},
|
||||||
|
);
|
||||||
|
assert.equal(response.status, 200);
|
||||||
|
assert.deepEqual(await response.json(), {
|
||||||
|
method: "POST",
|
||||||
|
url: "/api/v1/control/fleet/devices/refresh?test=1",
|
||||||
|
body: '{"agent_ids":[]}',
|
||||||
|
host: new URL(target).host,
|
||||||
|
});
|
||||||
|
assert.equal((await fetch(`${origin}/healthz`)).status, 200);
|
||||||
|
const page = await fetch(`${origin}/ui/`);
|
||||||
|
assert.equal(page.status, 200);
|
||||||
|
assert.match(await page.text(), /@vite\/client/);
|
||||||
|
await new Promise((resolve, reject) => {
|
||||||
|
const req = request(
|
||||||
|
`${origin}/api/v1/control/terminals/events/ws?test=1`,
|
||||||
|
{
|
||||||
|
headers: {
|
||||||
|
Connection: "Upgrade",
|
||||||
|
Upgrade: "websocket",
|
||||||
|
"Sec-WebSocket-Version": "13",
|
||||||
|
"Sec-WebSocket-Key": "dGhlIHNhbXBsZSBub25jZQ==",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
);
|
||||||
|
req.on("upgrade", (res, socket) => {
|
||||||
|
socket.destroy();
|
||||||
|
resolve();
|
||||||
|
});
|
||||||
|
req.on("response", (res) => {
|
||||||
|
res.resume();
|
||||||
|
reject(new Error(`Expected WS upgrade, got ${res.statusCode}`));
|
||||||
|
});
|
||||||
|
req.on("error", reject);
|
||||||
|
req.setTimeout(5000, () =>
|
||||||
|
req.destroy(new Error("WS upgrade timed out")),
|
||||||
|
);
|
||||||
|
req.end();
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
if (previous === undefined) delete process.env.WAKEY_DEV_API_TARGET;
|
||||||
|
else process.env.WAKEY_DEV_API_TARGET = previous;
|
||||||
|
for (const socket of sockets) socket.destroy();
|
||||||
|
if (vite) await vite.close();
|
||||||
|
await new Promise((resolve) => backend.close(resolve));
|
||||||
|
}
|
||||||
|
});
|
||||||
+33
-2
@@ -1,4 +1,4 @@
|
|||||||
import { defineConfig } from "vite";
|
import { defineConfig, loadEnv } from "vite";
|
||||||
import react from "@vitejs/plugin-react";
|
import react from "@vitejs/plugin-react";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
import { fileURLToPath } from "node:url";
|
import { fileURLToPath } from "node:url";
|
||||||
@@ -21,7 +21,26 @@ const ReactCompilerConfig = {
|
|||||||
runtimeModule: "react-compiler-runtime", // Redirects the missing specifier
|
runtimeModule: "react-compiler-runtime", // Redirects the missing specifier
|
||||||
};
|
};
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig(({ command, mode, isPreview }) => {
|
||||||
|
const env = loadEnv(mode, __dirname, "WAKEY_DEV_");
|
||||||
|
const target = env.WAKEY_DEV_API_TARGET || "http://127.0.0.1:8080";
|
||||||
|
const dev = command === "serve" && !isPreview;
|
||||||
|
if (dev) {
|
||||||
|
const url = new URL(target);
|
||||||
|
if (
|
||||||
|
!["http:", "https:"].includes(url.protocol) ||
|
||||||
|
url.username ||
|
||||||
|
url.password ||
|
||||||
|
url.pathname !== "/" ||
|
||||||
|
url.search ||
|
||||||
|
url.hash
|
||||||
|
) {
|
||||||
|
throw new Error(
|
||||||
|
"WAKEY_DEV_API_TARGET must be an HTTP(S) origin without credentials or a path",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return {
|
||||||
plugins: [
|
plugins: [
|
||||||
react({
|
react({
|
||||||
babel: {
|
babel: {
|
||||||
@@ -39,4 +58,16 @@ export default defineConfig({
|
|||||||
build: {
|
build: {
|
||||||
sourcemap: debugEnabled(),
|
sourcemap: debugEnabled(),
|
||||||
},
|
},
|
||||||
|
// Keep browser requests same-origin, including terminal/event WebSockets.
|
||||||
|
// WAKEY_DEV_* is read only by this config, never exposed as VITE_* client data.
|
||||||
|
server: dev
|
||||||
|
? {
|
||||||
|
host: "127.0.0.1",
|
||||||
|
proxy: {
|
||||||
|
"^/api/": { target, changeOrigin: true, ws: true },
|
||||||
|
"^/healthz$": { target, changeOrigin: true },
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
};
|
||||||
});
|
});
|
||||||
@@ -338,6 +338,12 @@ impl Instruments {
|
|||||||
duration: meter
|
duration: meter
|
||||||
.f64_histogram("wakey.operation.duration")
|
.f64_histogram("wakey.operation.duration")
|
||||||
.with_unit("s")
|
.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(),
|
.build(),
|
||||||
snapshot_devices: meter.u64_counter("wakey.snapshot.devices").build(),
|
snapshot_devices: meter.u64_counter("wakey.snapshot.devices").build(),
|
||||||
terminal_active: meter.i64_up_down_counter("wakey.terminal.active").build(),
|
terminal_active: meter.i64_up_down_counter("wakey.terminal.active").build(),
|
||||||
|
|||||||
@@ -409,6 +409,37 @@ async fn exports_all_signals_and_preserves_remote_command_parent() {
|
|||||||
assert!(names.contains(&name), "missing {name}");
|
assert!(names.contains(&name), "missing {name}");
|
||||||
}
|
}
|
||||||
let latest = metrics.last().unwrap();
|
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 [
|
for (name, unit, expected) in [
|
||||||
(
|
(
|
||||||
|
|||||||
Reference in new issue
Block a user