theater_cli/commands/
start.rs1use anyhow::Result;
2use clap::Parser;
3use std::net::SocketAddr;
4use tokio::sync::mpsc;
5use tracing::debug;
6
7use crate::client::ManagementResponse;
8use crate::tui;
9use crate::utils::event_display::{display_events_header, display_single_event};
10use crate::{error::CliError, output::formatters::ActorStarted, CommandContext};
11use theater::utils::resolve_reference;
12
13#[derive(Debug, Parser)]
14pub struct StartArgs {
15 #[arg(required = true)]
17 pub manifest: String,
18
19 #[arg(short, long)]
21 pub address: Option<SocketAddr>,
22
23 #[arg(short, long)]
25 pub initial_state: Option<String>,
26
27 #[arg(short, long)]
29 pub subscribe: bool,
30
31 #[arg(short, long)]
33 pub parent: bool,
34
35 #[arg(long)]
37 pub id_only: bool,
38
39 #[arg(short, long, default_value = "compact")]
41 pub format: String,
42}
43
44pub async fn execute_async(args: &StartArgs, ctx: &CommandContext) -> Result<(), CliError> {
46 debug!("Starting actor from manifest: {}", args.manifest);
47
48 let address = ctx.server_address(args.address);
50 debug!("Connecting to server at: {}", address);
51
52 let manifest_bytes = resolve_reference(&args.manifest).await.map_err(|e| {
54 CliError::invalid_manifest(format!(
55 "Failed to resolve manifest reference '{}': {}",
56 args.manifest, e
57 ))
58 })?;
59
60 let manifest_content = String::from_utf8(manifest_bytes).map_err(|e| {
62 CliError::invalid_manifest(format!("Manifest content is not valid UTF-8: {}", e))
63 })?;
64
65 let initial_state = if let Some(state_str) = &args.initial_state {
67 match resolve_reference(state_str).await {
69 Ok(bytes) => {
70 debug!("Resolved initial state from reference: {}", state_str);
71 Some(bytes)
72 }
73 Err(_) => {
74 debug!("Using provided string as JSON initial state");
76 Some(state_str.as_bytes().to_vec())
77 }
78 }
79 } else {
80 None
81 };
82
83 let client = ctx.create_client();
85 client
86 .connect()
87 .await
88 .map_err(|e| CliError::connection_failed(address, e))?;
89
90 debug!("Calling start actor on client");
92 client
93 .start_actor(manifest_content, initial_state, args.parent, args.subscribe)
94 .await
95 .map_err(|e| CliError::actor_not_found(format!("Failed to start actor: {}", e)))?;
96 debug!("Actor start request sent successfully");
97
98 let use_tui = args.subscribe && args.parent && !ctx.json && !args.id_only;
100
101 if use_tui {
102 return run_with_tui(args, client).await;
104 } else if args.subscribe && !ctx.json {
105 println!("");
106 display_events_header(&args.format);
107 }
108
109 let mut actor_started = false;
110
111 let timeout_duration = tokio::time::Duration::from_secs(30);
113
114 debug!("Entering response loop, waiting for actor start confirmation or events");
115 loop {
116 tokio::select! {
117 data = client.next_response() => {
118 debug!("Received response from client");
119 debug!("Response data: {:?}", data);
120 if let Ok(data) = data {
121 match data {
122 ManagementResponse::ActorStarted { id } => {
123 debug!("Management response received: Actor started with ID: {}", id);
124 actor_started = true;
125
126 if args.id_only {
127 println!("{}", id);
128 break;
129 } else {
130 let result = ActorStarted {
131 actor_id: id.to_string(),
132 manifest_path: args.manifest.clone(),
133 address: address.to_string(),
134 subscribing: args.subscribe,
135 acting_as_parent: args.parent,
136 };
137 debug!("Outputting result: {:?}", result);
138 ctx.output.output(&result, None)?;
139
140 if !args.subscribe && !args.parent {
142 break;
143 }
144 }
145 }
146 ManagementResponse::ActorEvent { event } => {
147 if args.subscribe {
148 display_single_event(&event, &args.format)
149 .map_err(|e| CliError::invalid_input("event_display", "event", e.to_string()))?;
150 }
151 }
152 ManagementResponse::ActorError { error } => {
153 if args.subscribe {
154 println!("-----[actor error]-----------------");
155 println!(" {}", error);
156 println!("-----------------------------------");
157 }
158 }
159 ManagementResponse::ActorStopped { id } => {
160 println!("-----[actor stopped]-----------------");
161 println!("{}", id);
162 println!("-------------------------------------");
163 break;
164 }
165 ManagementResponse::ActorResult(actor_result) => {
166 if args.parent {
167 println!("-----[actor result]-----------------");
168 println!(" {}", actor_result);
169 println!("------------------------------------");
170 }
171 }
172 ManagementResponse::Error { error } => {
173 return Err(CliError::management_error(error));
174 }
175 _ => {
176 println!("Unknown response received");
177 break;
178 }
179 }
180 }
181 }
182 _ = tokio::time::sleep(timeout_duration) => {
183 if !actor_started {
184 return Err(CliError::operation_timeout("Actor startup", timeout_duration.as_secs()));
185 }
186 }
187 _ = tokio::signal::ctrl_c() => {
188 debug!("Received Ctrl-C, stopping");
189 if !ctx.json {
190 println!("\n{}\n", "Interrupted by user");
191 }
192 break;
193 }
194 }
195 }
196
197 Ok(())
198}
199
200async fn run_with_tui(
202 args: &StartArgs,
203 client: crate::client::TheaterClient,
204) -> Result<(), CliError> {
205 debug!("Starting TUI mode for actor monitoring");
206
207 let (response_tx, response_rx) = mpsc::unbounded_channel();
209
210 let mut _actor_id: Option<String> = None;
212 let mut tui_started = false;
213 let mut tui_completed = false;
214
215 let timeout_duration = tokio::time::Duration::from_secs(30);
217
218 let mut tui_handle = {
220 let manifest_path = args.manifest.clone();
221 tokio::spawn(async move {
222 if let Err(e) =
224 tui::run_tui("Starting...".to_string(), manifest_path, response_rx).await
225 {
226 eprintln!("TUI error: {}", e);
227 }
228 })
229 };
230
231 loop {
232 tokio::select! {
233 data = client.next_response() => {
234 if let Ok(response) = data {
235 match &response {
236 ManagementResponse::ActorStarted { id } => {
237 _actor_id = Some(id.to_string());
238 debug!("Actor started with ID: {}", id);
239 tui_started = true;
240 }
241 ManagementResponse::ActorStopped { .. } => {
242 let _ = response_tx.send(response);
244 break;
245 }
246 _ => {}
247 }
248
249 if let Err(_) = response_tx.send(response) {
251 debug!("TUI channel closed, stopping");
253 break;
254 }
255 }
256 }
257 _ = tokio::time::sleep(timeout_duration) => {
258 if !tui_started {
259 return Err(CliError::operation_timeout("Actor startup", timeout_duration.as_secs()));
260 }
261 }
262 _ = tokio::signal::ctrl_c() => {
263 debug!("Received Ctrl-C, stopping TUI mode");
264 break;
265 }
266 result = &mut tui_handle, if !tui_completed => {
267 match result {
268 Ok(_) => debug!("TUI task completed"),
269 Err(e) => debug!("TUI task error: {}", e),
270 }
271 tui_completed = true;
272 break;
273 }
274 }
275 }
276
277 if !tui_completed {
279 let _ = tokio::time::timeout(tokio::time::Duration::from_millis(500), tui_handle).await;
280 }
281
282 Ok(())
283}