Skip to main content

aimdb_client/
discovery.rs

1//! AimDB Instance Discovery
2//!
3//! Scans known directories for running AimDB instances.
4
5use crate::connection::AimxClient;
6use crate::error::{ClientError, ClientResult};
7use crate::protocol::WelcomeMessage;
8use std::path::PathBuf;
9use std::time::Duration;
10
11/// Known directories where AimDB sockets might be located
12const SOCKET_SEARCH_DIRS: &[&str] = &["/tmp", "/var/run/aimdb"];
13
14/// Information about a discovered AimDB instance
15#[derive(Debug, Clone)]
16pub struct InstanceInfo {
17    pub socket_path: PathBuf,
18    pub server_version: String,
19    pub protocol_version: String,
20    pub permissions: Vec<String>,
21    pub writable_records: Vec<String>,
22    pub max_subscriptions: Option<usize>,
23    pub authenticated: bool,
24}
25
26impl From<(PathBuf, WelcomeMessage)> for InstanceInfo {
27    fn from((socket_path, welcome): (PathBuf, WelcomeMessage)) -> Self {
28        Self {
29            socket_path,
30            server_version: welcome.server,
31            protocol_version: welcome.version,
32            permissions: welcome.permissions,
33            writable_records: welcome.writable_records,
34            max_subscriptions: welcome.max_subscriptions,
35            authenticated: welcome.authenticated.unwrap_or(false),
36        }
37    }
38}
39
40/// Discover all running AimDB instances
41pub async fn discover_instances() -> ClientResult<Vec<InstanceInfo>> {
42    let mut instances = Vec::new();
43
44    for dir_path in SOCKET_SEARCH_DIRS {
45        if let Ok(entries) = tokio::fs::read_dir(dir_path).await {
46            instances.extend(scan_directory(entries).await);
47        }
48    }
49
50    if instances.is_empty() {
51        return Err(ClientError::NoInstancesFound);
52    }
53
54    Ok(instances)
55}
56
57/// Scan a directory for AimDB socket files
58async fn scan_directory(mut entries: tokio::fs::ReadDir) -> Vec<InstanceInfo> {
59    let mut instances = Vec::new();
60
61    while let Ok(Some(entry)) = entries.next_entry().await {
62        let path = entry.path();
63
64        // Check if it's a socket file (ends with .sock)
65        if path.extension().and_then(|s| s.to_str()) == Some("sock") {
66            // Try to connect and get instance info
67            if let Ok(info) = probe_instance(&path).await {
68                instances.push(info);
69            }
70        }
71    }
72
73    instances
74}
75
76/// Try to connect to a socket and get instance information
77async fn probe_instance(socket_path: &PathBuf) -> ClientResult<InstanceInfo> {
78    // Try to connect with a short timeout
79    let connect_timeout = Duration::from_millis(500);
80
81    let client = tokio::time::timeout(connect_timeout, AimxClient::connect(socket_path))
82        .await
83        .map_err(|_| {
84            ClientError::connection_failed(
85                socket_path.display().to_string(),
86                "timeout during discovery probe",
87            )
88        })??;
89
90    let welcome = client.server_info().clone();
91
92    Ok(InstanceInfo::from((socket_path.clone(), welcome)))
93}
94
95/// Find a specific instance by socket path or name
96pub async fn find_instance(socket_hint: Option<&str>) -> ClientResult<InstanceInfo> {
97    // If socket path provided, try that directly
98    if let Some(socket_path) = socket_hint {
99        let path = PathBuf::from(socket_path);
100        if path.exists() {
101            return probe_instance(&path).await;
102        } else {
103            return Err(ClientError::connection_failed(
104                socket_path.to_string(),
105                "socket file does not exist",
106            ));
107        }
108    }
109
110    // Otherwise, discover all and return the first one
111    let instances = discover_instances().await?;
112
113    instances
114        .into_iter()
115        .next()
116        .ok_or(ClientError::NoInstancesFound)
117}