theater_cli/client/
cli_wrapper.rs

1// CLI wrapper around theater-client with timeout and retry logic
2
3use std::net::SocketAddr;
4use theater_client::TheaterConnection;
5use theater_server::{ManagementCommand, ManagementResponse};
6
7use crate::config::Config;
8use crate::error::{CliError, CliResult};
9
10/// CLI wrapper around theater-client with timeout and retry logic
11pub struct CliTheaterClient {
12    connection: TheaterConnection,
13    config: Config,
14    address: SocketAddr,
15}
16
17impl CliTheaterClient {
18    /// Create a new CLI client
19    pub fn new(address: SocketAddr, config: Config) -> Self {
20        Self {
21            connection: TheaterConnection::new(address),
22            config,
23            address,
24        }
25    }
26
27    /// Send a command and receive a response with CLI timeout and error handling
28    pub async fn send_command(
29        &mut self,
30        command: ManagementCommand,
31    ) -> CliResult<ManagementResponse> {
32        // Connect with timeout
33        self.ensure_connected().await?;
34
35        // Send command with timeout
36        tokio::time::timeout(self.config.server.timeout, self.connection.send(command))
37            .await
38            .map_err(|_| CliError::ConnectionTimeout {
39                timeout: self.config.server.timeout.as_secs(),
40            })?
41            .map_err(|e| CliError::connection_failed(self.address, e))?;
42
43        // Receive response with timeout
44        let response = tokio::time::timeout(self.config.server.timeout, self.connection.receive())
45            .await
46            .map_err(|_| CliError::ConnectionTimeout {
47                timeout: self.config.server.timeout.as_secs(),
48            })?
49            .map_err(|e| CliError::connection_failed(self.address, e))?;
50
51        Ok(response)
52    }
53
54    /// Send a command without waiting for response (fire and forget)
55    pub async fn send_command_no_response(&mut self, command: ManagementCommand) -> CliResult<()> {
56        // Connect with timeout
57        self.ensure_connected().await?;
58
59        // Send command with timeout
60        tokio::time::timeout(self.config.server.timeout, self.connection.send(command))
61            .await
62            .map_err(|_| CliError::ConnectionTimeout {
63                timeout: self.config.server.timeout.as_secs(),
64            })?
65            .map_err(|e| CliError::connection_failed(self.address, e))?;
66
67        Ok(())
68    }
69
70    /// Receive the next response (for streaming operations)
71    pub async fn receive(&mut self) -> CliResult<ManagementResponse> {
72        let response = tokio::time::timeout(self.config.server.timeout, self.connection.receive())
73            .await
74            .map_err(|_| CliError::ConnectionTimeout {
75                timeout: self.config.server.timeout.as_secs(),
76            })?
77            .map_err(|e| CliError::connection_failed(self.address, e))?;
78
79        Ok(response)
80    }
81
82    /// Ensure we're connected with retry logic
83    async fn ensure_connected(&mut self) -> CliResult<()> {
84        if self.connection.is_connected() {
85            return Ok(());
86        }
87
88        let mut attempts = 0;
89        let max_attempts = self.config.server.retry_attempts;
90
91        while attempts < max_attempts {
92            match tokio::time::timeout(self.config.server.timeout, self.connection.connect()).await
93            {
94                Ok(Ok(())) => return Ok(()),
95                Ok(Err(e)) => {
96                    attempts += 1;
97                    if attempts >= max_attempts {
98                        return Err(CliError::connection_failed(self.address, e));
99                    }
100                    // Wait before retry
101                    tokio::time::sleep(self.config.server.retry_delay).await;
102                }
103                Err(_) => {
104                    attempts += 1;
105                    if attempts >= max_attempts {
106                        return Err(CliError::ConnectionTimeout {
107                            timeout: self.config.server.timeout.as_secs(),
108                        });
109                    }
110                    tokio::time::sleep(self.config.server.retry_delay).await;
111                }
112            }
113        }
114
115        Err(CliError::ConnectionTimeout {
116            timeout: self.config.server.timeout.as_secs(),
117        })
118    }
119
120    /// Check if connected
121    pub fn is_connected(&self) -> bool {
122        self.connection.is_connected()
123    }
124
125    /// Get the server address
126    pub fn address(&self) -> SocketAddr {
127        self.address
128    }
129}
130
131// Helper functions for common operations
132
133/// List all actors with CLI error handling
134pub async fn list_actors(
135    address: SocketAddr,
136    config: &Config,
137) -> CliResult<Vec<(theater::id::TheaterId, String)>> {
138    let mut client = CliTheaterClient::new(address, config.clone());
139    let response = client.send_command(ManagementCommand::ListActors).await?;
140
141    match response {
142        ManagementResponse::ActorList { actors } => Ok(actors),
143        ManagementResponse::Error { error } => Err(CliError::ServerError {
144            message: format!("{:?}", error),
145        }),
146        _ => Err(CliError::UnexpectedResponse {
147            response: format!("{:?}", response),
148        }),
149    }
150}
151
152/// Stop an actor with CLI error handling
153pub async fn stop_actor(address: SocketAddr, config: &Config, actor_id: &str) -> CliResult<()> {
154    let mut client = CliTheaterClient::new(address, config.clone());
155    let theater_id = actor_id
156        .parse()
157        .map_err(|_| CliError::invalid_actor_id(actor_id))?;
158
159    let response = client
160        .send_command(ManagementCommand::StopActor { id: theater_id })
161        .await?;
162
163    match response {
164        ManagementResponse::ActorStopped { .. } => Ok(()),
165        ManagementResponse::Error { error } => {
166            let error_str = format!("{:?}", error);
167            if error_str.contains("not found") {
168                Err(CliError::actor_not_found(actor_id))
169            } else {
170                Err(CliError::ServerError { message: error_str })
171            }
172        }
173        _ => Err(CliError::UnexpectedResponse {
174            response: format!("{:?}", response),
175        }),
176    }
177}
178
179/// Get actor state with CLI error handling
180pub async fn get_actor_state(
181    address: SocketAddr,
182    config: &Config,
183    actor_id: &str,
184) -> CliResult<Option<Vec<u8>>> {
185    let mut client = CliTheaterClient::new(address, config.clone());
186    let theater_id = actor_id
187        .parse()
188        .map_err(|_| CliError::invalid_actor_id(actor_id))?;
189
190    let response = client
191        .send_command(ManagementCommand::GetActorState { id: theater_id })
192        .await?;
193
194    match response {
195        ManagementResponse::ActorState { state, .. } => Ok(state),
196        ManagementResponse::Error { error } => {
197            let error_str = format!("{:?}", error);
198            if error_str.contains("not found") {
199                Err(CliError::actor_not_found(actor_id))
200            } else {
201                Err(CliError::ServerError { message: error_str })
202            }
203        }
204        _ => Err(CliError::UnexpectedResponse {
205            response: format!("{:?}", response),
206        }),
207    }
208}