1use anyhow::Result;
2use clap::{Parser, ValueEnum};
3use std::io::Write;
4use std::path::PathBuf;
5use tokio::sync::mpsc;
6use tracing::{debug, error};
7
8use crate::{error::CliError, CommandContext};
9use theater::chain::ChainEvent;
10use theater::config::actor_manifest::{
11 RuntimeHostConfig, StoreHandlerConfig, SupervisorHostConfig, TcpHandlerConfig,
12 TerminalHandlerConfig, TimerHandlerConfig,
13};
14use theater::handler::HandlerRegistry;
15use theater::messages::{default_init_state, TheaterCommand};
16use theater::pack_bridge::Value;
17use theater::theater_runtime::TheaterRuntime;
18use theater::utils::resolve_reference;
19use theater::ManifestConfig;
20use theater::TheaterId;
21use theater_handler_loop::LoopHandler;
22use theater_handler_message_server::{MessageRouter, MessageServerHandler};
23use theater_handler_podman::PodmanHandler;
24use theater_handler_rpc::RpcHandler;
25use theater_handler_runtime::RuntimeHandler;
26use theater_handler_store::StoreHandler;
27use theater_handler_supervisor::SupervisorHandler;
28use theater_handler_tcp::TcpHandler;
29use theater_handler_terminal::TerminalHandler;
30use theater_handler_timer::TimerHandler;
31
32#[derive(Debug, Clone, Copy, ValueEnum, Default)]
34pub enum EventFormat {
35 Json,
37 #[default]
39 Short,
40 Full,
42}
43
44#[derive(Debug, Parser)]
46pub struct SpawnArgs {
47 #[arg(default_value = "manifest.toml")]
49 pub manifest: String,
50
51 #[arg(long)]
53 pub events: bool,
54
55 #[arg(long, value_enum, default_value = "short")]
57 pub events_format: EventFormat,
58
59 #[arg(long, default_missing_value = ".chains", num_args = 0..=1)]
62 pub save: Option<PathBuf>,
63
64 #[arg(long)]
66 pub no_actor_logs: bool,
67}
68
69pub type SetupArgs = SpawnArgs;
71
72fn format_event_short(event: &ChainEvent, actor_id: &TheaterId) -> String {
74 let id_str = actor_id.to_string();
75 let short_id = &id_str[..8.min(id_str.len())];
76 format!("[{}] {}\n", short_id, event)
77}
78
79fn format_event_full(event: &ChainEvent, actor_id: &TheaterId) -> String {
81 let id_str = actor_id.to_string();
82 let short_id = &id_str[..8.min(id_str.len())];
83 let hash_hex = hex::encode(&event.hash);
84 let parent_hex = event
85 .parent_hash
86 .as_ref()
87 .map(hex::encode)
88 .unwrap_or_else(|| "none".to_string());
89 let data_str = String::from_utf8_lossy(&event.data);
90
91 format!(
92 "EVENT [{}] {}\nparent: {}\ntype: {}\nsize: {}\n{}\n\n",
93 short_id,
94 hash_hex,
95 parent_hex,
96 event.event_type,
97 event.data.len(),
98 data_str
99 )
100}
101
102fn format_event_json(event: &ChainEvent, actor_id: &TheaterId) -> String {
104 let json = serde_json::json!({
105 "actor_id": actor_id.to_string(),
106 "hash": hex::encode(&event.hash),
107 "parent_hash": event.parent_hash.as_ref().map(hex::encode),
108 "event_type": event.event_type,
109 "data": format!("{} bytes (pack-encoded)", event.data.len())
110 });
111 serde_json::to_string(&json).unwrap_or_else(|_| "{}".to_string())
112}
113
114fn create_handler_registry(
116 theater_tx: mpsc::Sender<TheaterCommand>,
117 show_actor_logs: bool,
118) -> Result<HandlerRegistry, CliError> {
119 let mut registry = HandlerRegistry::new();
120
121 let runtime_config = RuntimeHostConfig {};
123 registry.register(
124 RuntimeHandler::new(runtime_config, theater_tx.clone(), None)
125 .with_show_logs(show_actor_logs),
126 );
127
128 let store_config = StoreHandlerConfig::default();
130 registry.register(StoreHandler::new(store_config, None));
131
132 let supervisor_config = SupervisorHostConfig {};
134 registry.register(SupervisorHandler::new(supervisor_config, None));
135
136 let message_router = MessageRouter::new();
138 registry.register(MessageServerHandler::new(None, message_router.clone()));
139
140 registry.register(RpcHandler::new(theater_tx.clone()));
142
143 let tcp_config = TcpHandlerConfig {
145 listen: None,
146 max_connections: None,
147 ..Default::default()
148 };
149 registry.register(TcpHandler::new(tcp_config));
150
151 let terminal_config = TerminalHandlerConfig::default();
153 registry.register(TerminalHandler::new(terminal_config));
154
155 let timer_config = TimerHandlerConfig::default();
157 registry.register(TimerHandler::new(timer_config));
158
159 registry.register(LoopHandler::new());
161
162 let podman_config = theater::config::actor_manifest::PodmanHandlerConfig::default();
164 registry.register(PodmanHandler::new(podman_config));
165
166 Ok(registry)
167}
168
169pub async fn execute_spawn(args: &SpawnArgs, ctx: &CommandContext) -> Result<(), CliError> {
174 run(args, ctx, true).await
175}
176
177pub async fn execute_setup(args: &SetupArgs, ctx: &CommandContext) -> Result<(), CliError> {
182 run(args, ctx, false).await
183}
184
185async fn run(args: &SpawnArgs, ctx: &CommandContext, call_init: bool) -> Result<(), CliError> {
188 debug!("Starting actor from manifest: {}", args.manifest);
189
190 let manifest_bytes = resolve_reference(&args.manifest).await.map_err(|e| {
192 CliError::invalid_manifest(format!(
193 "Failed to resolve manifest reference '{}': {}",
194 args.manifest, e
195 ))
196 })?;
197
198 let manifest_content = String::from_utf8(manifest_bytes).map_err(|e| {
199 CliError::invalid_manifest(format!("Manifest content is not valid UTF-8: {}", e))
200 })?;
201
202 let manifest = ManifestConfig::from_toml_str(&manifest_content)
206 .map_err(|e| CliError::invalid_manifest(format!("Failed to parse manifest: {}", e)))?;
207
208 let (theater_tx, theater_rx) = mpsc::channel::<TheaterCommand>(32);
210 let handler_registry = create_handler_registry(theater_tx.clone(), !args.no_actor_logs)?;
211
212 let mut runtime = TheaterRuntime::new(
213 theater_tx.clone(),
214 theater_rx,
215 None, handler_registry,
217 )
218 .await
219 .map_err(|e| CliError::server_error(format!("Failed to create runtime: {}", e)))?;
220
221 if let Some(ref dir) = args.save {
223 runtime.chain_dir = Some(dir.clone());
224 }
225
226 let (global_events_tx, mut global_events_rx) = mpsc::channel(256);
228 runtime.add_global_subscription(global_events_tx);
229
230 let runtime_handle = tokio::spawn(async move {
232 if let Err(e) = runtime.run().await {
233 error!("Theater runtime error: {}", e);
234 }
235 });
236
237 let wasm_path = if manifest.package.starts_with('/') || manifest.package.contains("://") {
239 manifest.package.clone()
241 } else {
242 let manifest_path = std::path::Path::new(&args.manifest);
244 if let Some(manifest_dir) = manifest_path.parent() {
245 manifest_dir
246 .join(&manifest.package)
247 .to_string_lossy()
248 .to_string()
249 } else {
250 manifest.package.clone()
251 }
252 };
253
254 let wasm_bytes = resolve_reference(&wasm_path).await.map_err(|e| {
256 CliError::server_error(format!("Failed to load WASM from '{}': {}", wasm_path, e))
257 })?;
258
259 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
261
262 let (supervisor_tx, mut supervisor_rx) = mpsc::channel(32);
264
265 let init_state = match manifest.initial_state.as_ref() {
274 Some(s) => Value::String(s.clone()),
275 None => default_init_state(),
276 };
277
278 let cmd = if call_init {
282 TheaterCommand::SpawnActor {
283 wasm_bytes,
284 name: Some(manifest.name.clone()),
285 manifest: Some(manifest),
286 init_state,
287 response_tx,
288 supervisor_tx: Some(supervisor_tx),
289 subscription_tx: None, }
291 } else {
292 TheaterCommand::SetupActor {
293 wasm_bytes,
294 name: Some(manifest.name.clone()),
295 manifest: Some(manifest),
296 init_state,
297 response_tx,
298 supervisor_tx: Some(supervisor_tx),
299 subscription_tx: None, }
301 };
302
303 theater_tx
304 .send(cmd)
305 .await
306 .map_err(|e| CliError::server_error(format!("Failed to send spawn command: {}", e)))?;
307
308 let actor_id = match response_rx.await {
310 Ok(Ok(id)) => {
311 debug!("Actor started: {}", id);
312 id
313 }
314 Ok(Err(e)) => {
315 return Err(CliError::server_error(format!(
316 "Failed to start actor: {}",
317 e
318 )));
319 }
320 Err(e) => {
321 return Err(CliError::server_error(format!(
322 "Failed to receive spawn response: {}",
323 e
324 )));
325 }
326 };
327
328 loop {
338 tokio::select! {
339 result = supervisor_rx.recv() => {
341 match result {
342 Some(actor_result) => {
343 debug!("Actor exited: {:?}", actor_result);
344 match actor_result {
345 theater::messages::ActorResult::Success(success) => {
346 if let Some(output) = success.result {
347 let _ = std::io::stdout().write_all(&output);
349 let _ = std::io::stdout().flush();
350 }
351 }
352 theater::messages::ActorResult::Error(err) => {
353 eprintln!("Actor error: {}", err.error);
354 std::process::exit(1);
355 }
356 theater::messages::ActorResult::ExternalStop(_) => {
357 debug!("Actor stopped externally");
358 }
359 }
360 break;
361 }
362 None => {
363 debug!("Supervisor channel closed");
365 break;
366 }
367 }
368 }
369
370 event = global_events_rx.recv() => {
372 if let Some((event_actor_id, event_result)) = event {
373 match event_result {
374 Ok(chain_event) => {
375 if args.events {
378 match args.events_format {
379 EventFormat::Json => {
380 println!("{}", format_event_json(&chain_event, &event_actor_id));
381 }
382 EventFormat::Short => {
383 print!("{}", format_event_short(&chain_event, &event_actor_id));
384 }
385 EventFormat::Full => {
386 print!("{}", format_event_full(&chain_event, &event_actor_id));
387 }
388 }
389 }
390
391 if event_actor_id == actor_id && chain_event.event_type == "shutdown" {
393 break;
394 }
395 }
396 Err(e) => {
397 debug!("Actor error event: {:?}", e);
398 }
399 }
400 }
401 }
402
403 _ = tokio::signal::ctrl_c() => {
405 debug!("Received Ctrl+C, stopping actor {}", actor_id);
406 eprintln!("\nStopping actor...");
407
408 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
409 let _ = theater_tx.send(TheaterCommand::StopActor {
410 actor_id,
411 response_tx: stop_tx,
412 }).await;
413
414 match tokio::time::timeout(
416 tokio::time::Duration::from_secs(5),
417 stop_rx,
418 ).await {
419 Ok(Ok(Ok(()))) => debug!("Actor stopped gracefully"),
420 _ => debug!("Actor stop timed out or failed"),
421 }
422 break;
423 }
424
425 _ = ctx.shutdown_token.cancelled() => {
427 debug!("Shutdown token cancelled");
428 break;
429 }
430 }
431 }
432
433 drop(theater_tx);
435
436 let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), runtime_handle).await;
438
439 Ok(())
440}