use std::time::Duration;
use reqwest::blocking::Client;
use serde::{Deserialize, Serialize};
use tracing::debug;
use crate::config::TlsConfig;
use crate::error::{Error, ReqwestResultExt, Result, ResultExt};
#[derive(Debug, Serialize)]
pub struct RegisterRequest {
pub host: String,
pub port: u16,
pub language: String,
pub name: String,
#[serde(rename = "workspaceRoot")]
pub workspace_root: String,
pub hostname: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub pid: Option<u32>,
#[serde(skip_serializing_if = "Option::is_none")]
pub token: Option<String>,
#[serde(rename = "safeMode")]
#[serde(skip_serializing_if = "std::ops::Not::not")]
pub safe_mode: bool,
}
#[derive(Debug, Deserialize)]
#[serde(rename_all = "camelCase")]
struct RegisterResponse {
connection_id: String,
#[allow(dead_code)]
status: Option<String>,
}
pub struct DaemonClient {
client: Client,
}
impl DaemonClient {
pub fn new(tls_config: Option<&TlsConfig>) -> Result<Self> {
let mut builder = Client::builder().timeout(Duration::from_secs(30));
if let Some(tls) = tls_config {
tls.validate()?;
if !tls.verify {
builder = builder.danger_accept_invalid_certs(true);
}
if let Some(ref ca_path) = tls.ca_bundle {
let cert_bytes = std::fs::read(ca_path).config("failed to read CA bundle")?;
let cert = reqwest::Certificate::from_pem(&cert_bytes)
.config("failed to parse CA bundle")?;
builder = builder.add_root_certificate(cert);
}
}
Ok(Self {
client: builder.build().config("failed to create HTTP client")?,
})
}
pub fn health_check(&self, daemon_url: &str, timeout: Duration) -> Result<()> {
let url = format!("{}/health", daemon_url);
let response = self
.client
.get(&url)
.timeout(timeout)
.send()
.daemon_unreachable(daemon_url)?;
if !response.status().is_success() {
return Err(Error::DaemonUnreachable {
url: daemon_url.to_string(),
message: format!("health check failed: status {}", response.status()),
});
}
debug!("Daemon health check passed: {}", daemon_url);
Ok(())
}
pub fn register(
&self,
daemon_url: &str,
request: RegisterRequest,
timeout: Duration,
) -> Result<String> {
let url = format!("{}/api/v1/connections", daemon_url);
debug!(
"Registering connection {} at {}:{}",
request.name, request.host, request.port
);
let response = self
.client
.post(&url)
.timeout(timeout)
.json(&request)
.send()
.registration_context()?;
if !response.status().is_success() {
let status = response.status();
let body = response.text().unwrap_or_default();
return Err(Error::RegistrationFailed(format!(
"status {}: {}",
status, body
)));
}
let reg_response: RegisterResponse =
response.json().registration("failed to parse response")?;
debug!(
"Registered with connection ID: {}",
reg_response.connection_id
);
Ok(reg_response.connection_id)
}
pub fn unregister(&self, daemon_url: &str, connection_id: &str, timeout: Duration) {
let url = format!("{}/api/v1/connections/{}", daemon_url, connection_id);
debug!("Unregistering connection: {}", connection_id);
let result = self.client.delete(&url).timeout(timeout).send();
match result {
Ok(response) => {
let status = response.status();
if status.is_success() || status.as_u16() == 404 {
debug!("Unregistered connection: {}", connection_id);
} else {
tracing::warn!(
"Failed to unregister connection {}: status {}",
connection_id,
status
);
}
}
Err(e) => {
tracing::warn!("Failed to unregister connection {}: {}", connection_id, e);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_register_request_serialization() {
let request = RegisterRequest {
host: "127.0.0.1".to_string(),
port: 5678,
language: "rust".to_string(),
name: "test-client".to_string(),
workspace_root: "/workspace".to_string(),
hostname: "test-host".to_string(),
pid: Some(12345),
token: Some("secret".to_string()),
safe_mode: true,
};
let json = serde_json::to_string(&request).unwrap();
assert!(json.contains("\"host\":\"127.0.0.1\""));
assert!(json.contains("\"port\":5678"));
assert!(json.contains("\"language\":\"rust\""));
assert!(json.contains("\"workspaceRoot\":\"/workspace\""));
assert!(json.contains("\"hostname\":\"test-host\""));
assert!(json.contains("\"pid\":12345"));
assert!(json.contains("\"safeMode\":true"));
}
#[test]
fn test_register_request_without_optionals() {
let request = RegisterRequest {
host: "127.0.0.1".to_string(),
port: 5678,
language: "rust".to_string(),
name: "test-client".to_string(),
workspace_root: "/workspace".to_string(),
hostname: "test-host".to_string(),
pid: None,
token: None,
safe_mode: false,
};
let json = serde_json::to_string(&request).unwrap();
assert!(!json.contains("token"));
assert!(!json.contains("pid"));
assert!(!json.contains("safeMode"));
assert!(json.contains("\"workspaceRoot\":\"/workspace\""));
assert!(json.contains("\"hostname\":\"test-host\""));
}
}