This commit is contained in:
lda
2026-04-06 17:42:04 +07:00 Unverified
parent 9305702b36
commit c393111cb0
9 changed files with 374 additions and 316 deletions
Generated
+243 -309
View File
File diff suppressed because it is too large Load Diff
+3 -1
View File
@@ -24,7 +24,9 @@ tokio = { version = "1", features = [
"io-util",
"macros",
] }
tower-http = { version = "0", features = ["fs"] }
tower-http = { version = "0", features = ["fs", "trace"] }
tracing = "0.1"
tracing-subscriber = { version = "0.3", features = ["env-filter", "fmt"] }
urlencoding = "2"
wakey-core = { path = "wakey-core" }
wakey-linux = { path = "wakey-linux" }
+23
View File
@@ -64,6 +64,29 @@ The long-term direction is:
- keep HTTP as an adapter, not the architecture
- eventually move toward an agent + control-plane model
## Future direction
The likely next large step is splitting the current temporary HTTP/web hosting
role away from the main `wakey` binary.
The intended shape is roughly:
- `wakey`
- stable service layer and operator CLI
- `wakey-agent`
- router-side daemon exposing a network API over the service layer
- control-center app
- remote UI or control plane that talks to one or more agents
That future agent layer will likely need:
- explicit registration/authentication
- a stable remote API
- a small deployment/bootstrap story on the router
The current CLI and compatibility HTTP adapter are being kept small on purpose
so that split can happen later without moving the real product logic again.
## CLI
`wakey` is usable as a local/operator CLI.
+8
View File
@@ -1,3 +1,9 @@
//! Compatibility types and mappers for the legacy HTTP/static client.
//!
//! These types intentionally preserve old JSON shapes expected by `/static`
//! while the core and service layers evolve underneath them. They do not define
//! the long-term domain model of the project.
use serde::Serialize;
use wakey_core::parse::mac;
use wakey_core::{
@@ -49,6 +55,8 @@ pub struct LegacyWakeResultRow {
}
/// Map legacy-style status rows into the old response shape.
///
/// This helper exists for compatibility with the original frontend contract.
pub fn legacy_status_from_domain(status: Status<NeighborEntry>) -> LegacyStatusResponse {
LegacyStatusResponse {
name: status.name,
+18 -2
View File
@@ -1,3 +1,9 @@
//! Temporary HTTP adapter for the legacy web/static surface.
//!
//! This module exists to keep the old `/api` routes and `/static` frontend
//! working while the project is migrated toward a service-first architecture.
//! New product logic should live in [`crate::service`], not here.
pub mod compat;
pub mod route;
@@ -5,12 +11,17 @@ use std::{io, net::SocketAddr};
use axum::Router;
use tokio::net::TcpListener;
use tower_http::services::ServeDir;
use tower_http::{services::ServeDir, trace::TraceLayer};
use tracing::info;
/// Build the temporary HTTP app that serves the legacy API and static frontend.
///
/// This is a compatibility surface. It should stay thin and delegate actual
/// product behavior to the service layer.
pub fn http_app(static_root: std::path::PathBuf) -> Router {
Router::new()
.nest("/api", route::api_router())
.layer(TraceLayer::new_for_http())
.fallback_service(axum::routing::get_service(
ServeDir::new(static_root)
.append_index_html_on_directories(true)
@@ -22,12 +33,17 @@ pub fn http_app(static_root: std::path::PathBuf) -> Router {
}
/// Serve the temporary HTTP app on the provided socket address.
///
/// This is intended for transition and compatibility, not as the long-term
/// architecture boundary of the project.
pub async fn serve_http(addr: SocketAddr, static_root: std::path::PathBuf) -> io::Result<()> {
info!(%addr, static_root = %static_root.display(), "starting legacy http adapter");
let listener = TcpListener::bind(addr).await?;
axum::serve(listener, http_app(static_root).into_make_service()).await
}
/// Serve the HTTP app using the `static/` directory next to the current executable.
/// Serve the HTTP app using the `static/` directory next to the current
/// executable.
pub async fn serve_http_from_current_exe(addr: SocketAddr) -> io::Result<()> {
let exe = std::env::current_exe()?;
let root = exe
+6
View File
@@ -1,2 +1,8 @@
//! Transitional compatibility wrappers preserved during the migration.
//!
//! Items in this module exist so the codebase can keep working while older
//! parsing paths and adapter surfaces are being retired or replaced. New logic
//! should prefer the service layer and the dedicated crate boundaries instead.
pub mod arpparse;
pub mod dhcpparse;
+66 -2
View File
@@ -2,7 +2,8 @@ mod cli_table;
use std::net::{IpAddr, SocketAddr};
use clap::{Args, Parser, Subcommand};
use clap::{ArgAction, Args, Parser, Subcommand};
use tracing::{debug, info};
use wakey_core::{DeviceFilters, DeviceQuery, InterfaceSummary, WakeResult};
#[derive(Parser)]
@@ -12,6 +13,10 @@ use wakey_core::{DeviceFilters, DeviceQuery, InterfaceSummary, WakeResult};
long_about = "Wakey can run as a local/operator CLI or serve the legacy HTTP/static interface during the migration to a service-first architecture."
)]
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,
}
@@ -200,14 +205,17 @@ fn main() -> anyhow::Result<()> {
#[tokio::main]
async fn main() -> anyhow::Result<()> {
let cli = Cli::parse();
init_tracing(cli.verbose);
match cli.command {
Command::Http(args) => {
let addr = SocketAddr::new(args.host, args.port);
info!(%addr, "dispatching http command");
wakey::serve_http_from_current_exe(addr).await?;
}
Command::Status(args) => {
let as_json = args.json;
let query = status_args_to_query(args);
debug!(?query, json = as_json, "dispatching status command");
let status = if query.name.is_some()
&& query.filter.ips.is_empty()
&& query.filter.devs.is_empty()
@@ -228,6 +236,11 @@ async fn main() -> anyhow::Result<()> {
}
}
Command::Leases(args) => {
debug!(
include_state = args.include_state,
json = args.json,
"dispatching leases command"
);
let leases = wakey::get_leases(wakey_core::LeaseQuery {
include_state: args.include_state,
})
@@ -240,6 +253,13 @@ async fn main() -> anyhow::Result<()> {
}
Command::Wake(args) => {
let as_json = args.json;
debug!(
has_query = args.query.is_some(),
has_mac = args.mac.is_some(),
has_ip = args.ip.is_some(),
json = as_json,
"dispatching wake command"
);
let result = run_wake(args).await?;
if as_json {
println!("{}", serde_json::to_string_pretty(&result)?);
@@ -248,6 +268,7 @@ async fn main() -> anyhow::Result<()> {
}
}
Command::Devs(args) => {
debug!(dev = ?args.dev, up = args.up, json = args.json, "dispatching devs command");
let devs = if let Some(name) = &args.dev {
wakey::get_interface_summary(name)
.await?
@@ -267,9 +288,32 @@ async fn main() -> anyhow::Result<()> {
Ok(())
}
#[cfg(target_os = "linux")]
fn init_tracing(verbose: u8) {
use tracing_subscriber::{EnvFilter, fmt, layer::SubscriberExt, util::SubscriberInitExt};
let filter = EnvFilter::try_from_default_env()
.or_else(|_| EnvFilter::try_new(default_filter_for_verbosity(verbose)))
.expect("static tracing filter should parse");
tracing_subscriber::registry()
.with(filter)
.with(fmt::layer())
.init();
}
#[cfg(target_os = "linux")]
fn default_filter_for_verbosity(verbose: u8) -> &'static str {
match verbose {
0 => "wakey=info,tower_http=info",
1 => "wakey=debug,tower_http=debug",
_ => "wakey=trace,tower_http=trace",
}
}
#[cfg(test)]
mod tests {
use super::WakeArgs;
use super::{WakeArgs, default_filter_for_verbosity};
#[test]
fn wake_rejects_ip_without_mac() {
@@ -318,4 +362,24 @@ mod tests {
})
.expect("manual mac mode should be accepted");
}
#[test]
fn verbosity_maps_to_expected_default_filters() {
assert_eq!(
default_filter_for_verbosity(0),
"wakey=info,tower_http=info"
);
assert_eq!(
default_filter_for_verbosity(1),
"wakey=debug,tower_http=debug"
);
assert_eq!(
default_filter_for_verbosity(2),
"wakey=trace,tower_http=trace"
);
assert_eq!(
default_filter_for_verbosity(9),
"wakey=trace,tower_http=trace"
);
}
}
+4 -1
View File
@@ -122,7 +122,10 @@ mod tests {
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"));
assert!(
err.to_string()
.contains("no broadcast-capable interfaces found")
);
}
#[test]
+3 -1
View File
@@ -72,7 +72,9 @@ async fn broadcast_wake_targets_real_router_resolve_from_interfaces() -> anyhow:
"all broadcast targets should preserve the requested MAC"
);
assert!(
targets.iter().all(|target| matches!(target.ip, Some(IpAddr::V4(_)))),
targets
.iter()
.all(|target| matches!(target.ip, Some(IpAddr::V4(_)))),
"broadcast targets should be IPv4 broadcast destinations"
);