THE ABSOLUTE CRIME of not committing frequently

This commit is contained in:
lda
2025-10-20 22:33:23 +07:00 Unverified
parent fabf67297e
commit 1e7db089d5
29 changed files with 538 additions and 489 deletions
+2 -1
View File
@@ -1,3 +1,4 @@
{ {
"rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf" "rust-analyzer.cargo.target": "armv7-unknown-linux-musleabihf",
"rust-analyzer.diagnostics.disabled": ["unlinked-file"]
} }
Generated
+1 -1
View File
@@ -1179,7 +1179,7 @@ checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65"
[[package]] [[package]]
name = "wakey" name = "wakey"
version = "0.1.4" version = "0.1.5"
dependencies = [ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
+1 -1
View File
@@ -1,6 +1,6 @@
[package] [package]
name = "wakey" name = "wakey"
version = "0.1.4" version = "0.1.5"
edition = "2024" edition = "2024"
publish = ["gitea"] publish = ["gitea"]
+1 -1
View File
@@ -15,7 +15,7 @@ start() {
RETRY_COUNT=0 RETRY_COUNT=0
# shellcheck disable=SC2016 # shellcheck disable=SC2016
nohup sh -c ' sh -c '
while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do while [ "$RETRY_COUNT" -lt "$MAX_RETRIES" ]; do
if fn; then if fn; then
echo "[update_tailscale] Success at $(date)" >>"$LOGFILE" echo "[update_tailscale] Success at $(date)" >>"$LOGFILE"
+2 -1
View File
@@ -2,7 +2,8 @@
const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json"; const MAC_NAME_CACHE: &str = "/tmp/wakey_mac_names.json";
/// Load MAC->name cache from disk /// Load MAC->name cache from disk
async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>> { pub(crate) async fn load_mac_name_cache() -> io::Result<std::collections::BTreeMap<String, String>>
{
match tokio::fs::read_to_string(MAC_NAME_CACHE).await { match tokio::fs::read_to_string(MAC_NAME_CACHE).await {
Ok(s) => serde_json::from_str(&s).map_err(io::Error::other), Ok(s) => serde_json::from_str(&s).map_err(io::Error::other),
Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()), Err(e) if e.kind() == ErrorKind::NotFound => Ok(Default::default()),
+12 -8
View File
@@ -1,7 +1,14 @@
use axum::{ //! braindead version v0.1.x
Router, //!
routing::{get, post}, //! # whats next
}; //!
//! for version 2 i hope to have:
//!
//! 1. idk reworked frontend;
//! 2. incorporate ip -j;
//! 3. small 1-5 second caching;
use axum::{Router, routing::get};
use tokio::net::TcpListener; use tokio::net::TcpListener;
mod arpparse; mod arpparse;
pub mod assets; pub mod assets;
@@ -10,18 +17,15 @@ mod route;
mod utils; mod utils;
use std::io; use std::io;
const MACHINE_NAME: &str = "lda.lan";
#[cfg(target_os = "linux")] #[cfg(target_os = "linux")]
#[tokio::main] #[tokio::main]
async fn entry() -> io::Result<()> { async fn entry() -> io::Result<()> {
use crate::route::{api_router, home_2, home_2_route, wake_handler}; use crate::route::{api_router, home_2, home_2_route};
let app = Router::new() let app = Router::new()
// .route("/home", get(home)) // .route("/home", get(home))
.route("/", get(home_2)) .route("/", get(home_2))
.merge(home_2_route()) .merge(home_2_route())
.route("/wake", post(wake_handler))
// .route("/status", get(get_status_2)) // .route("/status", get(get_status_2))
.nest("/api", api_router()); .nest("/api", api_router());
-99
View File
@@ -1,99 +0,0 @@
pub mod api;
pub mod devs;
pub mod dhcp;
pub mod status;
pub mod wake;
pub use crate::route::api::{DeviceQuery, api_router};
use crate::{
assets::{self},
utils::{ping::_ping_ip, wake::wake},
};
use axum::{
Router,
http::{StatusCode, header},
response::{Html, IntoResponse},
routing::get,
};
use axum_extra::extract::Query;
use crate::utils::route::serve_js;
use crate::{MACHINE_NAME, utils::_status_build};
pub async fn wake_handler(
Query(DeviceQuery { name, .. }): Query<DeviceQuery>,
) -> axum::response::Result<impl IntoResponse> {
match wake(name.as_deref().unwrap_or(MACHINE_NAME)).await {
Ok(0) => Err((StatusCode::NOT_FOUND, "No packets sent!").into()),
Ok(x) => Ok((
StatusCode::ACCEPTED,
format!("{x} packet{s} sent!", s = if x > 1 { "s" } else { "" }),
)),
_ => Err((StatusCode::GATEWAY_TIMEOUT, "Wake failed").into()),
}
}
pub async fn _get_status_2(q: Query<DeviceQuery>) -> Html<String> {
let name = match q {
Query(DeviceQuery {
name: Some(name), ..
}) => name,
_ => MACHINE_NAME.to_string(),
};
Html(_status_build(&name).await)
}
pub async fn _home() -> Html<String> {
Html(format!(
r#"
<html>
<body>
<p><a href="/home_2">Alternate UI</a></p>
<p>the machine is {}! <a href="/status">Status</a></p>
<form method="POST" action="/wake">
<button type="submit">Wake LDA</button>
</form>
</body>
</html>
"#,
if _ping_ip((MACHINE_NAME, 22)).await {
"on"
} else {
"off"
} // match get_ips(MACHINE_NAME).await {
// Ok(ips) => {
// // let addrs: Vec<SocketAddr> = ips.into_iter().map(|ip|(ip, 22).into()).collect();
// }
// Err(_) => "off",
// }
))
}
pub async fn home_2() -> Html<&'static str> {
Html(assets::HOME_2_HTML)
}
pub fn home_2_route() -> Router {
use assets::*;
Router::new()
.route("/home_2", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2/", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2.html", get(|| async { Html(HOME_2_HTML) }))
.route(
"/home_2/styles.css",
get(|| async {
(
[
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
home_2::STYLES_CSS,
)
}),
)
.route("/home_2/main.js", get(|| serve_js(home_2::MAIN_JS)))
.route("/home_2/leases.js", get(|| serve_js(home_2::LEASES_JS)))
.route("/home_2/status.js", get(|| serve_js(home_2::STATUS_JS)))
.route("/home_2/utils.js", get(|| serve_js(home_2::UTILS_JS)))
.route("/home_2/wake.js", get(|| serve_js(home_2::WAKE_JS)))
.route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS)))
}
+52 -19
View File
@@ -1,32 +1,63 @@
use crate::route::error::ApiError;
use crate::utils::query_parser::{QueryType, parse_query};
use axum::Json; use axum::Json;
use axum::http::StatusCode; use axum::http::StatusCode;
use axum::response::IntoResponse; use axum::response::IntoResponse;
use axum::routing::post; use axum::{extract::Path, response::Redirect};
use axum::{Router, extract::Path, response::Redirect, routing::get};
use crate::utils::route; use crate::route::status::{DeviceQuery, Filters, NamePath};
use crate::utils::query::get_ips;
// Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly // Smart redirect: accept IP, MAC, dev, or NUD state and redirect to /api/status accordingly
pub async fn status_smart_redirect( pub async fn status_smart_redirect(
Path(q): Path<String>, Path(q): Path<String>,
) -> axum::response::Result<Redirect, impl IntoResponse> { ) -> axum::response::Result<Redirect, impl IntoResponse> {
match serde_html_form::to_string(route::status_smart_redirect(q).await) { // no less bullshit
let query = match parse_query(q) {
QueryType::Ip(ip_addr) => DeviceQuery {
filter: Filters {
ips: vec![ip_addr],
..Default::default()
},
..Default::default()
},
QueryType::Mac(mac_addr) => DeviceQuery {
filter: Filters {
macs: vec![mac_addr],
..Default::default()
},
..Default::default()
},
QueryType::Dev(s) => DeviceQuery {
filter: Filters {
devs: vec![s],
..Default::default()
},
..Default::default()
},
QueryType::Nud(nudstate) => DeviceQuery {
filter: Filters {
nuds: vec![nudstate],
..Default::default()
},
..Default::default()
},
QueryType::Unknown(n) => DeviceQuery {
name: Some(n),
..Default::default()
},
};
match serde_html_form::to_string(query) {
Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))), Ok(e) => Ok(Redirect::to(&format!("/api/status?{e}"))),
Err(e) => Err(( Err(e) => Err((
StatusCode::BAD_GATEWAY, StatusCode::BAD_GATEWAY,
Json(StatusError { Json(ApiError {
error: e.to_string(), error: e.to_string(),
..Default::default()
}), }),
)), )),
} }
} }
pub use crate::route::devs::*;
pub use crate::route::dhcp::*;
pub use crate::route::status::*;
pub use crate::route::wake::*;
pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect { pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirect {
Redirect::permanent(&format!( Redirect::permanent(&format!(
"/api/status?name={name}", "/api/status?name={name}",
@@ -34,12 +65,14 @@ pub async fn status_redirect(Path(NamePath { name }): Path<NamePath>) -> Redirec
)) ))
} }
pub fn api_router() -> Router { pub async fn ip(Path(name): Path<String>) -> impl IntoResponse {
Router::new() get_ips(&name).await.map_or_else(
.route("/status/{name}", get(status_redirect)) |e| {
.route("/status", get(get_status_json)) ApiError {
.route("/dhcp_leases", get(get_dhcp_leases)) error: e.to_string(),
.route("/smart/{q}", get(status_smart_redirect)) }
.route("/devs", get(devs_router)) .into_response()
.route("/wake", post(wake_multi)) },
|ips| Json(ips.collect::<Vec<_>>()).into_response(),
)
} }
+8 -5
View File
@@ -1,4 +1,8 @@
use crate::{dhcpparse, route::api::StatusError, utils::parse::boolish_str}; use crate::{
dhcpparse::read_dhcp_leases_with_names,
route::error::ApiError,
utils::{parse::boolish_str, query::enrich_leases_with_nud_state},
};
use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse}; use axum::{Json, extract::Query, http::StatusCode, response::IntoResponse};
// DHCP lease endpoints // DHCP lease endpoints
@@ -14,19 +18,18 @@ pub async fn get_dhcp_leases(Query(raw): Query<DhcpLeasesQueryRaw>) -> impl Into
.map(boolish_str) .map(boolish_str)
.unwrap_or(false); .unwrap_or(false);
match dhcpparse::read_dhcp_leases_with_names().await { match read_dhcp_leases_with_names().await {
Ok(leases_with_names) => { Ok(leases_with_names) => {
if !include_state { if !include_state {
return (StatusCode::OK, Json(leases_with_names)).into_response(); return (StatusCode::OK, Json(leases_with_names)).into_response();
} }
let out = crate::utils::query::enrich_leases_with_nud_state(leases_with_names).await; let out = enrich_leases_with_nud_state(leases_with_names).await;
(StatusCode::OK, Json(out)).into_response() (StatusCode::OK, Json(out)).into_response()
} }
Err(e) => ( Err(e) => (
StatusCode::BAD_GATEWAY, StatusCode::BAD_GATEWAY,
Json(StatusError { Json(ApiError {
error: e.to_string(), error: e.to_string(),
..Default::default()
}), }),
) )
.into_response(), .into_response(),
+17
View File
@@ -0,0 +1,17 @@
use axum::{
Json,
http::StatusCode,
response::{IntoResponse, Response},
};
use serde::Serialize;
#[derive(Debug, Serialize)]
pub struct ApiError {
pub error: String,
}
impl IntoResponse for ApiError {
fn into_response(self) -> Response {
(StatusCode::INTERNAL_SERVER_ERROR, Json(self)).into_response()
}
}
+60
View File
@@ -0,0 +1,60 @@
pub mod api;
pub mod devs;
pub mod dhcp;
pub mod error;
pub mod status;
pub mod wake;
use crate::assets;
use crate::route::api::ip;
use crate::route::api::status_redirect;
use crate::route::api::status_smart_redirect;
use crate::route::devs::devs_router;
use crate::route::dhcp::get_dhcp_leases;
use crate::route::status::get_status_json;
use crate::route::wake::wake_multi;
use axum::routing::post;
use axum::{Router, http::header, response::Html, routing::get};
use crate::utils::route::serve_js;
pub async fn home_2() -> Html<&'static str> {
Html(assets::HOME_2_HTML)
}
pub fn home_2_route() -> Router {
use assets::*;
Router::new()
.route("/home_2", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2/", get(|| async { Html(HOME_2_HTML) }))
.route("/home_2.html", get(|| async { Html(HOME_2_HTML) }))
.route(
"/home_2/styles.css",
get(|| async {
(
[
(header::CONTENT_TYPE, "text/css; charset=utf-8"),
(header::CACHE_CONTROL, "public, max-age=300"),
],
home_2::STYLES_CSS,
)
}),
)
.route("/home_2/main.js", get(|| serve_js(home_2::MAIN_JS)))
.route("/home_2/leases.js", get(|| serve_js(home_2::LEASES_JS)))
.route("/home_2/status.js", get(|| serve_js(home_2::STATUS_JS)))
.route("/home_2/utils.js", get(|| serve_js(home_2::UTILS_JS)))
.route("/home_2/wake.js", get(|| serve_js(home_2::WAKE_JS)))
.route("/home_2/dom.js", get(|| serve_js(home_2::DOM_JS)))
}
pub fn api_router() -> Router {
Router::new()
.route("/status/{name}", get(status_redirect))
.route("/status", get(get_status_json))
.route("/dhcp_leases", get(get_dhcp_leases))
.route("/smart/{q}", get(status_smart_redirect))
.route("/devs", get(devs_router))
.route("/wake", post(wake_multi))
.route("/ips/{name}", get(ip))
}
+42 -78
View File
@@ -1,33 +1,21 @@
use crate::{ use crate::route::error::ApiError;
arpparse::NUDState,
utils::{
parse::{de_many, serialize_macs},
query::get_macs,
},
};
use axum::{Json, http::StatusCode, response::IntoResponse}; use axum::{Json, http::StatusCode, response::IntoResponse};
use axum_extra::extract::Query; use axum_extra::extract::Query;
use macaddr::MacAddr; use macaddr::MacAddr;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
use crate::arpparse::NUDState;
use crate::utils::parse::de_many;
use crate::utils::parse::serialize_macs;
use crate::utils::query::get_macs;
#[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)] #[derive(Debug, Default, Clone, Hash, Deserialize, Serialize)]
pub struct DeviceQuery { pub struct DeviceQuery {
pub name: Option<String>, pub name: Option<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")] #[serde(flatten)]
pub ip: Vec<IpAddr>, pub filter: Filters,
#[serde(
default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs"
)]
pub mac: Vec<MacAddr>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub dev: Vec<String>,
#[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub nud: Vec<NUDState>,
} }
#[derive(Debug, Default, Clone, Hash, Deserialize)] #[derive(Debug, Default, Clone, Hash, Deserialize)]
@@ -43,21 +31,23 @@ pub struct Status<T> {
pub filters: Filters, pub filters: Filters,
} }
#[derive(Debug, Default, Serialize)] #[derive(Debug, Default, Clone, Hash, Serialize, Deserialize)]
pub struct Filters { pub struct Filters {
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub ip: Vec<IpAddr>, pub ips: Vec<IpAddr>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub dev: Vec<String>, pub devs: Vec<String>,
#[serde(skip_serializing_if = "Vec::is_empty")] #[serde(default, deserialize_with = "de_many::vec_from_strs")]
pub nud: Vec<NUDState>, pub nuds: Vec<NUDState>,
#[serde( #[serde(
skip_serializing_if = "Vec::is_empty", default,
deserialize_with = "de_many::vec_from_strs",
serialize_with = "serialize_macs" serialize_with = "serialize_macs"
)] )]
pub mac: Vec<MacAddr>, pub macs: Vec<MacAddr>,
} }
#[deprecated = "use ApiError"]
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Default)] #[derive(Debug, Serialize, Default)]
pub struct StatusError { pub struct StatusError {
@@ -68,57 +58,32 @@ pub struct StatusError {
pub async fn get_status_json( pub async fn get_status_json(
Query(DeviceQuery { Query(DeviceQuery {
name, name,
ip, filter:
dev, Filters {
nud, ips,
mac, devs,
nuds,
macs,
},
.. ..
}): Query<DeviceQuery>, }): Query<DeviceQuery>,
) -> impl IntoResponse { ) -> impl IntoResponse {
fn to_opts<T: Clone>(slice: &[T]) -> Vec<Option<T>> { match get_macs(
if slice.is_empty() { &name.iter().collect::<Vec<_>>(),
vec![None] &ips,
} else { &devs.iter().collect::<Vec<_>>(),
slice.iter().cloned().map(Some).collect() &nuds,
} &macs,
} )
let ips_opt = if ip.is_empty() { .await
None
} else {
Some(ip.clone())
};
let dev_opts: Vec<Option<String>> = to_opts(&dev);
let nud_opts: Vec<Option<NUDState>> = to_opts(&nud);
let filters = Filters {
ip,
dev,
nud,
mac: mac.clone(),
};
let mut tasks = Vec::new();
// can we jus collect the whole table instead of doing this.
for d in &dev_opts {
for n in &nud_opts {
tasks.push(get_macs(
// this i hate sm
name.as_deref(),
ips_opt.as_deref(),
d.as_deref(),
*n,
));
}
}
// why try join all?
match futures::future::try_join_all(tasks)
.await
.map(|v| v.into_iter().flatten().collect::<Vec<_>>())
{ {
Ok(mut table) => { Ok(table) => {
if !mac.is_empty() { let filters = Filters {
// mac here ips,
let wanted: HashSet<MacAddr> = mac.into_iter().collect(); devs,
table.retain(|row| row.mac.map(|m| wanted.contains(&m)).unwrap_or(false)); nuds,
} macs,
};
( (
StatusCode::OK, StatusCode::OK,
Json(Status { Json(Status {
@@ -131,8 +96,7 @@ pub async fn get_status_json(
} }
Err(error) => ( Err(error) => (
StatusCode::BAD_GATEWAY, StatusCode::BAD_GATEWAY,
Json(StatusError { Json(ApiError {
name,
error: error.to_string(), error: error.to_string(),
}), }),
) )
+13 -20
View File
@@ -23,9 +23,8 @@ pub struct WakeResult {
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Serialize, Clone, Copy)] #[derive(Debug, Serialize, Clone, Copy)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub ip: Option<IpAddr>, #[serde(flatten)]
#[serde(serialize_with = "ser_opm")] pub target: WakeTarget,
pub mac: Option<MacAddr>,
pub status: WakeTargetStatus, pub status: WakeTargetStatus,
} }
@@ -41,11 +40,11 @@ pub enum WakeTargetStatus {
} }
#[skip_serializing_none] #[skip_serializing_none]
#[derive(Debug, Deserialize, Clone, Copy)] #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
pub struct WakeTarget { pub struct WakeTarget {
#[serde(default)] #[serde(default)]
pub ip: Option<IpAddr>, pub ip: Option<IpAddr>,
#[serde(default, deserialize_with = "des_opm")] #[serde(default, serialize_with = "ser_opm", deserialize_with = "des_opm")]
pub mac: Option<MacAddr>, pub mac: Option<MacAddr>,
} }
@@ -82,21 +81,15 @@ pub async fn wake_multi_split(
.await?; .await?;
sock.set_broadcast(true)?; sock.set_broadcast(true)?;
Ok( let iter = targets.into_iter().map(async |c| {
futures::future::join_all(targets.into_iter().map(async |c| { if c.is_incomplete() {
if c.is_incomplete() { c.to_incomplete()
c.to_incomplete() } else {
} else { let t = c.try_into().expect("complete struct failed to try_into");
wake_one( wake_one(&sock, t).await.into()
&sock, }
c.try_into().expect("complete struct failed to try_into"), // type state pattern? });
) Ok(futures::future::join_all(iter).await)
.await
.into()
}
}))
.await,
)
} }
/* #[derive(Debug, Serialize)] /* #[derive(Debug, Serialize)]
pub struct WakeStatusLine { pub struct WakeStatusLine {
-64
View File
@@ -1,64 +0,0 @@
// pub const LDA_MACS: [[u8; 6]; 2] = [
// // ether
// [0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
// // wifi
// [0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
// ];
/// is it time to lookup host lda.lan for this...
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub mod wake;
use crate::utils::query::_get_macs_2_1;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
pub mod query;
pub mod route;
// no custom ip deserializer needed when using axum_extra::extract::Query
// but we add a generic one to ignore blanks and accept OneOrMany
pub(crate) mod parse;
pub async fn _status_build(machine_name: &str) -> String {
let formatted_macs = match _get_macs_2_1(machine_name).await {
Ok(table) => {
let the: String = table
.iter()
.map(|(ip, mac, state)| {
let mac_str = // if let Some(mac) = mac {
mac.to_string()
// } else {
// "None".into()
// }
;
format!(
"<tr><td>{ip}</td><td>{mac_str}</td><td>{state}</td></tr>",
state = state._dumber_state()
)
})
.collect();
format!(
r#"<p>info of {machine_name}:</p>
<table>
<tr><th>IP</th><th>MAC</th><th>State</th></tr>
{the}
</table>"#
)
}
Err(e) => format!("<p>errors getting table for {machine_name}: {e}</p>"),
};
format!(
r#"
<html>
<body>
{formatted_macs}
</body>
</html>
"#,
)
}
+23
View File
@@ -0,0 +1,23 @@
// pub const LDA_MACS: [[u8; 6]; 2] = [
// // ether
// [0x04, 0x7c, 0x16, 0x79, 0x6d, 0xee],
// // wifi
// [0xbc, 0x09, 0x1b, 0xec, 0x65, 0xd0],
// ];
/// is it time to lookup host lda.lan for this...
// pub static LDA_MACS_2: LazyLock<[MacAddr; 2]> = LazyLock::new(|| LDA_MACS.map(MacAddr::from));
pub mod wake;
pub mod cmd;
pub mod error;
/// generic so you can do "123.45.67.89:22" or "lda.lan:22" as an input
// this is so bad
pub mod ping;
pub mod query;
pub mod query_parser;
pub mod route;
// no custom ip deserializer needed when using axum_extra::extract::Query
// but we add a generic one to ignore blanks and accept OneOrMany
pub(crate) mod parse;
+2 -2
View File
@@ -7,7 +7,7 @@ use tokio::{
time::timeout, time::timeout,
}; };
use crate::{arpparse::NUDState, utils::query::get_macs}; use crate::{arpparse::NUDState, utils::query::get_mac};
pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool { pub async fn _ping_ip<T: ToSocketAddrs>(addr: T) -> bool {
timeout(Duration::from_secs(1), TcpStream::connect(addr)) timeout(Duration::from_secs(1), TcpStream::connect(addr))
@@ -19,7 +19,7 @@ pub async fn _ping_ip_2<T: ToSocketAddrs>(_addr: T) -> bool {
} }
pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 { pub async fn _ping_ip_3<T: Into<IpAddr>>(addr: T) -> u8 {
match get_macs(None, Some(&[addr.into()]), None, None).await { match get_mac(Some(addr.into()), None, None).await {
Err(_) => 0, Err(_) => 0,
Ok(l) => l Ok(l) => l
.into_iter() .into_iter()
+2 -2
View File
@@ -1,5 +1,6 @@
use crate::arpparse::NUDState; use crate::arpparse::NUDState;
use crate::dhcpparse::DhcpLeaseLine; use crate::dhcpparse::DhcpLeaseLine;
use crate::utils::query::get_macs;
use serde_with::skip_serializing_none; use serde_with::skip_serializing_none;
use std::net::IpAddr; use std::net::IpAddr;
@@ -14,11 +15,10 @@ pub struct DhcpLeaseOut {
/// Enrich DHCP leases with NUD state and rank using get_macs /// Enrich DHCP leases with NUD state and rank using get_macs
pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> { pub async fn enrich_leases_with_nud_state(leases: Vec<DhcpLeaseLine>) -> Vec<DhcpLeaseOut> {
use crate::utils::query::macs::get_macs;
let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect(); let ips: Vec<IpAddr> = leases.iter().map(|l| l.ip).collect();
let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> = let mut map: std::collections::HashMap<IpAddr, (NUDState, u8)> =
std::collections::HashMap::new(); std::collections::HashMap::new();
if let Ok(rows) = get_macs(None, Some(&ips), None, None).await { if let Ok(rows) = get_macs(&[] as &[&str], &ips, &[] as &[&str], &[], &[]).await {
for row in rows { for row in rows {
let state = row.state; let state = row.state;
let r = state.rank(); let r = state.rank();
+99 -79
View File
@@ -1,13 +1,14 @@
use macaddr::MacAddr;
use crate::arpparse::{self, IpNeighLine, NUDState}; use crate::arpparse::{self, IpNeighLine, NUDState};
use crate::utils::{ use crate::utils::{
cmd::exec_command, cmd::exec_command,
error::{self, Error, Result}, error::{self, Result},
}; };
use macaddr::MacAddr;
use std::collections::HashSet; use std::collections::HashSet;
use std::net::IpAddr; use std::net::IpAddr;
pub async fn get_ips(machine_name: &str) -> error::Result<impl Iterator<Item = IpAddr>> { pub async fn get_ips(machine_name: &str) -> Result<impl Iterator<Item = IpAddr>> {
Ok(tokio::net::lookup_host((machine_name, 0)) Ok(tokio::net::lookup_host((machine_name, 0))
.await .await
.map_err(|e| error::Error::DnsResolve { .map_err(|e| error::Error::DnsResolve {
@@ -17,84 +18,103 @@ pub async fn get_ips(machine_name: &str) -> error::Result<impl Iterator<Item = I
.map(|c| c.ip())) .map(|c| c.ip()))
} }
pub async fn _get_macs_2_1(machine_name: &str) -> Result<HashSet<(IpAddr, MacAddr, NUDState)>> { // #[deprecated(
Ok(get_macs_1(machine_name) // since = "0.1.5",
.await? // note = "just call once everything with get mac
.into_iter() // and then filter it bro WHY DO YOU EVEN DO TS"
.filter_map( // )]
|IpNeighLine { // good now
ip, //
dev: _, // Current logic: When filtering by exactly 1 dev/mac, exclude entries missing that field.
mac, // This is because missing dev/mac usually means the entry is incomplete/transient.
state, //
}| mac.map(|mac| (ip, mac, state)), // when there is only one MACs (getmac got some), the result will not have them fields.
) // so there are three cases:
.collect()) //
} // 1. dont got nothing: take all of them (macset.is_empty())
// 2. exactly one: pre-filtered by ip, everything matches,
pub async fn get_macs_2_mac(machine_name: &str) -> Result<HashSet<MacAddr>> { // devset.len() != 1 returns false, but then it works????
Ok(get_macs_1(machine_name) // OH THIS fuckass code i added it in the get_mac
.await? // 3. devset.len() > 1. if none then absolutely not match,
.into_iter() // if some then check with the set; thats normal
.filter_map(
|IpNeighLine {
ip: _,
dev: _,
mac,
state: _,
}| mac,
)
.collect())
}
pub async fn get_macs_1(machine_name: &str) -> Result<Vec<arpparse::IpNeighLine>> {
let dev = "br-lan";
let ips = get_ips(machine_name).await?;
let futures = ips.map(|ip| {
let ip = ip.to_canonical();
async move {
let cmd = "ip";
let args = ["neigh", "show", "to", &ip.to_string(), "dev", dev];
let o = exec_command(cmd, args).await?;
if !o.status.success() {
return Err(Error::CommandFailed {
cmd,
args: args.iter().map(ToString::to_string).collect(),
status: o.status.code(),
stderr: String::from_utf8_lossy(&o.stderr).into(),
});
};
Ok(String::from_utf8_lossy(&o.stdout)
.lines()
.flat_map(arpparse::parse_ip_neigh_line)
.collect::<Vec<_>>())
}
});
let res = futures::future::try_join_all(futures).await?;
Ok(res.into_iter().flatten().collect())
}
pub async fn get_macs( pub async fn get_macs(
machine_name: Option<&str>, machine_names: &[impl AsRef<str>],
ips: Option<&[IpAddr]>, ips: &[IpAddr],
dev: Option<&str>, devs: &[impl AsRef<str>],
state: Option<NUDState>, state: &[NUDState],
macs: &[MacAddr],
) -> Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let ip_list: Option<Vec<IpAddr>> = let mut ip_set: HashSet<IpAddr> = ips.iter().map(|ip| ip.to_canonical()).collect();
ips.map(|slice| slice.iter().map(|ip| ip.to_canonical()).collect()); let ip_m: HashSet<IpAddr> = futures::future::try_join_all(
let ip_list = match (ip_list, machine_name) { machine_names
(Some(list), _) => list, .iter()
(None, Some(name)) => get_ips(name).await?.collect(), .map(|c| async { get_ips(c.as_ref()).await }),
(None, None) => Vec::new(), )
}; .await?
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, dev, state); .into_iter()
if !ip_list.is_empty() { .flatten()
let futures = ip_list.into_iter().map(|ip| run_one(Some(ip))); .collect();
let res = futures::future::try_join_all(futures).await?; let ip_all = if ip_set.is_empty() && ip_m.is_empty() {
Ok(res.into_iter().flatten().collect()) None
} else if ip_set.is_empty() {
Some(ip_m)
} else if ip_m.is_empty() {
Some(ip_set)
} else { } else {
run_one(None).await Some({
} ip_set.retain(|c| ip_m.contains(c)); // inline AHHH
ip_set
})
};
let opt_dev = if devs.len() > 1 {
None
} else {
devs.iter().next().map(AsRef::as_ref)
};
let opt_state = if state.len() > 1 {
None
} else {
state.iter().next().copied()
};
let run_one = |to_ip: Option<IpAddr>| get_mac(to_ip, opt_dev, opt_state);
let mut ip_filtered = if let Some(something) = ip_all {
if something.len() == 1 {
run_one(something.into_iter().next()).await?
} else {
run_one(None)
.await?
.into_iter()
.filter(|c| something.contains(&c.ip))
.collect()
}
} else {
run_one(None).await?
};
// Apply additional filters if any were provided
if !devs.is_empty() || !macs.is_empty() || !state.is_empty() {
let devset: HashSet<_> = devs.iter().map(AsRef::as_ref).collect();
let nudset: HashSet<_> = state.iter().collect();
let macset: HashSet<_> = macs.iter().collect();
ip_filtered.retain(|entry| {
// Dev filter: if we're filtering by dev, entry must have a dev AND it must be in the set
let dev_ok =
devset.is_empty() || entry.dev.as_deref().is_some_and(|d| devset.contains(d));
// NUD filter: always present, simple check
let nud_ok = nudset.is_empty() || nudset.contains(&entry.state);
// MAC filter: if we're filtering by MAC, entry must have a MAC AND it must be in the set
let mac_ok = macset.is_empty() || entry.mac.is_some_and(|m| macset.contains(&m));
dev_ok && nud_ok && mac_ok
})
};
Ok(ip_filtered)
} }
/// the atomic get_macs. handle ONE thing only. /// the atomic get_macs. handle ONE thing only.
@@ -102,7 +122,7 @@ pub async fn get_mac(
ip: Option<IpAddr>, ip: Option<IpAddr>,
dev: Option<&str>, dev: Option<&str>,
state: Option<NUDState>, state: Option<NUDState>,
) -> error::Result<Vec<IpNeighLine>> { ) -> Result<Vec<IpNeighLine>> {
let mut args: Vec<String> = vec!["neigh".into(), "show".into()]; let mut args: Vec<String> = vec!["neigh".into(), "show".into()];
if let Some(ip) = ip { if let Some(ip) = ip {
args.push("to".into()); args.push("to".into());
+44
View File
@@ -0,0 +1,44 @@
use std::net::IpAddr;
use macaddr::MacAddr;
use crate::{arpparse::NUDState, utils::query::dev::has_dev};
pub enum QueryType {
Ip(IpAddr),
Mac(MacAddr),
Dev(String),
Nud(NUDState),
Unknown(String),
}
pub fn parse_query(q: String) -> QueryType {
let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q)
} else {
q.trim()
};
// 1) IP
let ip = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok())
} else {
s.parse::<IpAddr>().ok()
};
if let Some(ip) = ip {
return QueryType::Ip(ip);
}
// 2) MAC
if let Ok(mac) = s.parse::<MacAddr>() {
return QueryType::Mac(mac);
}
// 3) NUD state (reachable, stale, ...)
if let Ok(state) = s.parse::<NUDState>() {
return QueryType::Nud(state);
}
// 4) Known device? prefer dev first
if has_dev(s) {
return QueryType::Dev(s.to_string());
}
// Default: name last // it will fail also
QueryType::Unknown(s.to_string())
}
-61
View File
@@ -1,9 +1,4 @@
use std::net::IpAddr;
use axum::{http::header, response::IntoResponse}; use axum::{http::header, response::IntoResponse};
use macaddr::MacAddr;
use crate::{arpparse::NUDState, route::DeviceQuery, utils::query::dev::has_dev};
pub async fn serve_js(content: &'static str) -> impl IntoResponse { pub async fn serve_js(content: &'static str) -> impl IntoResponse {
( (
@@ -14,59 +9,3 @@ pub async fn serve_js(content: &'static str) -> impl IntoResponse {
content, content,
) )
} }
pub async fn status_smart_redirect(q: String) -> DeviceQuery {
let s = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::extract_host(&q)
} else {
q.trim()
};
// 1) IP
let ip = if cfg!(feature = "very-smart-parsing") {
crate::utils::parse::parse_numeric_ipv4(s).or_else(|| s.parse::<IpAddr>().ok())
} else {
s.parse::<IpAddr>().ok()
};
if let Some(ip) = ip {
let ip = vec![ip];
return DeviceQuery {
ip,
..Default::default()
};
}
// 2) MAC
if let Ok(mac) = s.parse::<MacAddr>() {
let mac = vec![mac];
return DeviceQuery {
mac,
..Default::default()
};
}
// 3) NUD state (reachable, stale, ...)
if let Ok(state) = s.parse::<NUDState>() {
let nud = vec![state];
return DeviceQuery {
nud,
..Default::default()
};
}
// 4) Known device? prefer dev first
if has_dev(s) {
return DeviceQuery {
dev: vec![s.to_string()],
..Default::default()
};
}
// 5) Try DNS: if it resolves, treat as name
/* if tokio::net::lookup_host((s, 0)).await.is_ok() {
return DeviceQuery {
name: Some(s.to_string()),
..Default::default()
};
} */
// Default: name last // it will fail also
DeviceQuery {
name: Some(s.to_string()),
..Default::default()
}
}
+11 -4
View File
@@ -22,11 +22,19 @@ impl TryFrom<RouteWakeTarget> for WakeTarget {
} }
} }
impl From<WakeTargetResult> for RouteWakeResult { impl From<WakeTarget> for RouteWakeTarget {
fn from(WakeTargetResult { ip, mac, status }: WakeTargetResult) -> Self { fn from(WakeTarget { ip, mac }: WakeTarget) -> Self {
Self { Self {
ip: Some(ip), ip: Some(ip),
mac: Some(mac), mac: Some(mac),
}
}
}
impl From<WakeTargetResult> for RouteWakeResult {
fn from(WakeTargetResult { target, status }: WakeTargetResult) -> Self {
Self {
target: target.into(),
status: status.into(), status: status.into(),
} }
} }
@@ -35,8 +43,7 @@ impl From<WakeTargetResult> for RouteWakeResult {
impl RouteWakeTarget { impl RouteWakeTarget {
pub fn to_incomplete(self) -> RouteWakeResult { pub fn to_incomplete(self) -> RouteWakeResult {
RouteWakeResult { RouteWakeResult {
ip: self.ip, target: self,
mac: self.mac,
status: RouteWakeStatus::Incomplete, status: RouteWakeStatus::Incomplete,
} }
} }
+6 -36
View File
@@ -5,35 +5,6 @@ use futures::TryFutureExt;
use macaddr::MacAddr; use macaddr::MacAddr;
use tokio::net::UdpSocket; use tokio::net::UdpSocket;
use crate::utils::query::get_macs_2_mac;
pub async fn wake(machine_name: &str) -> io::Result<u32> {
let suh = UdpSocket::bind("0.0.0.0:0").await?;
suh.set_broadcast(true)?;
let /* mut */ macs = get_macs_2_mac(machine_name).await.unwrap_or_default();
// macs.extend(*LDA_MACS_2);
let mut sent_ok = 0;
for mac in macs {
let mb = mac.as_bytes();
let mut pac = [0; 6 + 6 * 16]; // 6x FF + 16x mac6
pac[..6].fill(0xff);
for i in 1..=16 {
pac[i * 6..(i + 1) * 6].copy_from_slice(mb);
}
match suh
.send_to(&pac, (IpAddr::from([192, 168, 100, 255]), 9))
.await
{
Ok(n) if n == pac.len() => sent_ok += 1,
Ok(n) => eprintln!("partial send ({n}/{})", pac.len()),
Err(e) => eprintln!("send error: {e}"),
}
}
Ok(sent_ok)
}
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTarget { pub struct WakeTarget {
pub ip: IpAddr, pub ip: IpAddr,
@@ -41,8 +12,7 @@ pub struct WakeTarget {
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
pub struct WakeTargetResult { pub struct WakeTargetResult {
pub ip: IpAddr, pub target: WakeTarget,
pub mac: MacAddr,
pub status: WakeStatus, pub status: WakeStatus,
} }
#[derive(Debug, Clone, Copy, Hash)] #[derive(Debug, Clone, Copy, Hash)]
@@ -56,18 +26,18 @@ impl WakeTarget {
Self { ip, mac } Self { ip, mac }
} }
fn good(self) -> WakeTargetResult { fn good(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::Success) WakeTargetResult::new(self, WakeStatus::Success)
} }
fn bad(self) -> WakeTargetResult { fn bad(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::WrongSize) WakeTargetResult::new(self, WakeStatus::WrongSize)
} }
fn errored(self) -> WakeTargetResult { fn errored(self) -> WakeTargetResult {
WakeTargetResult::new(self.ip, self.mac, WakeStatus::NonexistentAddress) WakeTargetResult::new(self, WakeStatus::NonexistentAddress)
} }
} }
impl WakeTargetResult { impl WakeTargetResult {
fn new(ip: IpAddr, mac: MacAddr, status: WakeStatus) -> Self { fn new(target: WakeTarget, status: WakeStatus) -> Self {
Self { ip, mac, status } Self { target, status }
} }
} }
+1 -1
View File
@@ -27,7 +27,7 @@
<div class="row muted" style="gap: 16px"> <div class="row muted" style="gap: 16px">
<span <span
>Uses query >Uses query
<span title="available keys: name, ip, mac, dev, nud" <span title="available keys: name, ips, macs, devs, nuds"
>(?name=...)</span >(?name=...)</span
> >
on this page to view the status.<!-- and header (X-Target-Name) so either extractor on this page to view the status.<!-- and header (X-Target-Name) so either extractor
+12
View File
@@ -1,5 +1,17 @@
import { elLeases } from "./dom.js"; import { elLeases } from "./dom.js";
/**
*
* @param {{
* expires_epoch: Number
* rank?: Number
* ip: String
* mac: String
* nud_state?: String
* name?: String
* }[]} leases
* @returns
*/
export function renderLeases(leases) { export function renderLeases(leases) {
if (!elLeases) return; if (!elLeases) return;
if (!Array.isArray(leases) || leases.length === 0) { if (!Array.isArray(leases) || leases.length === 0) {
+89 -3
View File
@@ -2,6 +2,7 @@ import { elHtml, elLog, setPill, qs, pill } from "./dom.js";
import { rankState } from "./utils.js"; import { rankState } from "./utils.js";
import { merge_wake_data, translate_wake_message } from "./wake.js"; import { merge_wake_data, translate_wake_message } from "./wake.js";
// IpNeighLine
const status_map = { const status_map = {
ip: "ip", ip: "ip",
mac: "mac", mac: "mac",
@@ -9,8 +10,14 @@ const status_map = {
dev: "interface", dev: "interface",
}; };
export const status_array = Object.keys(status_map); export const status_array = Object.keys(status_map);
export const filter_array = ["ip", "dev", "nud", "mac"]; // Filters
export const filter_array = ["ips", "devs", "nuds", "macs"];
/**
*
* @param {String} name
* @returns {URL}
*/
function buildStatusUrl(name) { function buildStatusUrl(name) {
const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length); const hasExtraFilters = filter_array.some((k) => qs.getAll(k).length);
if (name && !hasExtraFilters) { if (name && !hasExtraFilters) {
@@ -25,6 +32,25 @@ function buildStatusUrl(name) {
return u; return u;
} }
/**
*
* @param {{
* has_wake?: true
* table: {
* wake_status?: Boolean
* ips: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* }} data
*/
export function renderStatus(data) { export function renderStatus(data) {
const tbl = document.createElement("table"); const tbl = document.createElement("table");
tbl.className = "table"; tbl.className = "table";
@@ -82,12 +108,51 @@ export function renderStatus(data) {
if (r >= 5) setPill("ok", "online"); if (r >= 5) setPill("ok", "online");
else if (r >= 2) setPill("warn", "maybe"); else if (r >= 2) setPill("warn", "maybe");
else setPill("bad", "offline"); else setPill("bad", "offline");
if (data.filters.nud?.length > 0) pill.textContent += " (filtered)"; if (data.filters.nuds?.length > 0) pill.textContent += " (filtered)";
} else { } else {
setPill("warn", "unknown"); setPill("warn", "unknown");
} }
} }
/**
* @param {String} name
* @param {Boolean} render
* @param {{
* ip: String,
* mac: String,
* status:
* "incomplete" | "succeed" | "nonexistent_address" | "wrong_size"
* }} data_wake
* @returns {{
* has_wake: false
* table: {
* ip: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* } | {
* has_wake: true
* table: {
* wake_status: Boolean
* ips: String
* dev: String
* mac: String
* state: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* }}
*/
export async function fetchStatus(name, render = true, data_wake) { export async function fetchStatus(name, render = true, data_wake) {
setPill("warn", "checking…"); setPill("warn", "checking…");
const u = buildStatusUrl(name); const u = buildStatusUrl(name);
@@ -97,6 +162,7 @@ export async function fetchStatus(name, render = true, data_wake) {
if (!r.ok) { if (!r.ok) {
let msg = String(r.status); let msg = String(r.status);
try { try {
/** @type {{error: string}} */
const err = await r.clone().json(); const err = await r.clone().json();
msg = err.error || JSON.stringify(err); msg = err.error || JSON.stringify(err);
} catch { } catch {
@@ -106,6 +172,26 @@ export async function fetchStatus(name, render = true, data_wake) {
setPill("bad", "error"); setPill("bad", "error");
return; return;
} }
/**
* this one does not have data wake
* @type {{
* table: {
* ip: String
* dev: String
* mac: String
* state: String
* wake_status?: String
* }[]
* filters: {
* ips?: String[]
* devs?: String[]
* nuds?: String[]
* macs?: String[]
* }
* has_wake?: true
* }}
*/
const data = await r.json(); const data = await r.json();
if (data_wake) { if (data_wake) {
+7
View File
@@ -15,6 +15,13 @@ body {
padding: 0; padding: 0;
font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif; font-family: system-ui, Segoe UI, Roboto, Helvetica, Arial, sans-serif;
} }
@media screen and (max-width: 768px) {
wrap {
margin-inline: 8px;
}
}
body { body {
display: flex; display: flex;
min-height: 100dvh; min-height: 100dvh;
+31 -3
View File
@@ -1,6 +1,9 @@
import { elHtml, elLog, setPill } from "./dom.js"; import { elHtml, elLog, setPill } from "./dom.js";
import { fetchStatus, renderStatus } from "./status.js"; import { fetchStatus, renderStatus } from "./status.js";
/**
* @param {String} name just plain name
*/
export async function sendWake(name) { export async function sendWake(name) {
const data = await fetchStatus(name, false); const data = await fetchStatus(name, false);
if (!data) return; // can not proceed; theres nothing. if (!data) return; // can not proceed; theres nothing.
@@ -51,8 +54,29 @@ export async function sendWake(name) {
/** /**
* *
* @param {Array} table * @param {{
* @param {Array} wake * ip: String
* dev: String
* mac: String
* state: String
* }[]} table
* @param {{
* ip: String,
* mac: String,
* status: "incomplete" | "succeed" | "nonexistent_address" | "wrong_size"
* }[]} wake
* @returns {{
* ip: String
* dev: String
* mac: String
* state: String
* }[] | {
* ip: String
* dev: String
* mac: String
* state: String
* wake_status: boolean
* }[]}
*/ */
export function merge_wake_data(table, wake) { export function merge_wake_data(table, wake) {
if (!wake) return table; if (!wake) return table;
@@ -90,7 +114,11 @@ export function merge_wake_data(table, wake) {
return return_array; return return_array;
} }
/**
*
* @param {"incomplete" | "succeed" | "nonexistent_address" | "wrong_size" | any} wake_msg
* @returns {string}
*/
export function translate_wake_message(wake_msg) { export function translate_wake_message(wake_msg) {
switch (wake_msg) { switch (wake_msg) {
case "incomplete": case "incomplete":