1use anyhow::Result;
2use clap::{Parser, ValueEnum};
3use std::collections::HashMap;
4use std::fs::{self, OpenOptions};
5use std::io::Write;
6use std::path::PathBuf;
7use tokio::sync::mpsc;
8use tracing::{debug, error};
9
10use crate::{error::CliError, CommandContext};
11use theater::chain::ChainEvent;
12use theater::config::actor_manifest::{
13 RuntimeHostConfig, StoreHandlerConfig, SupervisorHostConfig, TcpHandlerConfig,
14 TerminalHandlerConfig, TimerHandlerConfig,
15};
16use theater::handler::HandlerRegistry;
17use theater::messages::TheaterCommand;
18use theater::pack_bridge::{Value, ValueType};
19use theater::theater_runtime::TheaterRuntime;
20use theater::utils::resolve_reference;
21use theater::ManifestConfig;
22use theater::TheaterId;
23use theater_handler_loop::LoopHandler;
24use theater_handler_message_server::{MessageRouter, MessageServerHandler};
25use theater_handler_rpc::RpcHandler;
26use theater_handler_runtime::RuntimeHandler;
27use theater_handler_store::StoreHandler;
28use theater_handler_supervisor::SupervisorHandler;
29use theater_handler_tcp::TcpHandler;
30use theater_handler_terminal::TerminalHandler;
31use theater_handler_timer::TimerHandler;
32
33#[derive(Debug, Clone, Copy, ValueEnum, Default)]
35pub enum EventFormat {
36 Json,
38 #[default]
40 Short,
41 Full,
43}
44
45#[derive(Debug, Parser)]
46pub struct StartArgs {
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 chain_dir: Option<PathBuf>,
62
63 #[arg(long)]
65 pub no_init: bool,
66
67 #[arg(long)]
69 pub no_actor_logs: bool,
70}
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
114struct ChainFileManager {
116 dir: PathBuf,
117 files: HashMap<TheaterId, std::fs::File>,
118}
119
120impl ChainFileManager {
121 fn new(dir: PathBuf) -> Result<Self, CliError> {
122 fs::create_dir_all(&dir).map_err(|e| {
123 CliError::file_operation_failed("create directory", dir.display().to_string(), e)
124 })?;
125 Ok(Self {
126 dir,
127 files: HashMap::new(),
128 })
129 }
130
131 fn write_event(&mut self, actor_id: &TheaterId, event: &ChainEvent) -> Result<(), CliError> {
132 let file = self.files.entry(*actor_id).or_insert_with(|| {
133 let path = self.dir.join(format!("{}.chain", actor_id));
134 OpenOptions::new()
135 .create(true)
136 .append(true)
137 .open(&path)
138 .expect("Failed to open chain file")
139 });
140
141 let block = format_event_full(event, actor_id);
142 file.write_all(block.as_bytes()).map_err(|e| {
143 CliError::file_operation_failed("write event", format!("{}.chain", actor_id), e)
144 })?;
145 file.flush().map_err(|e| {
146 CliError::file_operation_failed("flush", format!("{}.chain", actor_id), e)
147 })?;
148 Ok(())
149 }
150}
151
152fn create_handler_registry(
154 theater_tx: mpsc::Sender<TheaterCommand>,
155 show_actor_logs: bool,
156) -> HandlerRegistry {
157 let mut registry = HandlerRegistry::new();
158
159 let runtime_config = RuntimeHostConfig {};
161 registry.register(
162 RuntimeHandler::new(runtime_config, theater_tx.clone(), None)
163 .with_show_logs(show_actor_logs),
164 );
165
166 let store_config = StoreHandlerConfig::default();
168 registry.register(StoreHandler::new(store_config, None));
169
170 let supervisor_config = SupervisorHostConfig {};
172 registry.register(SupervisorHandler::new(supervisor_config, None));
173
174 let message_router = MessageRouter::new();
176 registry.register(MessageServerHandler::new(None, message_router.clone()));
177
178 registry.register(RpcHandler::new(theater_tx.clone()));
180
181 let tcp_config = TcpHandlerConfig {
183 listen: None,
184 max_connections: None,
185 ..Default::default()
186 };
187 registry.register(TcpHandler::new(tcp_config));
188
189 let terminal_config = TerminalHandlerConfig::default();
191 registry.register(TerminalHandler::new(terminal_config));
192
193 let timer_config = TimerHandlerConfig::default();
195 registry.register(TimerHandler::new(timer_config));
196
197 registry.register(LoopHandler::new());
199
200 registry
201}
202
203pub async fn execute_async(args: &StartArgs, ctx: &CommandContext) -> Result<(), CliError> {
205 debug!("Starting actor from manifest: {}", args.manifest);
206
207 let manifest_bytes = resolve_reference(&args.manifest).await.map_err(|e| {
209 CliError::invalid_manifest(format!(
210 "Failed to resolve manifest reference '{}': {}",
211 args.manifest, e
212 ))
213 })?;
214
215 let manifest_content = String::from_utf8(manifest_bytes).map_err(|e| {
216 CliError::invalid_manifest(format!("Manifest content is not valid UTF-8: {}", e))
217 })?;
218
219 let mut chain_file_manager = if let Some(ref dir) = args.chain_dir {
221 Some(ChainFileManager::new(dir.clone())?)
222 } else {
223 None
224 };
225
226 let (theater_tx, theater_rx) = mpsc::channel::<TheaterCommand>(32);
228 let handler_registry = create_handler_registry(theater_tx.clone(), !args.no_actor_logs);
229
230 let mut runtime = TheaterRuntime::new(
231 theater_tx.clone(),
232 theater_rx,
233 None, handler_registry,
235 )
236 .await
237 .map_err(|e| CliError::server_error(format!("Failed to create runtime: {}", e)))?;
238
239 let (global_events_tx, mut global_events_rx) = mpsc::channel(256);
241 runtime.add_global_subscription(global_events_tx);
242
243 let runtime_handle = tokio::spawn(async move {
245 if let Err(e) = runtime.run().await {
246 error!("Theater runtime error: {}", e);
247 }
248 });
249
250 let manifest = ManifestConfig::from_toml_str(&manifest_content)
252 .map_err(|e| CliError::invalid_manifest(format!("Failed to parse manifest: {}", e)))?;
253
254 let wasm_path = if manifest.package.starts_with('/') || manifest.package.contains("://") {
256 manifest.package.clone()
258 } else {
259 let manifest_path = std::path::Path::new(&args.manifest);
261 if let Some(manifest_dir) = manifest_path.parent() {
262 manifest_dir
263 .join(&manifest.package)
264 .to_string_lossy()
265 .to_string()
266 } else {
267 manifest.package.clone()
268 }
269 };
270
271 let wasm_bytes = resolve_reference(&wasm_path).await.map_err(|e| {
273 CliError::server_error(format!("Failed to load WASM from '{}': {}", wasm_path, e))
274 })?;
275
276 let (response_tx, response_rx) = tokio::sync::oneshot::channel();
278
279 let (supervisor_tx, mut supervisor_rx) = mpsc::channel(32);
281
282 theater_tx
283 .send(TheaterCommand::SpawnActor {
284 wasm_bytes,
285 name: Some(manifest.name.clone()),
286 manifest: Some(manifest),
287 init_bytes: None,
288 response_tx,
289 supervisor_tx: Some(supervisor_tx),
290 subscription_tx: None, })
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 if !args.no_init {
317 let (handle_tx, handle_rx) = tokio::sync::oneshot::channel();
319 theater_tx
320 .send(TheaterCommand::GetActorHandle {
321 actor_id,
322 response_tx: handle_tx,
323 })
324 .await
325 .map_err(|e| CliError::server_error(format!("Failed to get actor handle: {}", e)))?;
326
327 let actor_handle = match handle_rx.await {
328 Ok(Some(handle)) => handle,
329 Ok(None) => {
330 return Err(CliError::server_error("Actor handle not found".to_string()));
331 }
332 Err(e) => {
333 return Err(CliError::server_error(format!(
334 "Failed to receive actor handle: {}",
335 e
336 )));
337 }
338 };
339
340 let init_state = Value::Option {
342 inner_type: ValueType::List(Box::new(ValueType::U8)),
343 value: None,
344 };
345
346 let init_params = Value::Tuple(vec![init_state]);
348 debug!("Calling init on actor {}", actor_id);
349 let _init_result = actor_handle
350 .call_function("theater:simple/actor.init".to_string(), init_params)
351 .await
352 .map_err(|e| CliError::server_error(format!("Failed to call init: {}", e)))?;
353 debug!("Init completed");
354 }
355
356 loop {
366 tokio::select! {
367 result = supervisor_rx.recv() => {
369 match result {
370 Some(actor_result) => {
371 debug!("Actor exited: {:?}", actor_result);
372 match actor_result {
373 theater::messages::ActorResult::Success(success) => {
374 if let Some(output) = success.result {
375 let _ = std::io::stdout().write_all(&output);
377 let _ = std::io::stdout().flush();
378 }
379 }
380 theater::messages::ActorResult::Error(err) => {
381 eprintln!("Actor error: {}", err.error);
382 std::process::exit(1);
383 }
384 theater::messages::ActorResult::ExternalStop(_) => {
385 debug!("Actor stopped externally");
386 }
387 }
388 break;
389 }
390 None => {
391 debug!("Supervisor channel closed");
393 break;
394 }
395 }
396 }
397
398 event = global_events_rx.recv() => {
400 if let Some((event_actor_id, event_result)) = event {
401 match event_result {
402 Ok(chain_event) => {
403 if let Some(ref mut manager) = chain_file_manager {
405 if let Err(e) = manager.write_event(&event_actor_id, &chain_event) {
406 eprintln!("Warning: failed to write chain event: {}", e);
407 }
408 }
409
410 if args.events {
413 match args.events_format {
414 EventFormat::Json => {
415 println!("{}", format_event_json(&chain_event, &event_actor_id));
416 }
417 EventFormat::Short => {
418 print!("{}", format_event_short(&chain_event, &event_actor_id));
419 }
420 EventFormat::Full => {
421 print!("{}", format_event_full(&chain_event, &event_actor_id));
422 }
423 }
424 }
425
426 if event_actor_id == actor_id && chain_event.event_type == "shutdown" {
428 break;
429 }
430 }
431 Err(e) => {
432 debug!("Actor error event: {:?}", e);
433 }
434 }
435 }
436 }
437
438 _ = tokio::signal::ctrl_c() => {
440 debug!("Received Ctrl+C, stopping actor {}", actor_id);
441 eprintln!("\nStopping actor...");
442
443 let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
444 let _ = theater_tx.send(TheaterCommand::StopActor {
445 actor_id,
446 response_tx: stop_tx,
447 }).await;
448
449 match tokio::time::timeout(
451 tokio::time::Duration::from_secs(5),
452 stop_rx,
453 ).await {
454 Ok(Ok(Ok(()))) => debug!("Actor stopped gracefully"),
455 _ => debug!("Actor stop timed out or failed"),
456 }
457 break;
458 }
459
460 _ = ctx.shutdown_token.cancelled() => {
462 debug!("Shutdown token cancelled");
463 break;
464 }
465 }
466 }
467
468 drop(theater_tx);
470
471 let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), runtime_handle).await;
473
474 Ok(())
475}