Skip to main content

theater_cli/commands/
start.rs

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/// Output format for chain events
34#[derive(Debug, Clone, Copy, ValueEnum, Default)]
35pub enum EventFormat {
36    /// JSON format (one JSON object per line)
37    Json,
38    /// Short format (compact, one line per event)
39    #[default]
40    Short,
41    /// Full format (complete event data, multi-line)
42    Full,
43}
44
45#[derive(Debug, Parser)]
46pub struct StartArgs {
47    /// Path or URL to the actor manifest file
48    #[arg(default_value = "manifest.toml")]
49    pub manifest: String,
50
51    /// Output chain events from all actors
52    #[arg(long)]
53    pub events: bool,
54
55    /// Format for event output (used with --events)
56    #[arg(long, value_enum, default_value = "short")]
57    pub events_format: EventFormat,
58
59    /// Directory to persist chain events (one file per actor)
60    #[arg(long)]
61    pub chain_dir: Option<PathBuf>,
62
63    /// Skip calling the actor's init function after spawning
64    #[arg(long)]
65    pub no_init: bool,
66
67    /// Disable actor log output to stdout
68    #[arg(long)]
69    pub no_actor_logs: bool,
70}
71
72/// Format a chain event with actor ID prefix using ChainEvent's Display impl (short)
73fn 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
79/// Format a chain event with full data (multi-line, complete)
80fn 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
102/// Format a chain event as JSON for stdout
103fn 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
114/// Manages chain file writers for multiple actors
115struct 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
152/// Create a handler registry with all Theater handlers
153fn create_handler_registry(
154    theater_tx: mpsc::Sender<TheaterCommand>,
155    show_actor_logs: bool,
156) -> HandlerRegistry {
157    let mut registry = HandlerRegistry::new();
158
159    // Runtime handler - provides log, get-chain, shutdown
160    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    // Store handler - provides content storage
167    let store_config = StoreHandlerConfig::default();
168    registry.register(StoreHandler::new(store_config, None));
169
170    // Supervisor handler - allows spawning/managing child actors
171    let supervisor_config = SupervisorHostConfig {};
172    registry.register(SupervisorHandler::new(supervisor_config, None));
173
174    // Message server handler - inter-actor messaging
175    let message_router = MessageRouter::new();
176    registry.register(MessageServerHandler::new(None, message_router.clone()));
177
178    // RPC handler - direct actor-to-actor function calls
179    registry.register(RpcHandler::new(theater_tx.clone()));
180
181    // TCP handler - TCP server/client functionality
182    let tcp_config = TcpHandlerConfig {
183        listen: None,
184        max_connections: None,
185        ..Default::default()
186    };
187    registry.register(TcpHandler::new(tcp_config));
188
189    // Terminal handler - stdin/stdout/stderr for interactive CLI apps
190    let terminal_config = TerminalHandlerConfig::default();
191    registry.register(TerminalHandler::new(terminal_config));
192
193    // Timer handler - periodic tick callbacks for game loops, polling, etc.
194    let timer_config = TimerHandlerConfig::default();
195    registry.register(TimerHandler::new(timer_config));
196
197    // Loop handler - cooperative looping with yield points
198    registry.register(LoopHandler::new());
199
200    registry
201}
202
203/// Execute the start command - spin up a local runtime and run the actor
204pub async fn execute_async(args: &StartArgs, ctx: &CommandContext) -> Result<(), CliError> {
205    debug!("Starting actor from manifest: {}", args.manifest);
206
207    // Resolve the manifest reference (file path, URL, or store path)
208    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    // Set up chain file manager if --chain-dir is specified
220    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    // Create the TheaterRuntime in-process
227    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, // no channel events forwarding needed
234        handler_registry,
235    )
236    .await
237    .map_err(|e| CliError::server_error(format!("Failed to create runtime: {}", e)))?;
238
239    // Set up global event subscription (receives events from ALL actors)
240    let (global_events_tx, mut global_events_rx) = mpsc::channel(256);
241    runtime.add_global_subscription(global_events_tx);
242
243    // Spawn the runtime event loop in a background task
244    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    // Parse the manifest
251    let manifest = ManifestConfig::from_toml_str(&manifest_content)
252        .map_err(|e| CliError::invalid_manifest(format!("Failed to parse manifest: {}", e)))?;
253
254    // Resolve WASM path relative to manifest directory
255    let wasm_path = if manifest.package.starts_with('/') || manifest.package.contains("://") {
256        // Absolute path or URL - use as is
257        manifest.package.clone()
258    } else {
259        // Relative path - resolve relative to manifest's directory
260        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    // Load WASM bytes
272    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    // Spawn the actor
277    let (response_tx, response_rx) = tokio::sync::oneshot::channel();
278
279    // Set up a supervisor channel so we get notified when the actor exits
280    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, // Using global subscription instead
291        })
292        .await
293        .map_err(|e| CliError::server_error(format!("Failed to send spawn command: {}", e)))?;
294
295    // Wait for the actor to start
296    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    // Call init unless --no-init flag is set
316    if !args.no_init {
317        // Get the actor handle
318        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        // Build init state (None for now)
341        let init_state = Value::Option {
342            inner_type: ValueType::List(Box::new(ValueType::U8)),
343            value: None,
344        };
345
346        // Call init
347        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    // Now wait for either:
357    // - The actor to exit (supervisor notification)
358    // - Ctrl+C
359    // - Shutdown token cancellation
360    //
361    // Output modes:
362    // - Default: print only log messages as [actor-id] message
363    // - --events: print all chain events as JSON
364    // - --chain-dir: also persist events to files
365    loop {
366        tokio::select! {
367            // Actor result (exit/error)
368            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                                    // Write actor result to stdout
376                                    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                        // Supervisor channel closed, actor is done
392                        debug!("Supervisor channel closed");
393                        break;
394                    }
395                }
396            }
397
398            // Global event subscription (all actors)
399            event = global_events_rx.recv() => {
400                if let Some((event_actor_id, event_result)) = event {
401                    match event_result {
402                        Ok(chain_event) => {
403                            // Persist to chain file if enabled
404                            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                            // Output events if --events mode is enabled
411                            // (Actor logs are printed directly by RuntimeHandler, not extracted here)
412                            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                            // Check for root actor shutdown
427                            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            // Ctrl+C
439            _ = 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                // Wait briefly for graceful shutdown
450                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            // Shutdown token
461            _ = ctx.shutdown_token.cancelled() => {
462                debug!("Shutdown token cancelled");
463                break;
464            }
465        }
466    }
467
468    // Drop the theater_tx to signal the runtime to stop
469    drop(theater_tx);
470
471    // Wait for runtime to finish (with timeout)
472    let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), runtime_handle).await;
473
474    Ok(())
475}