1use anyhow::Result;
2use clap::{Parser, ValueEnum};
3use std::io::Write;
4use std::sync::Arc;
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, resolve_reference_cached, ResourceCache};
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)]
61 pub no_actor_logs: bool,
62}
63
64pub type SetupArgs = SpawnArgs;
66
67fn format_event_short(event: &ChainEvent, actor_id: &TheaterId) -> String {
69 let id_str = actor_id.to_string();
70 let short_id = &id_str[..8.min(id_str.len())];
71 format!("[{}] {}\n", short_id, event)
72}
73
74fn format_event_full(event: &ChainEvent, actor_id: &TheaterId) -> String {
76 let id_str = actor_id.to_string();
77 let short_id = &id_str[..8.min(id_str.len())];
78 let hash_hex = hex::encode(&event.hash);
79 let parent_hex = event
80 .parent_hash
81 .as_ref()
82 .map(hex::encode)
83 .unwrap_or_else(|| "none".to_string());
84 let data_str = String::from_utf8_lossy(&event.data);
85
86 format!(
87 "EVENT [{}] {}\nparent: {}\ntype: {}\nsize: {}\n{}\n\n",
88 short_id,
89 hash_hex,
90 parent_hex,
91 event.event_type,
92 event.data.len(),
93 data_str
94 )
95}
96
97fn format_event_json(event: &ChainEvent, actor_id: &TheaterId) -> String {
99 let json = serde_json::json!({
100 "actor_id": actor_id.to_string(),
101 "hash": hex::encode(&event.hash),
102 "parent_hash": event.parent_hash.as_ref().map(hex::encode),
103 "event_type": event.event_type,
104 "data": format!("{} bytes (pack-encoded)", event.data.len())
105 });
106 serde_json::to_string(&json).unwrap_or_else(|_| "{}".to_string())
107}
108
109fn create_handler_registry(
111 theater_tx: mpsc::Sender<TheaterCommand>,
112 show_actor_logs: bool,
113 resource_cache: Arc<ResourceCache>,
114) -> Result<HandlerRegistry, CliError> {
115 let mut registry = HandlerRegistry::new();
116
117 let runtime_config = RuntimeHostConfig {};
119 registry.register(
120 RuntimeHandler::new(runtime_config, theater_tx.clone(), None)
121 .with_show_logs(show_actor_logs),
122 );
123
124 let store_config = StoreHandlerConfig::default();
126 registry.register(StoreHandler::new(store_config, None));
127
128 let supervisor_config = SupervisorHostConfig {};
133 registry.register(
134 SupervisorHandler::new(supervisor_config, None).with_resource_cache(resource_cache),
135 );
136
137 let message_router = MessageRouter::new();
139 registry.register(MessageServerHandler::new(None, message_router.clone()));
140
141 registry.register(RpcHandler::new(theater_tx.clone()));
143
144 let tcp_config = TcpHandlerConfig {
146 listen: None,
147 max_connections: None,
148 ..Default::default()
149 };
150 registry.register(TcpHandler::new(tcp_config));
151
152 let terminal_config = TerminalHandlerConfig::default();
154 registry.register(TerminalHandler::new(terminal_config));
155
156 let timer_config = TimerHandlerConfig::default();
158 registry.register(TimerHandler::new(timer_config));
159
160 registry.register(LoopHandler::new());
162
163 let podman_config = theater::config::actor_manifest::PodmanHandlerConfig::default();
165 registry.register(PodmanHandler::new(podman_config));
166
167 Ok(registry)
168}
169
170pub async fn execute_spawn(args: &SpawnArgs, ctx: &CommandContext) -> Result<(), CliError> {
175 run(args, ctx, true).await
176}
177
178pub async fn execute_setup(args: &SetupArgs, ctx: &CommandContext) -> Result<(), CliError> {
183 run(args, ctx, false).await
184}
185
186async fn run(args: &SpawnArgs, ctx: &CommandContext, call_init: bool) -> Result<(), CliError> {
189 debug!("Starting actor from manifest: {}", args.manifest);
190
191 let manifest_bytes = resolve_reference(&args.manifest).await.map_err(|e| {
193 CliError::invalid_manifest(format!(
194 "Failed to resolve manifest reference '{}': {}",
195 args.manifest, e
196 ))
197 })?;
198
199 let manifest_content = String::from_utf8(manifest_bytes).map_err(|e| {
200 CliError::invalid_manifest(format!("Manifest content is not valid UTF-8: {}", e))
201 })?;
202
203 let manifest = ManifestConfig::from_toml_str(&manifest_content)
205 .map_err(|e| CliError::invalid_manifest(format!("Failed to parse manifest: {}", e)))?;
206
207 let (theater_tx, theater_rx) = mpsc::channel::<TheaterCommand>(32);
209 let resource_cache = Arc::new(ResourceCache::new());
214 let handler_registry = create_handler_registry(
215 theater_tx.clone(),
216 !args.no_actor_logs,
217 resource_cache.clone(),
218 )?;
219
220 let mut runtime = TheaterRuntime::new(
221 theater_tx.clone(),
222 theater_rx,
223 None, handler_registry,
225 resource_cache.clone(),
226 )
227 .await
228 .map_err(|e| CliError::server_error(format!("Failed to create runtime: {}", e)))?;
229
230 let (global_events_tx, mut global_events_rx) = mpsc::channel(256);
232 runtime.add_global_subscription(global_events_tx);
233
234 let runtime_handle = tokio::spawn(async move {
236 if let Err(e) = runtime.run().await {
237 error!("Theater runtime error: {}", e);
238 }
239 });
240
241 let wasm_path = if manifest.package.starts_with('/') || manifest.package.contains("://") {
243 manifest.package.clone()
245 } else {
246 let manifest_path = std::path::Path::new(&args.manifest);
248 if let Some(manifest_dir) = manifest_path.parent() {
249 manifest_dir
250 .join(&manifest.package)
251 .to_string_lossy()
252 .to_string()
253 } else {
254 manifest.package.clone()
255 }
256 };
257
258 let wasm_bytes = if manifest.static_package {
262 let (arc, _hit) = resolve_reference_cached(&wasm_path, &resource_cache)
263 .await
264 .map_err(|e| {
265 CliError::server_error(format!("Failed to load WASM from '{}': {}", wasm_path, e))
266 })?;
267 (*arc).clone()
268 } else {
269 resolve_reference(&wasm_path).await.map_err(|e| {
270 CliError::server_error(format!("Failed to load WASM from '{}': {}", wasm_path, e))
271 })?
272 };
273
274 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
276
277 let (supervisor_tx, mut supervisor_rx) = mpsc::channel(32);
279
280 let init_state = match manifest.initial_state.as_ref() {
289 Some(s) => Value::String(s.clone()),
290 None => default_init_state(),
291 };
292
293 let cmd = if call_init {
297 TheaterCommand::SpawnActor {
298 wasm_bytes,
299 name: Some(manifest.name.clone()),
300 manifest: Some(manifest),
301 init_state,
302 response_tx,
303 supervisor_tx: Some(supervisor_tx),
304 subscription_tx: None, }
306 } else {
307 TheaterCommand::SetupActor {
308 wasm_bytes,
309 name: Some(manifest.name.clone()),
310 manifest: Some(manifest),
311 init_state,
312 response_tx,
313 supervisor_tx: Some(supervisor_tx),
314 subscription_tx: None, }
316 };
317
318 theater_tx
319 .send(cmd)
320 .await
321 .map_err(|e| CliError::server_error(format!("Failed to send spawn command: {}", e)))?;
322
323 let actor_id = match response_rx.await {
325 Ok(Ok(id)) => {
326 debug!("Actor started: {}", id);
327 id
328 }
329 Ok(Err(e)) => {
330 return Err(CliError::server_error(format!(
331 "Failed to start actor: {}",
332 e
333 )));
334 }
335 Err(e) => {
336 return Err(CliError::server_error(format!(
337 "Failed to receive spawn response: {}",
338 e
339 )));
340 }
341 };
342
343 loop {
353 tokio::select! {
354 result = supervisor_rx.recv() => {
356 match result {
357 Some(actor_result) => {
358 debug!("Actor exited: {:?}", actor_result);
359 match actor_result {
360 theater::messages::ActorResult::Success(success) => {
361 if let Some(output) = success.result {
362 let _ = std::io::stdout().write_all(&output);
364 let _ = std::io::stdout().flush();
365 }
366 }
367 theater::messages::ActorResult::Error(err) => {
368 eprintln!("Actor error: {}", err.error);
369 std::process::exit(1);
370 }
371 theater::messages::ActorResult::ExternalStop(_) => {
372 debug!("Actor stopped externally");
373 }
374 }
375 break;
376 }
377 None => {
378 debug!("Supervisor channel closed");
380 break;
381 }
382 }
383 }
384
385 event = global_events_rx.recv() => {
387 if let Some((event_actor_id, chain_event)) = event {
388 if args.events {
391 match args.events_format {
392 EventFormat::Json => {
393 println!("{}", format_event_json(&chain_event, &event_actor_id));
394 }
395 EventFormat::Short => {
396 print!("{}", format_event_short(&chain_event, &event_actor_id));
397 }
398 EventFormat::Full => {
399 print!("{}", format_event_full(&chain_event, &event_actor_id));
400 }
401 }
402 }
403
404 if event_actor_id == actor_id && chain_event.event_type == "shutdown" {
406 break;
407 }
408 }
409 }
410
411 _ = tokio::signal::ctrl_c() => {
413 debug!("Received Ctrl+C, stopping actor {}", actor_id);
414 eprintln!("\nStopping actor...");
415
416 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
417 let _ = theater_tx.send(TheaterCommand::StopActor {
418 actor_id,
419 response_tx: stop_tx,
420 }).await;
421
422 match tokio::time::timeout(
424 tokio::time::Duration::from_secs(5),
425 stop_rx,
426 ).await {
427 Ok(Ok(Ok(()))) => debug!("Actor stopped gracefully"),
428 _ => debug!("Actor stop timed out or failed"),
429 }
430 break;
431 }
432
433 _ = ctx.shutdown_token.cancelled() => {
435 debug!("Shutdown token cancelled");
436 break;
437 }
438 }
439 }
440
441 drop(theater_tx);
443
444 let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), runtime_handle).await;
446
447 Ok(())
448}