use std::io::{BufRead, BufReader, Write};
use std::os::unix::net::UnixStream;
use std::time::Duration;
use anyhow::Result;
use serde::{Deserialize, Serialize};
use crate::catalog::{RecordingDetail, RecordingRow, ReindexStats, SkillRow};
use crate::record::Recording;
use crate::span::Event;
const NOTIFY_TIMEOUT: Duration = Duration::from_millis(50);
const QUERY_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Request {
Ping,
EventAppended { rec_id: String, event: Event },
RecordingClosed { recording: Recording },
SkillInstalled {
skill_name: String,
rec_id: String,
skill_path: String,
#[serde(default = "default_skill_status")]
status: String,
},
ListRecordings,
ShowRecording { id: String },
ListSkills,
Reindex,
Shutdown,
}
fn default_skill_status() -> String {
crate::catalog::STATUS_UNKNOWN.to_string()
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(tag = "type")]
pub enum Response {
Pong,
Ack,
Recordings { recordings: Vec<RecordingRow> },
Recording { recording: Option<RecordingDetail> },
Skills { skills: Vec<SkillRow> },
Reindexed { stats: ReindexStats },
Error { message: String },
}
pub fn notify_best_effort(req: &Request) {
let _ = send_notify(req);
}
fn send_notify(req: &Request) -> Result<()> {
let path = crate::paths::socket_path()?;
let mut stream = UnixStream::connect(path)?;
stream.set_write_timeout(Some(NOTIFY_TIMEOUT))?;
let mut line = serde_json::to_vec(req)?;
line.push(b'\n');
stream.write_all(&line)?;
stream.flush()?;
Ok(())
}
pub fn query(req: &Request) -> Result<Response> {
let path = crate::paths::socket_path()?;
let stream = UnixStream::connect(path)?;
stream.set_read_timeout(Some(QUERY_TIMEOUT))?;
stream.set_write_timeout(Some(QUERY_TIMEOUT))?;
let mut writer = stream.try_clone()?;
let mut line = serde_json::to_string(req)?;
line.push('\n');
writer.write_all(line.as_bytes())?;
writer.flush()?;
let mut reader = BufReader::new(stream);
let mut resp_line = String::new();
reader.read_line(&mut resp_line)?;
let resp: Response = serde_json::from_str(resp_line.trim())?;
Ok(resp)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn request_roundtrips_through_ndjson() {
let req = Request::SkillInstalled {
skill_name: "galdr-demo".into(),
rec_id: "01ABC".into(),
skill_path: "/x/SKILL.md".into(),
status: crate::catalog::STATUS_FINAL.into(),
};
let line = serde_json::to_string(&req).unwrap();
assert!(line.contains("\"type\":\"SkillInstalled\""));
let back: Request = serde_json::from_str(&line).unwrap();
match back {
Request::SkillInstalled { skill_name, .. } => assert_eq!(skill_name, "galdr-demo"),
_ => panic!("wrong variant"),
}
}
#[test]
fn response_roundtrips_through_ndjson() {
let resp = Response::Error {
message: "nope".into(),
};
let line = serde_json::to_string(&resp).unwrap();
let back: Response = serde_json::from_str(&line).unwrap();
assert!(matches!(back, Response::Error { .. }));
}
#[test]
fn notify_is_a_noop_without_a_daemon() {
notify_best_effort(&Request::Ping);
}
}