1use anyhow::Result;
2use clap::{Parser, ValueEnum};
3use std::io::Write;
4use tokio::sync::mpsc;
5use tracing::{debug, error};
6
7use crate::{error::CliError, CommandContext};
8use theater::chain::ChainEvent;
9use theater::config::actor_manifest::{
10 RuntimeHostConfig, StoreHandlerConfig, SupervisorHostConfig, TcpHandlerConfig,
11 TerminalHandlerConfig, TimerHandlerConfig,
12};
13use theater::handler::HandlerRegistry;
14use theater::messages::{default_init_state, TheaterCommand};
15use theater::pack_bridge::Value;
16use theater::theater_runtime::TheaterRuntime;
17use theater::utils::resolve_reference;
18use theater::ManifestConfig;
19use theater::TheaterId;
20use theater_handler_loop::LoopHandler;
21use theater_handler_message_server::{MessageRouter, MessageServerHandler};
22use theater_handler_podman::PodmanHandler;
23use theater_handler_rpc::RpcHandler;
24use theater_handler_runtime::RuntimeHandler;
25use theater_handler_store::StoreHandler;
26use theater_handler_supervisor::SupervisorHandler;
27use theater_handler_tcp::TcpHandler;
28use theater_handler_terminal::TerminalHandler;
29use theater_handler_timer::TimerHandler;
30
31#[derive(Debug, Clone, Copy, ValueEnum, Default)]
33pub enum EventFormat {
34 Json,
36 #[default]
38 Short,
39 Full,
41}
42
43#[derive(Debug, Parser)]
45pub struct SpawnArgs {
46 #[arg(default_value = "manifest.toml")]
48 pub manifest: String,
49
50 #[arg(long)]
52 pub events: bool,
53
54 #[arg(long, value_enum, default_value = "short")]
56 pub events_format: EventFormat,
57
58 #[arg(long)]
60 pub no_actor_logs: bool,
61}
62
63pub type SetupArgs = SpawnArgs;
65
66fn format_event_short(event: &ChainEvent, actor_id: &TheaterId) -> String {
68 let id_str = actor_id.to_string();
69 let short_id = &id_str[..8.min(id_str.len())];
70 format!("[{}] {}\n", short_id, event)
71}
72
73fn format_event_full(event: &ChainEvent, actor_id: &TheaterId) -> String {
75 let id_str = actor_id.to_string();
76 let short_id = &id_str[..8.min(id_str.len())];
77 let hash_hex = hex::encode(&event.hash);
78 let parent_hex = event
79 .parent_hash
80 .as_ref()
81 .map(hex::encode)
82 .unwrap_or_else(|| "none".to_string());
83 let data_str = String::from_utf8_lossy(&event.data);
84
85 format!(
86 "EVENT [{}] {}\nparent: {}\ntype: {}\nsize: {}\n{}\n\n",
87 short_id,
88 hash_hex,
89 parent_hex,
90 event.event_type,
91 event.data.len(),
92 data_str
93 )
94}
95
96fn format_event_json(event: &ChainEvent, actor_id: &TheaterId) -> String {
98 let json = serde_json::json!({
99 "actor_id": actor_id.to_string(),
100 "hash": hex::encode(&event.hash),
101 "parent_hash": event.parent_hash.as_ref().map(hex::encode),
102 "event_type": event.event_type,
103 "data": format!("{} bytes (pack-encoded)", event.data.len())
104 });
105 serde_json::to_string(&json).unwrap_or_else(|_| "{}".to_string())
106}
107
108fn create_handler_registry(
110 theater_tx: mpsc::Sender<TheaterCommand>,
111 show_actor_logs: bool,
112) -> Result<HandlerRegistry, CliError> {
113 let mut registry = HandlerRegistry::new();
114
115 let runtime_config = RuntimeHostConfig {};
117 registry.register(
118 RuntimeHandler::new(runtime_config, theater_tx.clone(), None)
119 .with_show_logs(show_actor_logs),
120 );
121
122 let store_config = StoreHandlerConfig::default();
124 registry.register(StoreHandler::new(store_config, None));
125
126 let supervisor_config = SupervisorHostConfig {};
128 registry.register(SupervisorHandler::new(supervisor_config, None));
129
130 let message_router = MessageRouter::new();
132 registry.register(MessageServerHandler::new(None, message_router.clone()));
133
134 registry.register(RpcHandler::new(theater_tx.clone()));
136
137 let tcp_config = TcpHandlerConfig {
139 listen: None,
140 max_connections: None,
141 ..Default::default()
142 };
143 registry.register(TcpHandler::new(tcp_config));
144
145 let terminal_config = TerminalHandlerConfig::default();
147 registry.register(TerminalHandler::new(terminal_config));
148
149 let timer_config = TimerHandlerConfig::default();
151 registry.register(TimerHandler::new(timer_config));
152
153 registry.register(LoopHandler::new());
155
156 let podman_config = theater::config::actor_manifest::PodmanHandlerConfig::default();
158 registry.register(PodmanHandler::new(podman_config));
159
160 Ok(registry)
161}
162
163pub async fn execute_spawn(args: &SpawnArgs, ctx: &CommandContext) -> Result<(), CliError> {
168 run(args, ctx, true).await
169}
170
171pub async fn execute_setup(args: &SetupArgs, ctx: &CommandContext) -> Result<(), CliError> {
176 run(args, ctx, false).await
177}
178
179async fn run(args: &SpawnArgs, ctx: &CommandContext, call_init: bool) -> Result<(), CliError> {
182 debug!("Starting actor from manifest: {}", args.manifest);
183
184 let manifest_bytes = resolve_reference(&args.manifest).await.map_err(|e| {
186 CliError::invalid_manifest(format!(
187 "Failed to resolve manifest reference '{}': {}",
188 args.manifest, e
189 ))
190 })?;
191
192 let manifest_content = String::from_utf8(manifest_bytes).map_err(|e| {
193 CliError::invalid_manifest(format!("Manifest content is not valid UTF-8: {}", e))
194 })?;
195
196 let manifest = ManifestConfig::from_toml_str(&manifest_content)
198 .map_err(|e| CliError::invalid_manifest(format!("Failed to parse manifest: {}", e)))?;
199
200 let (theater_tx, theater_rx) = mpsc::channel::<TheaterCommand>(32);
202 let handler_registry = create_handler_registry(theater_tx.clone(), !args.no_actor_logs)?;
203
204 let mut runtime = TheaterRuntime::new(
205 theater_tx.clone(),
206 theater_rx,
207 None, handler_registry,
209 )
210 .await
211 .map_err(|e| CliError::server_error(format!("Failed to create runtime: {}", e)))?;
212
213 let (global_events_tx, mut global_events_rx) = mpsc::channel(256);
215 runtime.add_global_subscription(global_events_tx);
216
217 let runtime_handle = tokio::spawn(async move {
219 if let Err(e) = runtime.run().await {
220 error!("Theater runtime error: {}", e);
221 }
222 });
223
224 let wasm_path = if manifest.package.starts_with('/') || manifest.package.contains("://") {
226 manifest.package.clone()
228 } else {
229 let manifest_path = std::path::Path::new(&args.manifest);
231 if let Some(manifest_dir) = manifest_path.parent() {
232 manifest_dir
233 .join(&manifest.package)
234 .to_string_lossy()
235 .to_string()
236 } else {
237 manifest.package.clone()
238 }
239 };
240
241 let wasm_bytes = resolve_reference(&wasm_path).await.map_err(|e| {
243 CliError::server_error(format!("Failed to load WASM from '{}': {}", wasm_path, e))
244 })?;
245
246 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
248
249 let (supervisor_tx, mut supervisor_rx) = mpsc::channel(32);
251
252 let init_state = match manifest.initial_state.as_ref() {
261 Some(s) => Value::String(s.clone()),
262 None => default_init_state(),
263 };
264
265 let cmd = if call_init {
269 TheaterCommand::SpawnActor {
270 wasm_bytes,
271 name: Some(manifest.name.clone()),
272 manifest: Some(manifest),
273 init_state,
274 response_tx,
275 supervisor_tx: Some(supervisor_tx),
276 subscription_tx: None, }
278 } else {
279 TheaterCommand::SetupActor {
280 wasm_bytes,
281 name: Some(manifest.name.clone()),
282 manifest: Some(manifest),
283 init_state,
284 response_tx,
285 supervisor_tx: Some(supervisor_tx),
286 subscription_tx: None, }
288 };
289
290 theater_tx
291 .send(cmd)
292 .await
293 .map_err(|e| CliError::server_error(format!("Failed to send spawn command: {}", e)))?;
294
295 let actor_id = match response_rx.await {
297 Ok(Ok(id)) => {
298 debug!("Actor started: {}", id);
299 id
300 }
301 Ok(Err(e)) => {
302 return Err(CliError::server_error(format!(
303 "Failed to start actor: {}",
304 e
305 )));
306 }
307 Err(e) => {
308 return Err(CliError::server_error(format!(
309 "Failed to receive spawn response: {}",
310 e
311 )));
312 }
313 };
314
315 loop {
325 tokio::select! {
326 result = supervisor_rx.recv() => {
328 match result {
329 Some(actor_result) => {
330 debug!("Actor exited: {:?}", actor_result);
331 match actor_result {
332 theater::messages::ActorResult::Success(success) => {
333 if let Some(output) = success.result {
334 let _ = std::io::stdout().write_all(&output);
336 let _ = std::io::stdout().flush();
337 }
338 }
339 theater::messages::ActorResult::Error(err) => {
340 eprintln!("Actor error: {}", err.error);
341 std::process::exit(1);
342 }
343 theater::messages::ActorResult::ExternalStop(_) => {
344 debug!("Actor stopped externally");
345 }
346 }
347 break;
348 }
349 None => {
350 debug!("Supervisor channel closed");
352 break;
353 }
354 }
355 }
356
357 event = global_events_rx.recv() => {
359 if let Some((event_actor_id, event_result)) = event {
360 match event_result {
361 Ok(chain_event) => {
362 if args.events {
365 match args.events_format {
366 EventFormat::Json => {
367 println!("{}", format_event_json(&chain_event, &event_actor_id));
368 }
369 EventFormat::Short => {
370 print!("{}", format_event_short(&chain_event, &event_actor_id));
371 }
372 EventFormat::Full => {
373 print!("{}", format_event_full(&chain_event, &event_actor_id));
374 }
375 }
376 }
377
378 if event_actor_id == actor_id && chain_event.event_type == "shutdown" {
380 break;
381 }
382 }
383 Err(e) => {
384 debug!("Actor error event: {:?}", e);
385 }
386 }
387 }
388 }
389
390 _ = tokio::signal::ctrl_c() => {
392 debug!("Received Ctrl+C, stopping actor {}", actor_id);
393 eprintln!("\nStopping actor...");
394
395 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
396 let _ = theater_tx.send(TheaterCommand::StopActor {
397 actor_id,
398 response_tx: stop_tx,
399 }).await;
400
401 match tokio::time::timeout(
403 tokio::time::Duration::from_secs(5),
404 stop_rx,
405 ).await {
406 Ok(Ok(Ok(()))) => debug!("Actor stopped gracefully"),
407 _ => debug!("Actor stop timed out or failed"),
408 }
409 break;
410 }
411
412 _ = ctx.shutdown_token.cancelled() => {
414 debug!("Shutdown token cancelled");
415 break;
416 }
417 }
418 }
419
420 drop(theater_tx);
422
423 let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), runtime_handle).await;
425
426 Ok(())
427}