sub-logic is own function

- lda
This commit is contained in:
lda
2026-04-06 16:55:19 +07:00 Unverified
parent 6f59c0c011
commit 9305702b36
3 changed files with 334 additions and 23 deletions
+177 -8
View File
@@ -1,17 +1,186 @@
# wakey
router shit
`wakey` is a Wake-on-LAN and LAN-observability tool for a Linux router.
## whatever i did must be BEAUTIFUL
It started as a small web UI running on-device. It is now being reshaped into a
service-first project with:
read [in vietnamese](https://ldlda.com/blogs/else/ssh-ax3000cv2-update-firmware-0-10-2)
- a reusable core model
- a Linux/OpenWrt adapter layer
- a CLI for operators
- a temporary legacy HTTP/static adapter during migration
## now i NEED the split arch to be on
## What it does
like RN rn rn so i can have web outside and the lil agent can be sitting here configuring itself (Parse/append to rc.local files? add to init.d? fuhh)
`wakey` currently focuses on a small set of router-side jobs:
I NEED IT
- inspect LAN neighbor state
- read DHCP lease data
- merge those facts into a higher-level device inventory
- list useful interface/broadcast information
- send Wake-on-LAN packets
I NEEED IT
In practice that means commands like:
I NEEEEEED it.
```sh
wakey status bedroom-pc
wakey leases --include-state
wakey devs
wakey wake bedroom-pc
wakey wake --mac aa:bb:cc:dd:ee:ff
wakey http --host :: --port 12012
```
## Workspace layout
- `wakey-core`
- shared types and parsing
- device, neighbor, DHCP, interface, and wake models
- `wakey-linux`
- Linux/OpenWrt adapter
- DHCP lease loading, interface summaries, neighbor lookup, WoL sending
- `wakey`
- service layer, CLI, and temporary HTTP/static adapter
- `ipjs`
- typed wrappers around Linux `ip -j ...` data
- JSON-first, with optional experimental netlink backends
## Current architecture
Inside the `wakey` crate:
- `src/service`
- the real use-case layer
- status, leases, inventory, interfaces, wake, and query resolution
- `src/http`
- temporary legacy HTTP/static adapter
- compatibility mapping for the current `/static` client
- `src/legacy`
- transitional compatibility wrappers kept during the migration
The long-term direction is:
- keep the service layer stable
- keep HTTP as an adapter, not the architecture
- eventually move toward an agent + control-plane model
## CLI
`wakey` is usable as a local/operator CLI.
### Status
Show device status rows using a free-form selector:
```sh
wakey status bedroom-pc
```
Or use explicit filters:
```sh
wakey status --dev br-lan --nud reachable
wakey status --mac aa:bb:cc:dd:ee:ff
wakey status --json
```
### Leases
Show DHCP leases:
```sh
wakey leases
wakey leases --include-state
wakey leases --include-state --json
```
### Wake
Query mode:
```sh
wakey wake bedroom-pc
```
Explicit/manual mode:
```sh
wakey wake --mac aa:bb:cc:dd:ee:ff
wakey wake --mac aa:bb:cc:dd:ee:ff --ip 192.168.1.255
wakey wake --mac aa:bb:cc:dd:ee:ff --json
```
Rules:
- query mode and explicit `--mac/--ip` mode are mutually exclusive
- `--ip` requires `--mac`
- `--mac` without `--ip` fans out to interface broadcast targets
### Interfaces
Show condensed interface summaries:
```sh
wakey devs
wakey devs br-lan
wakey devs --up
wakey devs --json
```
### Temporary HTTP adapter
The old web/static app can still be served during migration:
```sh
wakey http --host :: --port 12012
```
This should be treated as a compatibility surface, not the long-term product
shape.
## Tests
This repo has two useful testing modes:
- local compile checks
- live on-device tests against the real router/runtime environment
### Local
```sh
cargo check
cargo test --no-run
cargo clippy --all-targets --all-features -- -D warnings
```
### On-device
Some integration tests are intentionally `#[ignore]` because they use real
router state. Use the PowerShell helper:
```powershell
./scripts/test_remote.ps1 -Package wakey -BinaryFilter integration_live_services -Ignored -NoCapture
./scripts/test_remote.ps1 -Package wakey -BinaryFilter integration_inventory -Ignored -NoCapture
```
You can further narrow execution with `-Filter` to select individual Rust test
functions inside a test binary.
## Build target
This project is primarily aimed at an OpenWrt/Linux ARM router target. The
workspace is commonly built for:
```text
armv7-unknown-linux-musleabihf
```
Some crates and tests are Linux-specific by design.
## Notes
- `ipjs` is JSON-first by default; experimental netlink paths exist where they
are worth keeping.
- the current web client is temporary and kept alive through explicit
compatibility mapping
- the service layer is the part intended to survive the migration
+45
View File
@@ -43,6 +43,51 @@ This folder has small helpers for build, CI, and router install. Keep it simple;
- Uploads to `<RemotePath>.tmp` then atomically moves into place; `-Restart` restarts the service; `-Quiet` silences MOTD.
- `package_rootfs.ps1` — Produces `dist/wakey-rootfs-<version>-<target>.tgz` with `/root/.bin/wakey` and `/etc/init.d/*`.
- `publish.ps1` — Optional version bump + build + tag (and `cargo publish` only if you pass `-Publish`).
- `test_remote.ps1` — Build ARM Linux test binaries locally, upload them to the router, and run them there.
## Remote test runner
`test_remote.ps1` is the main way to run Linux/router-specific tests that do not
make sense on Windows.
Basic examples:
```powershell
./scripts/test_remote.ps1 -Package wakey
./scripts/test_remote.ps1 -Package wakey -BinaryFilter integration_live_services -Ignored -NoCapture
./scripts/test_remote.ps1 -Package wakey -BinaryFilter integration_live_services -Filter status_real_router_default_query_succeeds -Ignored -NoCapture
./scripts/test_remote.ps1 -AllPackages
```
Important semantics:
- `-BinaryFilter`
- selects which compiled Rust test executable(s) to run
- `-Filter`
- filters test functions inside a selected Rust test binary
- `-Ignored`
- runs `#[ignore]` tests
- `-IncludeIgnored`
- includes ignored tests in addition to normal ones
- `-List`
- lists tests inside the remote binary instead of executing them
Useful flags:
- `-BuildProfile debug|release`
- `-Exact`
- `-NoCapture`
- `-ShowOutput`
- `-Threads <n>`
- `-RemoteHost root@<router-ip>`
- `-RemoteTestPath /tmp/tmp/wakey-test`
Implementation notes:
- test executables are discovered via Cargo JSON messages, not regexing human output
- binaries are batch-uploaded per package run
- each package run gets a unique remote temp directory for safer concurrent use
- packages with no test executables are skipped instead of treated as failures
## CI (Gitea)
+112 -15
View File
@@ -1,7 +1,7 @@
use anyhow::{Context, Result};
use macaddr::MacAddr;
use std::net::IpAddr;
use wakey_core::{WakeResult, WakeTarget};
use wakey_core::{InterfaceSummary, WakeResult, WakeTarget};
use crate::service::interfaces::get_interface_summaries;
use crate::service::inventory::resolve_devices;
@@ -24,25 +24,14 @@ pub async fn wake_from_query(input: impl Into<String>) -> Result<WakeResult> {
///
/// This is used by explicit manual wake mode when only a MAC address is supplied.
pub async fn broadcast_wake_targets(mac: MacAddr) -> Result<Vec<WakeTarget>> {
Ok(get_interface_summaries()
.await?
.into_iter()
.flat_map(|iface| iface.addrs.into_iter())
.filter_map(|addr| addr.broadcast)
.map(|ip| WakeTarget {
ip: Some(IpAddr::V4(ip)),
mac: Some(mac),
})
.collect())
let interfaces = get_interface_summaries().await?;
broadcast_wake_targets_from_interfaces(&interfaces, mac)
}
/// Wake a device explicitly by MAC, optionally targeting a specific IP/broadcast.
pub async fn wake_explicit(mac: MacAddr, ip: Option<IpAddr>) -> Result<WakeResult> {
let targets = match ip {
Some(ip) => vec![WakeTarget {
ip: Some(ip),
mac: Some(mac),
}],
Some(ip) => explicit_wake_targets_for_ip(mac, ip),
None => broadcast_wake_targets(mac).await?,
};
wake_targets(targets).await
@@ -65,3 +54,111 @@ pub async fn resolve_wake_targets(input: impl Into<String>) -> Result<Vec<WakeTa
})
.collect())
}
fn explicit_wake_targets_for_ip(mac: MacAddr, ip: IpAddr) -> Vec<WakeTarget> {
vec![WakeTarget {
ip: Some(ip),
mac: Some(mac),
}]
}
fn broadcast_wake_targets_from_interfaces(
interfaces: &[InterfaceSummary],
mac: MacAddr,
) -> Result<Vec<WakeTarget>> {
let targets: Vec<WakeTarget> = interfaces
.iter()
.flat_map(|iface| iface.addrs.iter())
.filter_map(|addr| addr.broadcast)
.map(|ip| WakeTarget {
ip: Some(IpAddr::V4(ip)),
mac: Some(mac),
})
.collect();
if targets.is_empty() {
anyhow::bail!("no broadcast-capable interfaces found");
}
Ok(targets)
}
#[cfg(test)]
mod tests {
use std::net::{IpAddr, Ipv4Addr};
use super::{broadcast_wake_targets_from_interfaces, explicit_wake_targets_for_ip};
use wakey_core::{InterfaceAddr, InterfaceSummary};
#[test]
fn explicit_wake_target_for_ip_builds_one_complete_target() {
let mac: macaddr::MacAddr = "aa:bb:cc:dd:ee:ff".parse().expect("mac");
let ip = IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255));
let targets = explicit_wake_targets_for_ip(mac, ip);
assert_eq!(targets.len(), 1);
assert_eq!(targets[0].ip, Some(ip));
assert_eq!(targets[0].mac, Some(mac));
}
#[test]
fn broadcast_wake_targets_from_interfaces_errors_when_no_broadcast_exists() {
let mac: macaddr::MacAddr = "aa:bb:cc:dd:ee:ff".parse().expect("mac");
let interfaces = vec![InterfaceSummary {
ifindex: 1,
ifname: "eth0".into(),
operstate: "up".into(),
mac: None,
addrs: vec![InterfaceAddr {
family: Some("inet".into()),
cidr: Some("192.168.1.10/24".into()),
broadcast: None,
scope: Some("global".into()),
label: None,
}],
}];
let err = broadcast_wake_targets_from_interfaces(&interfaces, mac)
.expect_err("should error without broadcast-capable interfaces");
assert!(err.to_string().contains("no broadcast-capable interfaces found"));
}
#[test]
fn broadcast_wake_targets_from_interfaces_builds_targets_from_broadcast_rows() {
let mac: macaddr::MacAddr = "aa:bb:cc:dd:ee:ff".parse().expect("mac");
let interfaces = vec![InterfaceSummary {
ifindex: 2,
ifname: "br-lan".into(),
operstate: "up".into(),
mac: None,
addrs: vec![
InterfaceAddr {
family: Some("inet".into()),
cidr: Some("192.168.1.1/24".into()),
broadcast: Some(Ipv4Addr::new(192, 168, 1, 255)),
scope: Some("global".into()),
label: None,
},
InterfaceAddr {
family: Some("inet6".into()),
cidr: Some("fe80::1/64".into()),
broadcast: None,
scope: Some("link".into()),
label: None,
},
],
}];
let targets = broadcast_wake_targets_from_interfaces(&interfaces, mac)
.expect("broadcast target resolution should succeed");
assert_eq!(targets.len(), 1);
assert_eq!(
targets[0].ip,
Some(IpAddr::V4(Ipv4Addr::new(192, 168, 1, 255)))
);
assert_eq!(targets[0].mac, Some(mac));
}
}