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 _ => {
173 println!("Unknown response received");
174 break;
175 }
176 }
177 }
178 }
179 _ = tokio::time::sleep(timeout_duration) => {
180 if !actor_started {
181 return Err(CliError::operation_timeout("Actor startup", timeout_duration.as_secs()));
182 }
183 }
184 _ = tokio::signal::ctrl_c() => {
185 debug!("Received Ctrl-C, stopping");
186 if !ctx.json {
187 println!("\n{}\n", "Interrupted by user");
188 }
189 break;
190 }
191 }
192 }
193
194 Ok(())
195}
196
197async fn run_with_tui(
199 args: &StartArgs,
200 client: crate::client::TheaterClient,
201) -> Result<(), CliError> {
202 debug!("Starting TUI mode for actor monitoring");
203
204 let (response_tx, response_rx) = mpsc::unbounded_channel();
206
207 let mut _actor_id: Option<String> = None;
209 let mut tui_started = false;
210 let mut tui_completed = false;
211
212 let timeout_duration = tokio::time::Duration::from_secs(30);
214
215 let mut tui_handle = {
217 let manifest_path = args.manifest.clone();
218 tokio::spawn(async move {
219 if let Err(e) =
221 tui::run_tui("Starting...".to_string(), manifest_path, response_rx).await
222 {
223 eprintln!("TUI error: {}", e);
224 }
225 })
226 };
227
228 loop {
229 tokio::select! {
230 data = client.next_response() => {
231 if let Ok(response) = data {
232 match &response {
233 ManagementResponse::ActorStarted { id } => {
234 _actor_id = Some(id.to_string());
235 debug!("Actor started with ID: {}", id);
236 tui_started = true;
237 }
238 ManagementResponse::ActorStopped { .. } => {
239 let _ = response_tx.send(response);
241 break;
242 }
243 _ => {}
244 }
245
246 if let Err(_) = response_tx.send(response) {
248 debug!("TUI channel closed, stopping");
250 break;
251 }
252 }
253 }
254 _ = tokio::time::sleep(timeout_duration) => {
255 if !tui_started {
256 return Err(CliError::operation_timeout("Actor startup", timeout_duration.as_secs()));
257 }
258 }
259 _ = tokio::signal::ctrl_c() => {
260 debug!("Received Ctrl-C, stopping TUI mode");
261 break;
262 }
263 result = &mut tui_handle, if !tui_completed => {
264 match result {
265 Ok(_) => debug!("TUI task completed"),
266 Err(e) => debug!("TUI task error: {}", e),
267 }
268 tui_completed = true;
269 break;
270 }
271 }
272 }
273
274 if !tui_completed {
276 let _ = tokio::time::timeout(tokio::time::Duration::from_millis(500), tui_handle).await;
277 }
278
279 Ok(())
280}