Real Bug over here
This commit is contained in:
@@ -1,3 +1,4 @@
|
|||||||
We work towards clean, modular, maintainable design, clear documentation... im not the best at this thing, my choices can be bad, so you help me.
|
We work towards clean, modular, maintainable design, clear documentation... im not the best at this thing, my choices can be bad, so you help me.
|
||||||
|
|
||||||
DRY: spawn subagents.
|
DRY: spawn subagents.
|
||||||
|
Context: break those files out.
|
||||||
|
|||||||
@@ -6,10 +6,13 @@ USE_PROCD=1
|
|||||||
|
|
||||||
NAME=wakey
|
NAME=wakey
|
||||||
BIN=/root/.bin/wakey-agent
|
BIN=/root/.bin/wakey-agent
|
||||||
|
CONFIG=/etc/wakey-agent/config.toml
|
||||||
|
|
||||||
start_service() {
|
start_service() {
|
||||||
procd_open_instance
|
procd_open_instance
|
||||||
procd_set_param command "$BIN" serve
|
procd_set_param command "$BIN" serve --config "$CONFIG"
|
||||||
|
procd_set_param file "$CONFIG"
|
||||||
|
procd_set_param env RUST_LOG=wakey_agent=debug,wakey=debug
|
||||||
procd_set_param respawn 5 1 0
|
procd_set_param respawn 5 1 0
|
||||||
procd_set_param stdout 1
|
procd_set_param stdout 1
|
||||||
procd_set_param stderr 1
|
procd_set_param stderr 1
|
||||||
@@ -19,3 +22,8 @@ start_service() {
|
|||||||
stop_service() {
|
stop_service() {
|
||||||
: # procd manages the process; nothing to do here
|
: # procd manages the process; nothing to do here
|
||||||
}
|
}
|
||||||
|
|
||||||
|
reload_service() {
|
||||||
|
# Prefer in-process reload; fallback to full restart if signal path is unavailable.
|
||||||
|
procd_send_signal "$NAME" HUP 2>/dev/null || restart
|
||||||
|
}
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ async fn dispatch_leases(req: LeasesRequest) -> Result<CommandResult> {
|
|||||||
})
|
})
|
||||||
.await?;
|
.await?;
|
||||||
debug!(rows = leases.len(), include_state = req.include_state, "dispatched leases command");
|
debug!(rows = leases.len(), include_state = req.include_state, "dispatched leases command");
|
||||||
Ok(CommandResult::Leases(leases))
|
Ok(CommandResult::Leases { rows: leases })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch_devs(req: DevsRequest) -> Result<CommandResult> {
|
async fn dispatch_devs(req: DevsRequest) -> Result<CommandResult> {
|
||||||
@@ -54,7 +54,7 @@ async fn dispatch_devs(req: DevsRequest) -> Result<CommandResult> {
|
|||||||
devs.retain(|dev| dev.operstate == "up");
|
devs.retain(|dev| dev.operstate == "up");
|
||||||
}
|
}
|
||||||
debug!(rows = devs.len(), up_only = req.up_only, "dispatched devs command");
|
debug!(rows = devs.len(), up_only = req.up_only, "dispatched devs command");
|
||||||
Ok(CommandResult::Devs(devs))
|
Ok(CommandResult::Devs { rows: devs })
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn dispatch_inventory(req: InventoryRequest) -> Result<CommandResult> {
|
async fn dispatch_inventory(req: InventoryRequest) -> Result<CommandResult> {
|
||||||
|
|||||||
@@ -175,8 +175,8 @@ pub enum AgentCommand {
|
|||||||
#[serde(tag = "kind", rename_all = "snake_case")]
|
#[serde(tag = "kind", rename_all = "snake_case")]
|
||||||
pub enum CommandResult {
|
pub enum CommandResult {
|
||||||
Status(Status<NeighborEntry>),
|
Status(Status<NeighborEntry>),
|
||||||
Leases(Vec<DhcpLeaseWithState>),
|
Leases { rows: Vec<DhcpLeaseWithState> },
|
||||||
Devs(Vec<InterfaceSummary>),
|
Devs { rows: Vec<InterfaceSummary> },
|
||||||
Inventory(DeviceInventory),
|
Inventory(DeviceInventory),
|
||||||
Wake(WakeResult),
|
Wake(WakeResult),
|
||||||
}
|
}
|
||||||
@@ -236,4 +236,16 @@ mod tests {
|
|||||||
let err = RequestId::try_from(" ".to_string()).expect_err("must fail");
|
let err = RequestId::try_from(" ".to_string()).expect_err("must fail");
|
||||||
assert!(err.contains("must not be empty"));
|
assert!(err.contains("must not be empty"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn client_result_with_devs_serializes() {
|
||||||
|
let msg = ClientMessage::Result {
|
||||||
|
request_id: RequestId::try_from("req-devs-1".to_string()).expect("request id"),
|
||||||
|
result: CommandResult::Devs { rows: vec![] },
|
||||||
|
};
|
||||||
|
|
||||||
|
let json = serde_json::to_string(&msg).expect("serialize");
|
||||||
|
assert!(json.contains("\"type\":\"result\""));
|
||||||
|
assert!(json.contains("\"kind\":\"devs\""));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+139
-2
@@ -128,7 +128,7 @@ where
|
|||||||
match dispatch_command(command).await {
|
match dispatch_command(command).await {
|
||||||
Ok(result) => {
|
Ok(result) => {
|
||||||
info!(request_id = %request_id, command = %kind, "command execution completed");
|
info!(request_id = %request_id, command = %kind, "command execution completed");
|
||||||
send_json(sink, &ClientMessage::Result { request_id, result }).await?;
|
send_command_result(sink, request_id, kind, result).await?;
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
error!(request_id = %request_id, command = %kind, error = %err, "command dispatch failed");
|
error!(request_id = %request_id, command = %kind, error = %err, "command dispatch failed");
|
||||||
@@ -151,6 +151,49 @@ where
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async fn send_command_result<S>(
|
||||||
|
sink: &mut S,
|
||||||
|
request_id: crate::protocol::RequestId,
|
||||||
|
kind: &str,
|
||||||
|
result: crate::protocol::CommandResult,
|
||||||
|
) -> Result<()>
|
||||||
|
where
|
||||||
|
S: SinkExt<Message> + Unpin,
|
||||||
|
<S as futures_util::Sink<Message>>::Error: std::error::Error + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
if let Err(err) = send_json(
|
||||||
|
sink,
|
||||||
|
&ClientMessage::Result {
|
||||||
|
request_id: request_id.clone(),
|
||||||
|
result,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
{
|
||||||
|
error!(
|
||||||
|
request_id = %request_id,
|
||||||
|
command = %kind,
|
||||||
|
error = %err,
|
||||||
|
"failed to send command result; sending explicit error frame"
|
||||||
|
);
|
||||||
|
|
||||||
|
send_json(
|
||||||
|
sink,
|
||||||
|
&ClientMessage::Error {
|
||||||
|
request_id,
|
||||||
|
error: ErrorPayload {
|
||||||
|
code: "command_result_serialize_failed".into(),
|
||||||
|
message: err.to_string(),
|
||||||
|
retryable: Some(true),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn send_json<S>(sink: &mut S, message: &ClientMessage) -> Result<()>
|
async fn send_json<S>(sink: &mut S, message: &ClientMessage) -> Result<()>
|
||||||
where
|
where
|
||||||
S: SinkExt<Message> + Unpin,
|
S: SinkExt<Message> + Unpin,
|
||||||
@@ -205,7 +248,56 @@ pub fn websocket_url(server_url: &str) -> Result<url::Url> {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::{next_backoff_ms, websocket_url};
|
use std::collections::VecDeque;
|
||||||
|
use std::io;
|
||||||
|
use std::pin::Pin;
|
||||||
|
use std::task::{Context, Poll};
|
||||||
|
|
||||||
|
use futures_util::Sink;
|
||||||
|
use serde_json::Value;
|
||||||
|
use tokio_tungstenite::tungstenite::Message;
|
||||||
|
|
||||||
|
use super::{next_backoff_ms, send_command_result, websocket_url};
|
||||||
|
use crate::protocol::{CommandResult, RequestId};
|
||||||
|
|
||||||
|
struct RecordingSink {
|
||||||
|
fail_sends_remaining: usize,
|
||||||
|
sent: VecDeque<Message>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl RecordingSink {
|
||||||
|
fn new(fail_sends_remaining: usize) -> Self {
|
||||||
|
Self {
|
||||||
|
fail_sends_remaining,
|
||||||
|
sent: VecDeque::new(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Sink<Message> for RecordingSink {
|
||||||
|
type Error = io::Error;
|
||||||
|
|
||||||
|
fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn start_send(mut self: Pin<&mut Self>, item: Message) -> Result<(), Self::Error> {
|
||||||
|
if self.fail_sends_remaining > 0 {
|
||||||
|
self.fail_sends_remaining -= 1;
|
||||||
|
return Err(io::Error::other("injected send failure"));
|
||||||
|
}
|
||||||
|
self.sent.push_back(item);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
|
Poll::Ready(Ok(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn websocket_url_uses_expected_path() {
|
fn websocket_url_uses_expected_path() {
|
||||||
@@ -224,4 +316,49 @@ mod tests {
|
|||||||
fn backoff_never_shrinks_when_max_is_lower() {
|
fn backoff_never_shrinks_when_max_is_lower() {
|
||||||
assert_eq!(next_backoff_ms(8_000, 1_000), 8_000);
|
assert_eq!(next_backoff_ms(8_000, 1_000), 8_000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn send_command_result_emits_result_on_success() {
|
||||||
|
let mut sink = RecordingSink::new(0);
|
||||||
|
let request_id = RequestId::try_from("req-success".to_string()).expect("request id");
|
||||||
|
|
||||||
|
send_command_result(
|
||||||
|
&mut sink,
|
||||||
|
request_id,
|
||||||
|
"wake",
|
||||||
|
CommandResult::Wake(wakey_core::WakeResult { result: vec![] }),
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("send should succeed");
|
||||||
|
|
||||||
|
assert_eq!(sink.sent.len(), 1);
|
||||||
|
let Some(Message::Text(payload)) = sink.sent.pop_front() else {
|
||||||
|
panic!("expected text websocket message");
|
||||||
|
};
|
||||||
|
let v: Value = serde_json::from_str(payload.as_ref()).expect("json");
|
||||||
|
assert_eq!(v["type"], "result");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn send_command_result_falls_back_to_error_frame() {
|
||||||
|
let mut sink = RecordingSink::new(1);
|
||||||
|
let request_id = RequestId::try_from("req-fallback".to_string()).expect("request id");
|
||||||
|
|
||||||
|
send_command_result(
|
||||||
|
&mut sink,
|
||||||
|
request_id,
|
||||||
|
"devs",
|
||||||
|
CommandResult::Devs { rows: vec![] },
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.expect("fallback error frame should be sent");
|
||||||
|
|
||||||
|
assert_eq!(sink.sent.len(), 1);
|
||||||
|
let Some(Message::Text(payload)) = sink.sent.pop_front() else {
|
||||||
|
panic!("expected text websocket message");
|
||||||
|
};
|
||||||
|
let v: Value = serde_json::from_str(payload.as_ref()).expect("json");
|
||||||
|
assert_eq!(v["type"], "error");
|
||||||
|
assert_eq!(v["error"]["code"], "command_result_serialize_failed");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user