theater_cli/client/
connection.rs

1
2use bytes::Bytes;
3use futures::sink::SinkExt;
4use futures::stream::StreamExt;
5use std::net::SocketAddr;
6use tokio::net::TcpStream;
7use tokio::time::timeout;
8use tokio_util::codec::Framed;
9use tracing::{debug, error, info, warn};
10
11use theater_server::FragmentingCodec;
12
13use crate::config::Config;
14use crate::error::{CliError, CliResult};
15
16pub use theater_server::{ManagementCommand, ManagementResponse};
17
18/// A connection to the Theater server with automatic reconnection
19#[derive(Debug)]
20pub struct Connection {
21    address: SocketAddr,
22    config: Config,
23    framed: Option<Framed<TcpStream, FragmentingCodec>>,
24    last_error: Option<String>,
25}
26
27impl Connection {
28    pub fn new(address: SocketAddr, config: Config) -> Self {
29        Self {
30            address,
31            config,
32            framed: None,
33            last_error: None,
34        }
35    }
36
37    /// Ensure we have an active connection, reconnecting if necessary
38    pub async fn ensure_connected(&mut self) -> CliResult<()> {
39        if self.framed.is_none() {
40            self.connect().await?;
41        }
42
43        // Test the connection by sending a ping-like command
44        // If it fails, try to reconnect once
45        if !self.test_connection().await {
46            info!("Connection test failed, attempting to reconnect");
47            self.framed = None;
48            self.connect().await?;
49        }
50
51        Ok(())
52    }
53
54    /// Establish a new connection to the server
55    async fn connect(&mut self) -> CliResult<()> {
56        info!("Connecting to Theater server at {}", self.address);
57
58        let connect_future = TcpStream::connect(self.address);
59        let socket = timeout(self.config.server.timeout, connect_future)
60            .await
61            .map_err(|_| CliError::ConnectionTimeout {
62                timeout: self.config.server.timeout.as_secs(),
63            })?
64            .map_err(|e| CliError::connection_failed(self.address, e))?;
65
66        // Use the FragmentingCodec for transparent message chunking
67        let codec = FragmentingCodec::new();
68        self.framed = Some(Framed::new(socket, codec));
69        self.last_error = None;
70
71        info!("Successfully connected to Theater server with fragmentation support");
72        Ok(())
73    }
74
75    /// Test if the current connection is working
76    async fn test_connection(&mut self) -> bool {
77        if let Some(ref mut framed) = self.framed {
78            // Try a simple ping by checking if we can write to the socket
79            // In a real implementation, you'd send a proper ping command
80            match framed.get_ref().peer_addr() {
81                Ok(_) => true,
82                Err(_) => false,
83            }
84        } else {
85            false
86        }
87    }
88
89    /// Send a command and wait for a response
90    pub async fn send_command(
91        &mut self,
92        command: ManagementCommand,
93    ) -> CliResult<ManagementResponse> {
94        self.ensure_connected().await?;
95
96        let framed = self.framed.as_mut().unwrap();
97
98        // Serialize and send the command
99        debug!("Sending command: {:?}", command);
100        let command_bytes = serde_json::to_vec(&command).map_err(CliError::Serialization)?;
101
102        // Send with timeout - FragmentingCodec will handle chunking if needed - FragmentingCodec will handle chunking if needed
103        let send_future = framed.send(Bytes::from(command_bytes));
104        timeout(self.config.server.timeout, send_future)
105            .await
106            .map_err(|_| CliError::ConnectionTimeout {
107                timeout: self.config.server.timeout.as_secs(),
108            })?
109            .map_err(|e| {
110                error!("Failed to send command: {}", e);
111                CliError::ConnectionLost
112            })?;
113
114        debug!("Command sent, waiting for response");
115
116        // Receive response with timeout - FragmentingCodec will handle reassembly if needed
117        let receive_future = framed.next();
118        let response_bytes = timeout(self.config.server.timeout, receive_future)
119            .await
120            .map_err(|_| CliError::ConnectionTimeout {
121                timeout: self.config.server.timeout.as_secs(),
122            })?;
123
124        match response_bytes {
125            Some(Ok(bytes)) => {
126                let response: ManagementResponse =
127                    serde_json::from_slice(&bytes).map_err(|e| CliError::ProtocolError {
128                        reason: format!("Failed to deserialize response: {}", e),
129                    })?;
130                debug!("Received response: {:?}", response);
131                Ok(response)
132            }
133            Some(Err(e)) => {
134                error!("Error receiving response: {}", e);
135                self.framed = None; // Mark connection as broken
136                Err(CliError::ConnectionLost)
137            }
138            None => {
139                warn!("Connection closed by server");
140                self.framed = None;
141                Err(CliError::ConnectionLost)
142            }
143        }
144    }
145
146    /// Send a command without waiting for a response
147    pub async fn send_command_no_response(&mut self, command: ManagementCommand) -> CliResult<()> {
148        self.ensure_connected().await?;
149
150        let framed = self.framed.as_mut().unwrap();
151
152        // Serialize and send the command
153        debug!("Sending command (no response expected): {:?}", command);
154        let command_bytes = serde_json::to_vec(&command).map_err(CliError::Serialization)?;
155
156        // Send with timeout
157        let send_future = framed.send(Bytes::from(command_bytes));
158        timeout(self.config.server.timeout, send_future)
159            .await
160            .map_err(|_| CliError::ConnectionTimeout {
161                timeout: self.config.server.timeout.as_secs(),
162            })?
163            .map_err(|e| {
164                error!("Failed to send command: {}", e);
165                CliError::ConnectionLost
166            })?;
167
168        debug!("Command sent (no response expected)");
169        Ok(())
170    }
171
172    /// Get the next response from the connection (for streaming operations)
173    pub async fn next_response(&mut self) -> CliResult<Option<ManagementResponse>> {
174        if let Some(ref mut framed) = self.framed {
175            let receive_future = framed.next();
176            let response_bytes = timeout(self.config.server.timeout, receive_future)
177                .await
178                .map_err(|_| CliError::ConnectionTimeout {
179                    timeout: self.config.server.timeout.as_secs(),
180                })?;
181
182            match response_bytes {
183                Some(Ok(bytes)) => {
184                    let response: ManagementResponse =
185                        serde_json::from_slice(&bytes).map_err(|e| CliError::ProtocolError {
186                            reason: format!("Failed to deserialize response: {}", e),
187                        })?;
188                    debug!("Received streaming response: {:?}", response);
189                    Ok(Some(response))
190                }
191                Some(Err(e)) => {
192                    error!("Error receiving streaming response: {}", e);
193                    self.framed = None;
194                    Err(CliError::ConnectionLost)
195                }
196                None => {
197                    debug!("Stream ended");
198                    Ok(None)
199                }
200            }
201        } else {
202            Err(CliError::ConnectionLost)
203        }
204    }
205
206    /// Check if the connection is currently active
207    pub fn is_connected(&self) -> bool {
208        self.framed.is_some()
209    }
210
211    /// Get the server address
212    pub fn address(&self) -> SocketAddr {
213        self.address
214    }
215
216    /// Get the last connection error, if any
217    pub fn last_error(&self) -> Option<&str> {
218        self.last_error.as_deref()
219    }
220
221    /// Close the connection
222    pub async fn close(&mut self) {
223        if let Some(mut framed) = self.framed.take() {
224            let _ = framed.close().await;
225            info!("Connection closed");
226        }
227    }
228}
229
230impl Drop for Connection {
231    fn drop(&mut self) {
232        if self.framed.is_some() {
233            debug!("Connection dropped without explicit close");
234        }
235    }
236}
237
238#[cfg(test)]
239mod tests {
240    use super::*;
241
242    #[test]
243    fn test_connection_creation() {
244        let config = Config::default();
245        let addr = "127.0.0.1:9000".parse().unwrap();
246        let conn = Connection::new(addr, config);
247
248        assert_eq!(conn.address(), addr);
249        assert!(!conn.is_connected());
250        assert!(conn.last_error().is_none());
251    }
252}