Skip to main content

theater_client/
tcp.rs

1//! # TCP Connection for Theater
2//!
3//! Provides a low-level TCP connection to a Theater server with simple
4//! send and receive methods that can be used with tokio::select!
5
6use anyhow::{anyhow, Result};
7use bytes::Bytes;
8use futures::sink::SinkExt;
9use futures::stream::StreamExt;
10use std::net::SocketAddr;
11use tokio::net::TcpStream;
12use tokio_util::codec::Framed;
13use tracing::{debug, error, info};
14
15use theater_server::{FragmentingCodec, ManagementCommand, ManagementResponse};
16
17/// A client connection to a Theater server
18///
19/// This provides a thin wrapper around the TCP connection with simple
20/// send and receive methods that can be used with tokio::select!
21#[derive(Debug)]
22pub struct TheaterConnection {
23    /// Server address
24    pub address: SocketAddr,
25    /// The TCP connection
26    connection: Option<Framed<TcpStream, FragmentingCodec>>,
27}
28
29impl TheaterConnection {
30    /// Create a new TheaterConnection
31    ///
32    /// # Arguments
33    ///
34    /// * `address` - The address of the Theater server
35    ///
36    /// # Returns
37    ///
38    /// A new TheaterConnection instance (not yet connected)
39    pub fn new(address: SocketAddr) -> Self {
40        Self {
41            address,
42            connection: None,
43        }
44    }
45
46    /// Connect to the Theater server
47    ///
48    /// # Returns
49    ///
50    /// * `Ok(())` if the connection was successful
51    /// * `Err(...)` if there was an error connecting
52    pub async fn connect(&mut self) -> Result<()> {
53        if self.connection.is_some() {
54            return Ok(());
55        }
56
57        info!("Connecting to Theater server at {}", self.address);
58        let socket = TcpStream::connect(self.address).await?;
59
60        let codec = FragmentingCodec::new();
61        let framed = Framed::new(socket, codec);
62
63        self.connection = Some(framed);
64        info!("Connected to Theater server");
65
66        Ok(())
67    }
68
69    /// Send a command to the server
70    ///
71    /// # Arguments
72    ///
73    /// * `command` - The command to send
74    ///
75    /// # Returns
76    ///
77    /// * `Ok(())` if the command was sent successfully
78    /// * `Err(...)` if there was an error sending the command
79    pub async fn send(&mut self, command: ManagementCommand) -> Result<()> {
80        // Ensure we're connected
81        if self.connection.is_none() {
82            self.connect().await?;
83        }
84
85        // Serialize and send the command
86        debug!("Sending command: {:?}", command);
87        let command_bytes = serde_json::to_vec(&command)?;
88
89        let connection = self
90            .connection
91            .as_mut()
92            .ok_or_else(|| anyhow!("Connection lost"))?;
93
94        connection.send(Bytes::from(command_bytes)).await?;
95        debug!("Command sent");
96
97        Ok(())
98    }
99
100    /// Receive a response from the server
101    ///
102    /// This method will wait for the next response from the server.
103    /// It can be used with tokio::select! to handle multiple operations.
104    ///
105    /// # Returns
106    ///
107    /// * `Ok(response)` if a response was received
108    /// * `Err(...)` if there was an error receiving the response
109    pub async fn receive(&mut self) -> Result<ManagementResponse> {
110        // Ensure we're connected
111        if self.connection.is_none() {
112            return Err(anyhow!("Not connected"));
113        }
114
115        let connection = self
116            .connection
117            .as_mut()
118            .ok_or_else(|| anyhow!("Connection lost"))?;
119
120        // Wait for the next message
121        match connection.next().await {
122            Some(Ok(bytes)) => {
123                // Deserialize the response
124                let response: ManagementResponse = serde_json::from_slice(&bytes)?;
125                debug!("Received response: {:?}", response);
126                Ok(response)
127            }
128            Some(Err(e)) => {
129                error!("Error receiving response: {}", e);
130                self.connection = None;
131                Err(anyhow!("Connection error: {}", e))
132            }
133            None => {
134                // Connection closed
135                debug!("Connection closed by server");
136                self.connection = None;
137                Err(anyhow!("Connection closed by server"))
138            }
139        }
140    }
141
142    pub async fn send_and_receive(
143        &mut self,
144        command: ManagementCommand,
145    ) -> Result<ManagementResponse> {
146        // Send the command
147        self.send(command).await?;
148
149        // Receive the response
150        self.receive().await
151    }
152
153    /// Check if the connection is active
154    ///
155    /// # Returns
156    ///
157    /// * `true` if the connection is active
158    /// * `false` if the connection is not active
159    pub fn is_connected(&self) -> bool {
160        self.connection.is_some()
161    }
162
163    /// Close the connection
164    ///
165    /// # Returns
166    ///
167    /// * `Ok(())` if the connection was closed successfully
168    /// * `Err(...)` if there was an error closing the connection
169    pub async fn close(&mut self) -> Result<()> {
170        if let Some(mut connection) = self.connection.take() {
171            connection.close().await?;
172        }
173        Ok(())
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180    use std::time::Duration;
181    use tokio::net::TcpListener;
182    use tokio::sync::oneshot;
183
184    // Helper to run a mock server for testing
185    async fn run_mock_server(addr: SocketAddr, shutdown_rx: oneshot::Receiver<()>) -> Result<()> {
186        let listener = TcpListener::bind(addr).await?;
187        let shutdown_future = shutdown_rx;
188
189        tokio::select! {
190            _ = async {
191                while let Ok((socket, _)) = listener.accept().await {
192                    let mut framed = Framed::new(socket, FragmentingCodec::new());
193
194                    // Echo back whatever we receive
195                    while let Some(Ok(bytes)) = framed.next().await {
196                        framed.send(bytes.into()).await?;
197                    }
198                }
199                Ok::<(), anyhow::Error>(())
200            } => {},
201            _ = shutdown_future => {},
202        }
203
204        Ok(())
205    }
206
207    #[tokio::test]
208    async fn test_connection() -> Result<()> {
209        // Bind to a random port
210        let listener = TcpListener::bind("127.0.0.1:0").await?;
211        let addr = listener.local_addr()?;
212        drop(listener);
213
214        // Start mock server
215        let (shutdown_tx, shutdown_rx) = oneshot::channel();
216        let server_handle = tokio::spawn(run_mock_server(addr, shutdown_rx));
217
218        // Allow server to start
219        tokio::time::sleep(Duration::from_millis(100)).await;
220
221        // Create client and connect
222        let mut client = TheaterConnection::new(addr);
223        client.connect().await?;
224        assert!(client.is_connected());
225
226        // Clean up
227        client.close().await?;
228        let _ = shutdown_tx.send(());
229        let _ = server_handle.await;
230
231        Ok(())
232    }
233}