use std::sync::{Mutex, OnceLock};
use std::time::Instant;
use anyhow::{anyhow, Result};
use night_fury_daemon_core::cli::make_req;
use night_fury_daemon_core::{client, protocol::Response};
use serde::{Deserialize, Serialize};
use serde_json::json;
static DAEMON_START: OnceLock<Mutex<Instant>> = OnceLock::new();
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct HandshakeReply {
pub version: String,
pub uptime_secs: u64,
}
#[derive(Debug, thiserror::Error)]
pub enum HandshakeError {
#[error(
"daemon version mismatch: daemon={daemon}, CLI={cli}; run `tfd daemon stop` then retry"
)]
VersionMismatch { daemon: String, cli: String },
#[error("daemon handshake protocol error: {0}")]
Protocol(String),
}
pub fn mark_daemon_started() {
let start = DAEMON_START.get_or_init(|| Mutex::new(Instant::now()));
*start.lock().expect("daemon start time mutex poisoned") = Instant::now();
}
pub fn daemon_start_time() -> Instant {
*DAEMON_START
.get_or_init(|| Mutex::new(Instant::now()))
.lock()
.expect("daemon start time mutex poisoned")
}
pub fn handshake_response(id: &str) -> Response {
Response::ok(
id,
json!({
"version": env!("CARGO_PKG_VERSION"),
"uptime_secs": daemon_start_time().elapsed().as_secs(),
}),
)
}
pub async fn handshake(socket: &str) -> Result<HandshakeReply> {
let req = make_req("daemon.handshake", None, json!({}));
let resp = client::send(socket, &req).await?;
if !resp.ok {
return Err(anyhow!(
"daemon handshake failed: {}",
resp.error.unwrap_or_default()
));
}
let data = resp
.data
.ok_or_else(|| anyhow!("daemon handshake missing data"))?;
serde_json::from_value(data).map_err(Into::into)
}
pub async fn verify_compatible(socket: &str) -> Result<HandshakeReply> {
let cli_version = env!("CARGO_PKG_VERSION");
let req = make_req("daemon.handshake", None, json!({}));
let resp = client::send(socket, &req).await?;
if !resp.ok {
return Err(version_mismatch("pre-handshake"));
}
let data = resp
.data
.ok_or_else(|| HandshakeError::Protocol("missing handshake data".to_string()))?;
let reply: HandshakeReply = serde_json::from_value(data)
.map_err(|e| HandshakeError::Protocol(format!("invalid handshake data: {e}")))?;
if reply.version != cli_version {
return Err(version_mismatch(&reply.version));
}
Ok(reply)
}
fn version_mismatch(daemon_version: &str) -> anyhow::Error {
HandshakeError::VersionMismatch {
daemon: daemon_version.to_string(),
cli: env!("CARGO_PKG_VERSION").to_string(),
}
.into()
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn handshake_response_contains_version_and_uptime() {
mark_daemon_started();
let resp = handshake_response("h1");
assert!(resp.ok);
let data = resp.data.unwrap();
assert_eq!(data["version"], env!("CARGO_PKG_VERSION"));
assert!(data["uptime_secs"].as_u64().is_some());
}
}