taiga_plugin_api/daemon/
client.rs

1//! Generic client helper for daemon-based plugins
2//!
3//! Provides a retry-with-autospawn pattern for connecting to daemon processes.
4
5use super::ipc::{receive_message, send_message, spawn_daemon_process, DaemonSpawnConfig};
6use super::socket;
7use crate::PluginError;
8use serde::{Deserialize, Serialize};
9use std::time::Duration;
10
11/// Configuration for the daemon client
12#[derive(Debug, Clone)]
13pub struct DaemonClientConfig {
14    /// Path to the socket file
15    pub socket_path: String,
16    /// Configuration for spawning the daemon
17    pub daemon_spawn: DaemonSpawnConfig,
18    /// Time to wait after starting daemon before retrying connection (milliseconds)
19    pub startup_wait_ms: u64,
20    /// Buffer size for IPC messages
21    pub buffer_size: usize,
22}
23
24impl DaemonClientConfig {
25    /// Create a new daemon client configuration
26    pub fn new(
27        socket_path: impl Into<String>,
28        daemon_spawn: DaemonSpawnConfig,
29    ) -> Self {
30        Self {
31            socket_path: socket_path.into(),
32            daemon_spawn,
33            startup_wait_ms: 500,
34            buffer_size: 1024,
35        }
36    }
37
38    /// Set the startup wait time
39    pub fn with_startup_wait(mut self, wait_ms: u64) -> Self {
40        self.startup_wait_ms = wait_ms;
41        self
42    }
43
44    /// Set the buffer size
45    pub fn with_buffer_size(mut self, size: usize) -> Self {
46        self.buffer_size = size;
47        self
48    }
49}
50
51/// Send a command to the daemon with automatic spawning if not running
52///
53/// This implements the retry-with-autospawn pattern:
54/// 1. Try to connect to the daemon
55/// 2. If connection fails, spawn the daemon
56/// 3. Wait for startup, then retry connection
57/// 4. Send command and receive response
58///
59/// # Type Parameters
60/// * `Cmd` - The command type (must be serializable)
61/// * `Resp` - The response type (must be deserializable)
62///
63/// # Arguments
64/// * `config` - Configuration for the daemon client
65/// * `command` - The command to send
66///
67/// # Returns
68/// The response from the daemon, or an error if communication fails
69pub async fn send_command_with_autospawn<Cmd, Resp>(
70    config: &DaemonClientConfig,
71    command: &Cmd,
72) -> Result<Resp, PluginError>
73where
74    Cmd: Serialize,
75    Resp: for<'de> Deserialize<'de>,
76{
77    let stream_result = socket::connect(&config.socket_path).await;
78
79    let mut stream = match stream_result {
80        Ok(s) => s,
81        Err(conn_err) => {
82            // Daemon not running, try to start it
83            println!("Daemon not running. Starting it...");
84            spawn_daemon_process(&config.daemon_spawn)
85                .map_err(PluginError::daemon_not_running_with_source)?;
86
87            // Wait for daemon to start
88            tokio::time::sleep(Duration::from_millis(config.startup_wait_ms)).await;
89
90            // Retry connection
91            socket::connect(&config.socket_path).await.map_err(|_| {
92                // Daemon was started but we still can't connect - include original error
93                PluginError::ipc_connection_with_source(
94                    "Failed to connect after starting daemon",
95                    conn_err,
96                )
97            })?
98        }
99    };
100
101    // Send command and receive response
102    send_message(&mut stream, command).await?;
103    let resp: Resp = receive_message(&mut stream, config.buffer_size).await?;
104
105    Ok(resp)
106}
107
108#[cfg(test)]
109mod tests {
110    use super::*;
111
112    #[test]
113    fn test_daemon_client_config_creation() {
114        let spawn_config = DaemonSpawnConfig::new("test", "daemon");
115        let client_config = DaemonClientConfig::new("/tmp/test.sock", spawn_config);
116        assert_eq!(client_config.socket_path, "/tmp/test.sock");
117        assert_eq!(client_config.startup_wait_ms, 500);
118        assert_eq!(client_config.buffer_size, 1024);
119    }
120
121    #[test]
122    fn test_daemon_client_config_with_options() {
123        let spawn_config = DaemonSpawnConfig::new("test", "daemon");
124        let client_config = DaemonClientConfig::new("/tmp/test.sock", spawn_config)
125            .with_startup_wait(1000)
126            .with_buffer_size(2048);
127        assert_eq!(client_config.startup_wait_ms, 1000);
128        assert_eq!(client_config.buffer_size, 2048);
129    }
130}