The start of greatness is hella rocky

This commit is contained in:
lda
2026-04-06 23:46:07 +07:00 Unverified
parent 93a88741f9
commit 65111d7895
20 changed files with 2225 additions and 37 deletions
+3 -3
View File
@@ -9,7 +9,7 @@ on:
jobs: jobs:
build: build:
runs-on: [self-hosted, windows] runs-on: [self-hosted, linux, fedora, wsl, release]
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v4
@@ -28,12 +28,12 @@ jobs:
~/.cargo/registry ~/.cargo/registry
~/.cargo/git ~/.cargo/git
target target
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }} key: linux-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Build (armv7-musl) - name: Build (armv7-musl)
shell: pwsh shell: pwsh
run: | run: |
cargo build --release --target armv7-unknown-linux-musleabihf cargo build --release --target armv7-unknown-linux-musleabihf -p wakey -p wakey-agent
- name: Package rootfs tarball - name: Package rootfs tarball
shell: pwsh shell: pwsh
Generated
+1063 -8
View File
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -41,7 +41,7 @@ very-smart-parsing = [
] # this is the a-bit-redundant parse thing that copilot made ] # this is the a-bit-redundant parse thing that copilot made
[workspace] [workspace]
members = ["ipjs", "wakey-core", "wakey-linux"] members = ["ipjs", "wakey-agent", "wakey-core", "wakey-linux"]
[dependencies.lda-ipjs] [dependencies.lda-ipjs]
path = "ipjs" path = "ipjs"
+15 -7
View File
@@ -4,13 +4,13 @@ This folder has small helpers for build, CI, and router install. Keep it simple;
## Quick start (recommended) ## Quick start (recommended)
1. Start your Gitea runner on this Windows PC (if not already running): 1. Start your Fedora/WSL Gitea runner (recommended release path):
```powershell ```powershell
./scripts/act_runner.ps1 ./scripts/act_runner_wsl.ps1 -Action start -Attach
``` ```
If first-time, it prints the exact register command. Run it once, check labels include `windows:host,self-hosted`, then rerun the script. If first-time, use the runbook in [`WSL_RUNNER.md`](./WSL_RUNNER.md) to update and register the Fedora runner first.
2. Publish, tag, and push (recommended): 2. Publish, tag, and push (recommended):
@@ -18,7 +18,7 @@ This folder has small helpers for build, CI, and router install. Keep it simple;
./scripts/publish.ps1 -Tag ./scripts/publish.ps1 -Tag
``` ```
This will bump the version (if you specify one), build, ensure the runner is started, tag, and push. To also publish to the registry, add `-Publish`. This will bump the version (if you specify one), build locally, tag, push, and start the Fedora/WSL runner in `--once` mode by default. To also publish to the registry, add `-Publish`.
3. Install on the router 3. Install on the router
@@ -38,6 +38,8 @@ This folder has small helpers for build, CI, and router install. Keep it simple;
## Scripts overview ## Scripts overview
- `act_runner.ps1` — Start/seed the local Gitea runner. Use `-Attach` to see logs, `-ForceConfigure` to register non-interactively. - `act_runner.ps1` — Start/seed the local Gitea runner. Use `-Attach` to see logs, `-ForceConfigure` to register non-interactively.
- `act_runner_wsl.ps1` — Windows PowerShell wrapper that controls the Fedora/WSL runner.
- `act_runner_fedora.sh` — Linux/Fedora helper used inside WSL for update/register/start/stop/status.
- `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.
@@ -92,10 +94,16 @@ Implementation notes:
## CI (Gitea) ## CI (Gitea)
- Defined in `.gitea/workflows/release.yml`. - Defined in `.gitea/workflows/release.yml`.
- Runs on your `self-hosted, windows` runner. - Release job now targets your Fedora/WSL runner labels: `self-hosted`, `linux`, `fedora`, `wsl`, `release`.
- Steps: cross build → package rootfs → upload artifact (v3) → publish a release and attach tgz. - Steps: Linux-native build of `wakey` + `wakey-agent` → package rootfs → upload artifact (v3) → publish a release and attach tgz.
- Requires `secrets.GITEA_TOKEN` in the repo to publish the release. - Requires `secrets.GITEA_TOKEN` in the repo to publish the release.
## Runner migration
- Fedora/WSL is now the intended release-builder path.
- The Windows runner can stay registered in parallel during migration and fallback.
- The detailed operator runbook is in [`WSL_RUNNER.md`](./WSL_RUNNER.md).
## Local build/install (manual path) ## Local build/install (manual path)
```powershell ```powershell
@@ -121,4 +129,4 @@ Or you can use the script:
./scripts/dev_push.ps1 -Pass <password> ./scripts/dev_push.ps1 -Pass <password>
``` ```
Thats it. Keep the flow: start runner → tag push → install. Thats it. Keep the flow: start Fedora/WSL runner → tag push → install.
+131
View File
@@ -0,0 +1,131 @@
# Fedora / WSL Gitea Runner
This repo now expects Linux/router release builds to run on a Fedora 43 runner
inside WSL, not on the Windows host runner.
The goal is simple:
- build `wakey` and `wakey-agent` in a Linux-native environment
- stop fighting Windows-host ARM/musl/TLS toolchain issues
- keep PowerShell on Windows as the operator front door
## Runner layout
Recommended WSL location:
```text
/mnt/c/Users/Admin/Documents/realshit/wakey/ar_data/wsl_runner
```
This matches the default used by `act_runner_wsl.ps1` and
`act_runner_fedora.sh`, so WSL runner config is isolated at
`ar_data/wsl_runner/config.yaml` in this repo.
Recommended labels:
```text
self-hosted,linux,fedora,wsl,release:host
```
The Windows runner can stay registered during migration as fallback.
## Fedora setup
Install the basics in Fedora 43:
```bash
sudo dnf install -y git curl tar gzip findutils python3
sudo dnf install -y powershell
```
Install Rust and the router target:
```bash
rustup target add armv7-unknown-linux-musleabihf
```
Install the native cross toolchain pieces you need for Linux/ARM router builds.
The exact package names can vary over time, so confirm the current Fedora names
for:
- ARM Linux GCC
- musl development/toolchain support
- OpenSSL/ring/TLS build prerequisites if needed by your dependency graph
## Runner lifecycle
From Windows PowerShell, using the WSL wrapper:
```powershell
./scripts/act_runner_wsl.ps1 -Action update
./scripts/act_runner_wsl.ps1 -Action register -ServerUrl https://git.ldlda.com/ -Token <runner-token>
./scripts/act_runner_wsl.ps1 -Action start -Attach
./scripts/act_runner_wsl.ps1 -Action start -Once
./scripts/act_runner_wsl.ps1 -Action status
./scripts/act_runner_wsl.ps1 -Action stop
```
Path control (binary and config are independent):
```powershell
./scripts/act_runner_wsl.ps1 -Action status `
-RunnerBin /home/lda/gitea-runner/act_runner `
-ConfigPath /mnt/c/Users/Admin/Documents/realshit/wakey/ar_data/wsl_runner/config.yaml
```
If your distro name is not `FedoraLinux-43`, set it explicitly:
```powershell
./scripts/act_runner_wsl.ps1 -Distro FedoraLinux-43 -Action status
```
Or set:
```powershell
$env:WAKEY_WSL_DISTRO = "FedoraLinux-43"
```
Inside Fedora directly, you can also use:
```bash
./scripts/act_runner_fedora.sh update
./scripts/act_runner_fedora.sh register --server-url https://git.ldlda.com/ --token <runner-token>
./scripts/act_runner_fedora.sh start --attach
./scripts/act_runner_fedora.sh start --once
./scripts/act_runner_fedora.sh status
./scripts/act_runner_fedora.sh stop
```
## Release flow
The release workflow is expected to run on the Fedora/WSL runner labels:
```text
self-hosted + linux + fedora + wsl + release
```
It builds:
- `wakey`
- `wakey-agent`
and packages both into the rootfs tarball.
From Windows, the intended operator flow remains:
```powershell
./scripts/publish.ps1 -Tag
```
That script should trigger the WSL runner in `--once` mode by default.
## Rollback
If the Fedora runner path is broken:
1. stop using the WSL runner labels in the workflow
2. temporarily point the workflow back to the Windows runner labels
3. use the existing Windows runner as fallback while fixing the Fedora path
Do not delete the Windows runner until the Fedora path has already produced at
least one successful full release and one successful router install.
+4 -4
View File
@@ -1,12 +1,12 @@
# Starts the Gitea act_runner if not already running. # Starts the Windows-host Gitea act_runner if not already running.
# Usage: ./scripts/act_runner.ps1 -Config "C:/Users/Admin/Documents/gitea/runner.yaml" -RunnerPath "C:/Users/Admin/Documents/gitea/act_runner.exe" -ServerUrl "https://git.ldlda.com/" -Token "<reg token>" -Labels "self-hosted,windows" # Usage: ./scripts/act_runner.ps1 -Config "C:/Users/Admin/Documents/gitea/runner.yaml" -RunnerPath "C:/Users/Admin/Documents/gitea/act_runner.exe" -ServerUrl "https://git.ldlda.com/" -Token "<reg token>" -Labels "self-hosted,windows:host"
param( param(
[string]$RunnerPath = "$HOME/Documents/gitea/act_runner.exe", [string]$RunnerPath = "$HOME/Documents/gitea/act_runner.exe",
# Config is a FILE path (e.g., runner.yaml). Parent folder will be created if missing. # Config is a FILE path (e.g., runner.yaml). Parent folder will be created if missing.
[string]$Config = (Join-Path (Split-Path -Parent $PSScriptRoot) 'ar_data/config.yaml'), [string]$Config = (Join-Path (Split-Path -Parent $PSScriptRoot) 'ar_data/config.yaml'),
[string]$ServerUrl, [string]$ServerUrl,
[string]$Token, [string]$Token,
[string]$Labels = "windows:host,self-hosted", [string]$Labels = "self-hosted,windows:host",
# Opt-in to non-interactive configure; by default we print the command for you to run manually. # Opt-in to non-interactive configure; by default we print the command for you to run manually.
[switch]$ForceConfigure, [switch]$ForceConfigure,
# When -Attach, run in the foreground to see logs (good for first-time troubleshooting) # When -Attach, run in the foreground to see logs (good for first-time troubleshooting)
@@ -46,7 +46,7 @@ if (-not (Test-Path $configFile)) {
Pop-Location Pop-Location
} }
else { else {
Write-Host "Config not found. Run this manually once (note labels embed host executor on Windows):" Write-Host "Config not found. Run this manually once (Windows-host runner labels):"
Write-Host "`t$RunnerPath register --no-interactive --config `"$configFile`" --instance `"$ServerUrl`" --token `"$Token`" --labels `"$Labels`"" Write-Host "`t$RunnerPath register --no-interactive --config `"$configFile`" --instance `"$ServerUrl`" --token `"$Token`" --labels `"$Labels`""
exit 2 exit 2
} }
+190
View File
@@ -0,0 +1,190 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
REPO_ROOT="$(cd "$SCRIPT_DIR/.." && pwd)"
RUNNER_BIN="${RUNNER_BIN:-/home/lda/gitea-runner/act_runner}"
CONFIG_PATH="${CONFIG_PATH:-$REPO_ROOT/ar_data/wsl_runner/config.yaml}"
RUNNER_HOME="${RUNNER_HOME:-$(dirname "$CONFIG_PATH")}"
SERVER_URL="${SERVER_URL:-}"
TOKEN="${TOKEN:-}"
LABELS="${LABELS:-self-hosted,linux,fedora,wsl,release:host}"
usage() {
cat <<'EOF'
Usage:
act_runner_fedora.sh update
act_runner_fedora.sh register --server-url URL --token TOKEN [--labels LABELS]
act_runner_fedora.sh start [--attach] [--once]
act_runner_fedora.sh stop
act_runner_fedora.sh status
Environment overrides:
RUNNER_HOME, RUNNER_BIN, CONFIG_PATH, SERVER_URL, TOKEN, LABELS
Path overrides:
--runner-home DIR
--runner-bin PATH
--config-path PATH
EOF
}
ensure_dirs() {
mkdir -p "$RUNNER_HOME" "$(dirname "$RUNNER_BIN")"
}
update_runner() {
ensure_dirs
local api="https://gitea.com/api/v1/repos/gitea/act_runner/releases/latest"
local release
release="$(curl -fsSL "$api")"
local asset_url
asset_url="$(
python3 - "$(printf '%s' "$release")" <<'PY'
import json, sys
data = json.loads(sys.argv[1])
for asset in data["assets"]:
if asset["name"].endswith("linux-amd64"):
print(asset["browser_download_url"])
break
else:
raise SystemExit("no linux-amd64 act_runner asset found")
PY
)"
local tmp="$RUNNER_HOME/act_runner.tmp"
curl -fsSL "$asset_url" -o "$tmp"
chmod +x "$tmp"
mv -f "$tmp" "$RUNNER_BIN"
echo "Updated act_runner -> $RUNNER_BIN"
}
register_runner() {
ensure_dirs
if [[ -z "$SERVER_URL" || -z "$TOKEN" ]]; then
echo "register requires SERVER_URL and TOKEN (or --server-url/--token)" >&2
exit 2
fi
"$RUNNER_BIN" register \
--no-interactive \
--config "$CONFIG_PATH" \
--instance "$SERVER_URL" \
--token "$TOKEN" \
--labels "$LABELS"
}
start_runner() {
local mode="${1:-daemon}"
ensure_dirs
if [[ ! -f "$CONFIG_PATH" ]]; then
echo "Runner config not found at $CONFIG_PATH" >&2
exit 2
fi
case "$mode" in
attach)
exec "$RUNNER_BIN" daemon --config "$CONFIG_PATH"
;;
once)
exec "$RUNNER_BIN" daemon --config "$CONFIG_PATH" --once
;;
daemon)
nohup "$RUNNER_BIN" daemon --config "$CONFIG_PATH" >/tmp/act_runner.log 2>&1 &
echo "Started act_runner in background"
;;
*)
echo "Unknown start mode: $mode" >&2
exit 2
;;
esac
}
stop_runner() {
pkill -f "$RUNNER_BIN" || true
}
status_runner() {
if pgrep -f "$RUNNER_BIN" >/dev/null; then
echo "act_runner is running"
else
echo "act_runner is not running"
fi
}
ACTION="${1:-}"
shift || true
while [[ $# -gt 0 ]]; do
case "$1" in
--server-url)
SERVER_URL="$2"
shift 2
;;
--token)
TOKEN="$2"
shift 2
;;
--labels)
LABELS="$2"
shift 2
;;
--runner-home)
RUNNER_HOME="$2"
if [[ "${RUNNER_BIN:-}" == "/home/lda/gitea-runner/act_runner" ]]; then
RUNNER_BIN="$RUNNER_HOME/act_runner"
fi
if [[ "${CONFIG_PATH:-}" == "$REPO_ROOT/ar_data/wsl_runner/config.yaml" ]]; then
CONFIG_PATH="$RUNNER_HOME/config.yaml"
fi
shift 2
;;
--runner-bin)
RUNNER_BIN="$2"
shift 2
;;
--config-path)
CONFIG_PATH="$2"
RUNNER_HOME="$(dirname "$CONFIG_PATH")"
shift 2
;;
--attach)
START_MODE="attach"
shift
;;
--once)
START_MODE="once"
shift
;;
*)
echo "Unknown argument: $1" >&2
usage
exit 2
;;
esac
done
case "$ACTION" in
update)
update_runner
;;
register)
register_runner
;;
start)
start_runner "${START_MODE:-daemon}"
;;
stop)
stop_runner
;;
status)
status_runner
;;
*)
usage
exit 2
;;
esac
+60
View File
@@ -0,0 +1,60 @@
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]$RunnerHome,
[string]$RunnerBin = "/home/lda/gitea-runner/act_runner",
[string]$ConfigPath,
[string]$ServerUrl,
[string]$Token,
[string]$Labels = "self-hosted,linux,fedora,wsl,release:host",
[switch]$Attach,
[switch]$Once
)
$ErrorActionPreference = "Stop"
. "$PSScriptRoot/lib.ps1"
$scriptWin = Join-Path $PSScriptRoot "act_runner_fedora.sh"
if (-not (Test-Path $scriptWin)) {
throw "Missing Fedora runner helper: $scriptWin"
}
$scriptLinux = (& wsl.exe -d $Distro -e wslpath -a "$scriptWin").Trim()
if (-not $scriptLinux) {
throw "Failed to resolve WSL path for $scriptWin"
}
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"
}
}
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"
}
}
$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)) }
if ($Token) { $parts += @("--token", (Quote-ShArg $Token)) }
if ($Labels) { $parts += @("--labels", (Quote-ShArg $Labels)) }
if ($Attach) { $parts += "--attach" }
if ($Once) { $parts += "--once" }
$cmd = $parts -join " "
Write-Host $cmd
& wsl.exe -d $Distro -e bash -lc $cmd
if ($LASTEXITCODE -ne 0) {
throw "WSL runner action failed ($LASTEXITCODE)"
}
+17 -5
View File
@@ -1,12 +1,16 @@
[System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', "", [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidDefaultValueSwitchParameter', "",
Justification = 'Default true is intentional for fast dev loop')] Justification = 'Default true is intentional for fast dev loop')]
param( param(
[ValidateSet("cargo", "cross")]
[string]$Cargo = "cargo",
[string]$Pass, [string]$Pass,
[string]$HostName = "192.168.100.1", [string]$HostName = "192.168.100.1",
[string]$User = "root", [string]$User = "root",
[string]$RemotePath = "/root/.bin/wakey", [string]$RemotePath = "/root/.bin/wakey",
[string]$AgentRemotePath = "/root/.bin/wakey-agent",
[string]$Target = "armv7-unknown-linux-musleabihf", [string]$Target = "armv7-unknown-linux-musleabihf",
[string]$BinName = "wakey", [string]$BinName = "wakey",
[string]$AgentBinName = "wakey-agent",
[string]$HostKey, [string]$HostKey,
[switch]$Restart = $true, [switch]$Restart = $true,
[switch]$Quiet = $true [switch]$Quiet = $true
@@ -37,21 +41,26 @@ $repoRoot = Split-Path -Parent $PSScriptRoot
Push-Location $repoRoot Push-Location $repoRoot
try { try {
Write-Host "[build] cargo build --release --target $Target" -ForegroundColor Cyan Write-Host "[build] $Cargo build --release --target $Target -p wakey -p wakey-agent" -ForegroundColor Cyan
cargo build --release --target $Target . "$Cargo" build --release --target $Target -p wakey -p wakey-agent
if ($LASTEXITCODE -ne 0) { throw "cargo build failed ($LASTEXITCODE)" } if ($LASTEXITCODE -ne 0) { throw "$Cargo build failed ($LASTEXITCODE)" }
$localBin = Join-Path $repoRoot "target/$Target/release/$BinName" $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 $localBin)) { throw "binary not found: $localBin" }
if (-not (Test-Path $localAgentBin)) { throw "binary not found: $localAgentBin" }
$remoteTmp = "$RemotePath.tmp" $remoteTmp = "$RemotePath.tmp"
$agentRemoteTmp = "$AgentRemotePath.tmp"
$destTmp = "$User@${HostName}:$remoteTmp" $destTmp = "$User@${HostName}:$remoteTmp"
$agentDestTmp = "$User@${HostName}:$agentRemoteTmp"
$localDeploy = Join-Path $repoRoot 'scripts/remote_deploy_wakey.sh' $localDeploy = Join-Path $repoRoot 'scripts/remote_deploy_wakey.sh'
$deployTmp = '/var/tmp/remote_deploy_wakey.sh' $deployTmp = '/var/tmp/remote_deploy_wakey.sh'
$deployPreferred = '/root/.bin/remote_deploy_wakey.sh' $deployPreferred = '/root/.bin/remote_deploy_wakey.sh'
# Push main binary # Push binaries
Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Quiet:$Quiet Invoke-Scp -Local $localBin -Dest $destTmp -Pass $Pass -HostKey $HostKey -Quiet:$Quiet
Invoke-Scp -Local $localAgentBin -Dest $agentDestTmp -Pass $Pass -HostKey $HostKey -Quiet:$Quiet
# Push static assets # Push static assets
$localStatic = Join-Path $repoRoot "static" $localStatic = Join-Path $repoRoot "static"
@@ -74,7 +83,10 @@ try {
# Build and run remote deploy command # Build and run remote deploy command
$restartFlag = $(if ($Restart) { '1' } else { '0' }) $restartFlag = $(if ($Restart) { '1' } else { '0' })
$script = Get-DeployScript $deployPreferred $deployTmp $remoteTmp $RemotePath $restartFlag $script = @"
$(Get-DeployScript $deployPreferred $deployTmp $remoteTmp $RemotePath 0)
$(Get-DeployScript $deployPreferred $deployTmp $agentRemoteTmp $AgentRemotePath $restartFlag)
"@
$remoteCmd = "sh -lc '$($script -replace "`r",'')'" $remoteCmd = "sh -lc '$($script -replace "`r",'')'"
if ($Quiet) { $remoteCmd += " >/dev/null 2>&1" } if ($Quiet) { $remoteCmd += " >/dev/null 2>&1" }
+2
View File
@@ -5,10 +5,12 @@ START=99
USE_PROCD=1 USE_PROCD=1
NAME=wakey NAME=wakey
# BIN=/root/.bin/wakey-agent
BIN=/root/.bin/wakey BIN=/root/.bin/wakey
start_service() { start_service() {
procd_open_instance procd_open_instance
# procd_set_param command "$BIN" serve
procd_set_param command "$BIN" http --host :: --port 12012 procd_set_param command "$BIN" http --host :: --port 12012
procd_set_param respawn 5 1 0 procd_set_param respawn 5 1 0
procd_set_param stdout 1 procd_set_param stdout 1
+9
View File
@@ -1,6 +1,7 @@
# Build a rootfs tarball suitable for `wget -O- ... | tar -xz -C /` on OpenWrt. # Build a rootfs tarball suitable for `wget -O- ... | tar -xz -C /` on OpenWrt.
# It lays out files as absolute-root paths inside the archive: # It lays out files as absolute-root paths inside the archive:
# root/.bin/wakey # root/.bin/wakey
# root/.bin/wakey-agent
# etc/init.d/wakey # etc/init.d/wakey
# Usage: ./scripts/package_rootfs.ps1 -Version 0.1.0 -Target armv7-unknown-linux-musleabihf -OutDir dist # Usage: ./scripts/package_rootfs.ps1 -Version 0.1.0 -Target armv7-unknown-linux-musleabihf -OutDir dist
param( param(
@@ -17,7 +18,9 @@ $dist = Join-Path $root $OutDir
New-Item -ItemType Directory -Force -Path $dist | Out-Null New-Item -ItemType Directory -Force -Path $dist | Out-Null
$binName = if ($Target -like "*-windows-*") { "wakey.exe" } else { "wakey" } $binName = if ($Target -like "*-windows-*") { "wakey.exe" } else { "wakey" }
$agentBinName = if ($Target -like "*-windows-*") { "wakey-agent.exe" } else { "wakey-agent" }
$binSrc = Join-Path $root "target/$Target/release/$binName" $binSrc = Join-Path $root "target/$Target/release/$binName"
$agentBinSrc = Join-Path $root "target/$Target/release/$agentBinName"
$staging = Join-Path $dist ("rootfs-" + [System.Guid]::NewGuid().ToString("N")) $staging = Join-Path $dist ("rootfs-" + [System.Guid]::NewGuid().ToString("N"))
New-Item -ItemType Directory -Force -Path $staging | Out-Null New-Item -ItemType Directory -Force -Path $staging | Out-Null
@@ -35,6 +38,12 @@ if (-not $NoBin) {
else { else {
throw "Missing binary: $binSrc (build it first or pass -NoBin)" throw "Missing binary: $binSrc (build it first or pass -NoBin)"
} }
if (Test-Path $agentBinSrc) {
Copy-Item $agentBinSrc (Join-Path $rootDir "wakey-agent") -Force
}
else {
throw "Missing binary: $agentBinSrc (build it first or pass -NoBin)"
}
} }
# Normalize kill script line endings and copy # Normalize kill script line endings and copy
$killSrc = Join-Path $root 'scripts/kill_wakey.sh' $killSrc = Join-Path $root 'scripts/kill_wakey.sh'
+28 -9
View File
@@ -10,7 +10,10 @@ param(
[string]$Version, [string]$Version,
[switch]$Tag, [switch]$Tag,
[switch]$Publish, [switch]$Publish,
[string]$Registry [string]$Registry,
[ValidateSet('wsl', 'windows', 'none')]
[string]$RunnerMode = 'wsl',
[string]$WslDistro = $(if ($env:WAKEY_WSL_DISTRO) { $env:WAKEY_WSL_DISTRO } else { 'FedoraLinux' })
) )
$ErrorActionPreference = 'Stop' $ErrorActionPreference = 'Stop'
@@ -60,13 +63,29 @@ if ($Tag -and $Version) {
git push -f origin "v$Version" git push -f origin "v$Version"
Write-Host "Pushed tag: v$Version" Write-Host "Pushed tag: v$Version"
# Run act_runner in --once mode to process the release job switch ($RunnerMode) {
$actRunnerScript = Join-Path $PSScriptRoot 'act_runner.ps1' 'wsl' {
if (Test-Path $actRunnerScript) { $runnerScript = Join-Path $PSScriptRoot 'act_runner_wsl.ps1'
Write-Host "Starting act_runner (once mode) to process release job..." if (Test-Path $runnerScript) {
& $actRunnerScript -Once Write-Host "Starting Fedora/WSL act_runner (once mode) to process release job..."
} & $runnerScript -Distro $WslDistro -Action start -Once
else { }
Write-Warning "act_runner.ps1 not found at $actRunnerScript. Skipping runner." else {
Write-Warning "act_runner_wsl.ps1 not found at $runnerScript. Skipping runner."
}
}
'windows' {
$runnerScript = Join-Path $PSScriptRoot 'act_runner.ps1'
if (Test-Path $runnerScript) {
Write-Host "Starting Windows act_runner (once mode) to process release job..."
& $runnerScript -Once
}
else {
Write-Warning "act_runner.ps1 not found at $runnerScript. Skipping runner."
}
}
'none' {
Write-Host "RunnerMode=none; not starting a local runner helper."
}
} }
} }
+22
View File
@@ -0,0 +1,22 @@
[package]
name = "wakey-agent"
version = "0.1.0"
edition = "2024"
[dependencies]
anyhow = "1"
clap = { version = "4", features = ["derive"] }
futures-util = "0.3"
http = "1"
macaddr = { version = "1", features = ["serde", "serde_std"] }
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
tokio = { version = "1", features = ["fs", "macros", "rt-multi-thread", "time", "signal", "sync"] }
tokio-tungstenite = { version = "0.24", features = ["rustls-tls-webpki-roots"] }
toml = "0.8"
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
url = "2"
wakey = { path = ".." }
wakey-core = { path = "../wakey-core" }
+75
View File
@@ -0,0 +1,75 @@
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
use std::path::Path;
pub const DEFAULT_CONFIG_PATH: &str = "/etc/wakey-agent/config.toml";
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct AgentConfig {
pub server_url: String,
pub agent_id: String,
pub agent_token: String,
#[serde(default = "default_reconnect_base_ms")]
pub reconnect_base_ms: u64,
#[serde(default = "default_reconnect_max_ms")]
pub reconnect_max_ms: u64,
}
const fn default_reconnect_base_ms() -> u64 {
1_000
}
const fn default_reconnect_max_ms() -> u64 {
30_000
}
pub fn load_config(path: &Path) -> Result<AgentConfig> {
let content = std::fs::read_to_string(path)
.with_context(|| format!("failed to read agent config {}", path.display()))?;
toml::from_str(&content)
.with_context(|| format!("failed to parse agent config {}", path.display()))
}
pub fn save_config(path: &Path, config: &AgentConfig) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("failed to create config dir {}", parent.display()))?;
}
let content = toml::to_string_pretty(config).context("failed to serialize agent config")?;
let tmp = path.with_extension("toml.tmp");
std::fs::write(&tmp, content)
.with_context(|| format!("failed to write temp config {}", tmp.display()))?;
std::fs::rename(&tmp, path).with_context(|| {
format!(
"failed to move temp config {} into {}",
tmp.display(),
path.display()
)
})?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn config_roundtrip() {
let dir = std::env::temp_dir().join(format!("wakey-agent-config-{}", std::process::id()));
let path = dir.join("config.toml");
let config = AgentConfig {
server_url: "https://example.com".into(),
agent_id: "agent-1".into(),
agent_token: "secret".into(),
reconnect_base_ms: 123,
reconnect_max_ms: 456,
};
save_config(&path, &config).expect("save");
let loaded = load_config(&path).expect("load");
assert_eq!(loaded, config);
let _ = std::fs::remove_file(&path);
let _ = std::fs::remove_dir_all(&dir);
}
}
+114
View File
@@ -0,0 +1,114 @@
use anyhow::Result;
use tracing::{debug, instrument};
use crate::protocol::{
AgentCommand, CommandResult, DevsRequest, InventoryRequest, LeasesRequest, StatusRequest,
WakeRequest,
};
#[instrument(skip_all)]
pub async fn dispatch_command(command: AgentCommand) -> Result<CommandResult> {
match command {
AgentCommand::Status(req) => dispatch_status(req).await,
AgentCommand::Leases(req) => dispatch_leases(req).await,
AgentCommand::Devs(req) => dispatch_devs(req).await,
AgentCommand::Inventory(req) => dispatch_inventory(req).await,
AgentCommand::Wake(req) => dispatch_wake(req).await,
}
}
async fn dispatch_status(req: StatusRequest) -> Result<CommandResult> {
let query = req.into_device_query();
let status = if query.name.is_some()
&& query.filter.ips.is_empty()
&& query.filter.devs.is_empty()
&& query.filter.nuds.is_empty()
&& query.filter.macs.is_empty()
{
wakey::get_status_for_input(query.name.clone().unwrap_or_default()).await?
} else {
wakey::get_status(query).await?
};
debug!(rows = status.table.len(), "dispatched status command");
Ok(CommandResult::Status(status))
}
async fn dispatch_leases(req: LeasesRequest) -> Result<CommandResult> {
let leases = wakey::get_leases(wakey_core::LeaseQuery {
include_state: req.include_state,
})
.await?;
Ok(CommandResult::Leases(leases))
}
async fn dispatch_devs(req: DevsRequest) -> Result<CommandResult> {
let mut devs = if let Some(name) = &req.dev {
wakey::get_interface_summary(name).await?.into_iter().collect()
} else {
wakey::get_interface_summaries().await?
};
if req.up_only {
devs.retain(|dev| dev.operstate == "up");
}
Ok(CommandResult::Devs(devs))
}
async fn dispatch_inventory(req: InventoryRequest) -> Result<CommandResult> {
let inventory = wakey::inventory(req.into_device_query()).await?;
Ok(CommandResult::Inventory(inventory))
}
async fn dispatch_wake(req: WakeRequest) -> Result<CommandResult> {
validate_wake_request(&req)?;
let result = match (req.query, req.mac, req.ip) {
(Some(query), None, None) => wakey::wake_from_query(query).await?,
(None, Some(mac), ip) => wakey::wake_explicit(mac, ip).await?,
_ => unreachable!("wake request validated before dispatch"),
};
Ok(CommandResult::Wake(result))
}
pub fn validate_wake_request(req: &WakeRequest) -> Result<()> {
let has_query = req.query.is_some();
let has_mac = req.mac.is_some();
let has_ip = req.ip.is_some();
if has_ip && !has_mac {
anyhow::bail!("wake request `ip` requires `mac`");
}
if has_query && (has_mac || has_ip) {
anyhow::bail!("wake query mode and explicit mode are mutually exclusive");
}
if !has_query && !has_mac {
anyhow::bail!("wake request requires either `query` or `mac`");
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::protocol::WakeRequest;
#[test]
fn wake_rejects_ip_without_mac() {
let err = validate_wake_request(&WakeRequest {
query: None,
mac: None,
ip: Some("192.168.1.1".parse().expect("ip")),
})
.expect_err("should reject");
assert!(err.to_string().contains("requires `mac`"));
}
#[test]
fn wake_rejects_mixed_mode() {
let err = validate_wake_request(&WakeRequest {
query: Some("pc".into()),
mac: Some("aa:bb:cc:dd:ee:ff".parse().expect("mac")),
ip: None,
})
.expect_err("should reject");
assert!(err.to_string().contains("mutually exclusive"));
}
}
+70
View File
@@ -0,0 +1,70 @@
use anyhow::{Context, Result};
use reqwest::StatusCode;
use serde::{Deserialize, Serialize};
use std::path::Path;
use tracing::info;
use crate::config::{AgentConfig, save_config};
#[derive(Debug, Serialize)]
struct EnrollRequest<'a> {
enroll_token: &'a str,
}
#[derive(Debug, Deserialize)]
struct EnrollResponse {
agent_id: String,
agent_token: String,
server_url: Option<String>,
}
pub async fn enroll(server_url: &str, enroll_token: &str, config_path: &Path) -> Result<AgentConfig> {
let server_url = normalize_server_url(server_url);
let endpoint = format!("{server_url}/api/v1/agents/enroll");
let client = reqwest::Client::new();
let response = client
.post(endpoint)
.json(&EnrollRequest { enroll_token })
.send()
.await
.context("failed to call enrollment endpoint")?;
if response.status() != StatusCode::OK {
let status = response.status();
let body = response
.text()
.await
.unwrap_or_else(|_| "<unable to read enrollment error body>".into());
anyhow::bail!("enrollment failed with {status}: {body}");
}
let payload: EnrollResponse = response
.json()
.await
.context("failed to decode enrollment response")?;
let config = AgentConfig {
server_url: payload.server_url.unwrap_or(server_url),
agent_id: payload.agent_id,
agent_token: payload.agent_token,
reconnect_base_ms: 1_000,
reconnect_max_ms: 30_000,
};
save_config(config_path, &config)?;
info!(config_path = %config_path.display(), "wrote agent config");
Ok(config)
}
pub fn normalize_server_url(server_url: &str) -> String {
server_url.trim_end_matches('/').to_string()
}
#[cfg(test)]
mod tests {
use super::normalize_server_url;
#[test]
fn normalize_server_url_trims_slash() {
assert_eq!(normalize_server_url("https://example.com/"), "https://example.com");
}
}
+72
View File
@@ -0,0 +1,72 @@
mod config;
mod dispatch;
mod enroll;
mod protocol;
mod session;
mod tracing;
use std::path::PathBuf;
use clap::{ArgAction, Args, Parser, Subcommand};
use ::tracing::info;
#[derive(Parser)]
#[command(name = "wakey-agent")]
#[command(version, about = "Outbound control-plane agent for Wakey")]
struct Cli {
/// Increase log verbosity. Use `-v` for debug and `-vv` for trace.
#[arg(short = 'v', long = "verbose", action = ArgAction::Count, global = true)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Start the agent daemon and maintain the outbound control-plane session.
Serve(ServeArgs),
/// Enroll this router with a control plane and write agent config.
Enroll(EnrollArgs),
}
#[derive(Args)]
struct ServeArgs {
/// Path to the agent config file.
#[arg(long, default_value = config::DEFAULT_CONFIG_PATH)]
config: PathBuf,
}
#[derive(Args)]
struct EnrollArgs {
/// Base HTTPS URL of the control plane.
#[arg(long)]
server_url: String,
/// One-time or short-lived enroll token provided by the control plane.
#[arg(long)]
enroll_token: String,
/// Path to the agent config file to write.
#[arg(long, default_value = config::DEFAULT_CONFIG_PATH)]
config: PathBuf,
}
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
tracing::init(cli.verbose);
match cli.command {
Command::Serve(args) => {
let config = config::load_config(&args.config)?;
info!(config_path = %args.config.display(), agent_id = %config.agent_id, "starting wakey-agent");
session::run(config).await?;
}
Command::Enroll(args) => {
let config = enroll::enroll(&args.server_url, &args.enroll_token, &args.config).await?;
println!("agent_id={}", config.agent_id);
println!("config={}", args.config.display());
}
}
Ok(())
}
+173
View File
@@ -0,0 +1,173 @@
use macaddr::MacAddr;
use serde::{Deserialize, Serialize};
use std::net::IpAddr;
use wakey_core::{
DeviceFilters, DeviceInventory, DeviceQuery, DhcpLeaseWithState, InterfaceSummary, NeighborEntry,
Status, WakeResult,
};
use wakey_core::parse::mac;
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct StatusRequest {
pub query: Option<String>,
pub name: Option<String>,
#[serde(default)]
pub ips: Vec<IpAddr>,
#[serde(default)]
pub devs: Vec<String>,
#[serde(default)]
pub nuds: Vec<wakey_core::NeighborState>,
#[serde(default)]
#[serde(with = "mac::vec_mac")]
pub macs: Vec<MacAddr>,
}
impl StatusRequest {
pub fn into_device_query(self) -> DeviceQuery {
if let Some(query) = self.query.as_ref()
&& self.name.is_none()
&& self.ips.is_empty()
&& self.devs.is_empty()
&& self.nuds.is_empty()
&& self.macs.is_empty()
{
return DeviceQuery {
name: Some(query.clone()),
..Default::default()
};
}
DeviceQuery {
name: self.name.or(self.query),
filter: DeviceFilters {
ips: self.ips,
devs: self.devs,
nuds: self.nuds,
macs: self.macs,
},
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct LeasesRequest {
#[serde(default)]
pub include_state: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct DevsRequest {
pub dev: Option<String>,
#[serde(default)]
pub up_only: bool,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InventoryRequest {
pub query: Option<String>,
pub name: Option<String>,
#[serde(default)]
pub ips: Vec<IpAddr>,
#[serde(default)]
pub devs: Vec<String>,
#[serde(default)]
pub nuds: Vec<wakey_core::NeighborState>,
#[serde(default)]
#[serde(with = "mac::vec_mac")]
pub macs: Vec<MacAddr>,
}
impl InventoryRequest {
pub fn into_device_query(self) -> DeviceQuery {
StatusRequest {
query: self.query,
name: self.name,
ips: self.ips,
devs: self.devs,
nuds: self.nuds,
macs: self.macs,
}
.into_device_query()
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WakeRequest {
pub query: Option<String>,
#[serde(default)]
#[serde(with = "mac::option_mac")]
pub mac: Option<MacAddr>,
#[serde(default)]
pub ip: Option<IpAddr>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum AgentCommand {
Status(StatusRequest),
Leases(LeasesRequest),
Devs(DevsRequest),
Inventory(InventoryRequest),
Wake(WakeRequest),
}
#[derive(Debug, Serialize)]
#[serde(tag = "kind", rename_all = "snake_case")]
pub enum CommandResult {
Status(Status<NeighborEntry>),
Leases(Vec<DhcpLeaseWithState>),
Devs(Vec<InterfaceSummary>),
Inventory(DeviceInventory),
Wake(WakeResult),
}
#[derive(Debug, Serialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ClientMessage {
Hello {
agent_id: String,
},
Auth {
agent_id: String,
agent_token: String,
},
Heartbeat {
agent_id: String,
},
Result {
request_id: String,
result: CommandResult,
},
Error {
request_id: String,
error: String,
},
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(tag = "type", rename_all = "snake_case")]
pub enum ServerMessage {
Command {
request_id: String,
command: AgentCommand,
},
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn command_serialization_is_stable() {
let msg = ServerMessage::Command {
request_id: "req-1".into(),
command: AgentCommand::Leases(LeasesRequest {
include_state: true,
}),
};
let json = serde_json::to_string(&msg).expect("serialize");
assert!(json.contains("\"request_id\":\"req-1\""));
assert!(json.contains("\"kind\":\"leases\""));
}
}
+156
View File
@@ -0,0 +1,156 @@
use anyhow::{Context, Result};
use futures_util::{SinkExt, StreamExt};
use tokio::time::{Duration, MissedTickBehavior, interval, sleep};
use tokio_tungstenite::{connect_async, tungstenite::Message};
use tracing::{debug, error, info, warn};
use crate::config::AgentConfig;
use crate::dispatch::dispatch_command;
use crate::protocol::{ClientMessage, ServerMessage};
pub async fn run(config: AgentConfig) -> Result<()> {
let mut backoff = config.reconnect_base_ms.max(100);
loop {
match run_once(&config).await {
Ok(()) => {
backoff = config.reconnect_base_ms.max(100);
}
Err(err) => {
warn!(error = %err, backoff_ms = backoff, "agent session ended; reconnecting");
sleep(Duration::from_millis(backoff)).await;
backoff = (backoff.saturating_mul(2)).min(config.reconnect_max_ms.max(backoff));
}
}
}
}
async fn run_once(config: &AgentConfig) -> Result<()> {
let ws_url = websocket_url(&config.server_url)?;
info!(%ws_url, agent_id = %config.agent_id, "connecting agent websocket");
let (stream, _) = connect_async(ws_url.as_str())
.await
.context("failed to connect websocket")?;
let (mut sink, mut source) = stream.split();
send_json(&mut sink, &ClientMessage::Hello {
agent_id: config.agent_id.clone(),
})
.await?;
send_json(
&mut sink,
&ClientMessage::Auth {
agent_id: config.agent_id.clone(),
agent_token: config.agent_token.clone(),
},
)
.await?;
let mut heartbeat = interval(Duration::from_secs(30));
heartbeat.set_missed_tick_behavior(MissedTickBehavior::Skip);
loop {
tokio::select! {
_ = heartbeat.tick() => {
send_json(&mut sink, &ClientMessage::Heartbeat {
agent_id: config.agent_id.clone(),
}).await?;
}
maybe_msg = source.next() => {
let msg = match maybe_msg {
Some(msg) => msg.context("websocket frame failed")?,
None => anyhow::bail!("websocket closed by server"),
};
match msg {
Message::Text(text) => {
let message: ServerMessage = serde_json::from_str(&text)
.context("failed to decode server message")?;
handle_server_message(&mut sink, message).await?;
}
Message::Ping(payload) => {
sink.send(Message::Pong(payload)).await.context("failed to send pong")?;
}
Message::Pong(_) => {}
Message::Close(frame) => {
anyhow::bail!("websocket closed: {:?}", frame);
}
Message::Binary(_) => {
debug!("ignoring unexpected binary websocket frame");
}
Message::Frame(_) => {}
}
}
}
}
}
async fn handle_server_message<S>(sink: &mut S, message: ServerMessage) -> Result<()>
where
S: SinkExt<Message> + Unpin,
<S as futures_util::Sink<Message>>::Error: std::error::Error + Send + Sync + 'static,
{
match message {
ServerMessage::Command {
request_id,
command,
} => match dispatch_command(command).await {
Ok(result) => {
send_json(sink, &ClientMessage::Result { request_id, result }).await?;
}
Err(err) => {
error!(request_id, error = %err, "command dispatch failed");
send_json(
sink,
&ClientMessage::Error {
request_id,
error: err.to_string(),
},
)
.await?;
}
},
}
Ok(())
}
async fn send_json<S>(sink: &mut S, message: &ClientMessage) -> Result<()>
where
S: SinkExt<Message> + Unpin,
<S as futures_util::Sink<Message>>::Error: std::error::Error + Send + Sync + 'static,
{
let payload = serde_json::to_string(message).context("failed to serialize websocket message")?;
sink.send(Message::Text(payload))
.await
.context("failed to send websocket message")?;
Ok(())
}
pub fn websocket_url(server_url: &str) -> Result<url::Url> {
let base = url::Url::parse(server_url).context("invalid server_url")?;
let scheme = match base.scheme() {
"http" => "ws",
"https" => "wss",
"ws" => "ws",
"wss" => "wss",
other => anyhow::bail!("unsupported server_url scheme `{other}`"),
};
let mut url = base;
url.set_scheme(scheme)
.map_err(|_| anyhow::anyhow!("failed to convert server_url scheme"))?;
url.set_path("/api/v1/agent/ws");
url.set_query(None);
url.set_fragment(None);
Ok(url)
}
#[cfg(test)]
mod tests {
use super::websocket_url;
#[test]
fn websocket_url_uses_expected_path() {
let url = websocket_url("https://example.com/control").expect("url");
assert_eq!(url.as_str(), "wss://example.com/api/v1/agent/ws");
}
}
+20
View File
@@ -0,0 +1,20 @@
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
pub fn init(verbose: u8) {
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(default_filter(verbose)))
.expect("static tracing filter should parse");
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer())
.init();
}
fn default_filter(verbose: u8) -> &'static str {
match verbose {
0 => "wakey_agent=info,wakey=info",
1 => "wakey_agent=debug,wakey=debug",
_ => "wakey_agent=trace,wakey=debug",
}
}