Skip to main content

theater_cli/commands/
spawn.rs

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/// Output format for chain events
32#[derive(Debug, Clone, Copy, ValueEnum, Default)]
33pub enum EventFormat {
34    /// JSON format (one JSON object per line)
35    Json,
36    /// Short format (compact, one line per event)
37    #[default]
38    Short,
39    /// Full format (complete event data, multi-line)
40    Full,
41}
42
43/// Arguments shared by `theater spawn` and `theater setup`.
44#[derive(Debug, Parser)]
45pub struct SpawnArgs {
46    /// Path or URL to the actor manifest file
47    #[arg(default_value = "manifest.toml")]
48    pub manifest: String,
49
50    /// Output chain events from all actors
51    #[arg(long)]
52    pub events: bool,
53
54    /// Format for event output (used with --events)
55    #[arg(long, value_enum, default_value = "short")]
56    pub events_format: EventFormat,
57
58    /// Disable actor log output to stdout
59    #[arg(long)]
60    pub no_actor_logs: bool,
61}
62
63/// `theater setup` takes the same arguments as `theater spawn`.
64pub type SetupArgs = SpawnArgs;
65
66/// Format a chain event with actor ID prefix using ChainEvent's Display impl (short)
67fn 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
73/// Format a chain event with full data (multi-line, complete)
74fn 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
96/// Format a chain event as JSON for stdout
97fn 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
108/// Create a handler registry with all Theater handlers
109fn 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    // Runtime handler - provides log, get-chain, shutdown
116    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    // Store handler - provides content storage
123    let store_config = StoreHandlerConfig::default();
124    registry.register(StoreHandler::new(store_config, None));
125
126    // Supervisor handler - allows spawning/managing child actors
127    let supervisor_config = SupervisorHostConfig {};
128    registry.register(SupervisorHandler::new(supervisor_config, None));
129
130    // Message server handler - inter-actor messaging
131    let message_router = MessageRouter::new();
132    registry.register(MessageServerHandler::new(None, message_router.clone()));
133
134    // RPC handler - direct actor-to-actor function calls
135    registry.register(RpcHandler::new(theater_tx.clone()));
136
137    // TCP handler - TCP server/client functionality
138    let tcp_config = TcpHandlerConfig {
139        listen: None,
140        max_connections: None,
141        ..Default::default()
142    };
143    registry.register(TcpHandler::new(tcp_config));
144
145    // Terminal handler - stdin/stdout/stderr for interactive CLI apps
146    let terminal_config = TerminalHandlerConfig::default();
147    registry.register(TerminalHandler::new(terminal_config));
148
149    // Timer handler - periodic tick callbacks for game loops, polling, etc.
150    let timer_config = TimerHandlerConfig::default();
151    registry.register(TimerHandler::new(timer_config));
152
153    // Loop handler - cooperative looping with yield points
154    registry.register(LoopHandler::new());
155
156    // Podman handler - container management via the podman CLI
157    let podman_config = theater::config::actor_manifest::PodmanHandlerConfig::default();
158    registry.register(PodmanHandler::new(podman_config));
159
160    Ok(registry)
161}
162
163/// `theater spawn manifest.toml` — load the actor, set up its task loops,
164/// AND call its `theater:simple/actor.init` export before returning control
165/// to the caller. The runtime auto-inits (PR A in ticket #27); the CLI
166/// doesn't fire init itself.
167pub async fn execute_spawn(args: &SpawnArgs, ctx: &CommandContext) -> Result<(), CliError> {
168    run(args, ctx, /* call_init = */ true).await
169}
170
171/// `theater setup manifest.toml` — load the actor and set up its task loops,
172/// but do NOT call `actor.init`. Used by replay (the replay handler walks
173/// the recorded chain and fires init from there) and by callers that want
174/// to drive init themselves with custom typed params.
175pub async fn execute_setup(args: &SetupArgs, ctx: &CommandContext) -> Result<(), CliError> {
176    run(args, ctx, /* call_init = */ false).await
177}
178
179/// Shared body for `spawn` and `setup`. Differs only in which
180/// `TheaterCommand` variant it dispatches.
181async fn run(args: &SpawnArgs, ctx: &CommandContext, call_init: bool) -> Result<(), CliError> {
182    debug!("Starting actor from manifest: {}", args.manifest);
183
184    // Resolve the manifest reference (file path, URL, or store path)
185    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    // Parse the manifest first (needed to check for replay handler)
197    let manifest = ManifestConfig::from_toml_str(&manifest_content)
198        .map_err(|e| CliError::invalid_manifest(format!("Failed to parse manifest: {}", e)))?;
199
200    // Create the TheaterRuntime in-process
201    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, // no channel events forwarding needed
208        handler_registry,
209    )
210    .await
211    .map_err(|e| CliError::server_error(format!("Failed to create runtime: {}", e)))?;
212
213    // Set up global event subscription (receives events from ALL actors)
214    let (global_events_tx, mut global_events_rx) = mpsc::channel(256);
215    runtime.add_global_subscription(global_events_tx);
216
217    // Spawn the runtime event loop in a background task
218    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    // Resolve WASM path relative to manifest directory
225    let wasm_path = if manifest.package.starts_with('/') || manifest.package.contains("://") {
226        // Absolute path or URL - use as is
227        manifest.package.clone()
228    } else {
229        // Relative path - resolve relative to manifest's directory
230        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    // Load WASM bytes
242    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    // Spawn the actor
247    let (response_tx, response_rx) = tokio::sync::oneshot::channel();
248
249    // Set up a supervisor channel so we get notified when the actor exits
250    let (supervisor_tx, mut supervisor_rx) = mpsc::channel(32);
251
252    // The runtime stores `init_state` as the actor's initial state and
253    // (for SpawnActor) prepends it to the auto-fired actor.init call.
254    // For the CLI, the only place a caller can supply that state is the
255    // manifest's `initial_state` field — fall back to it here when set,
256    // otherwise use the conventional none sentinel.
257    //
258    // PR A (#58) moved this resolver out of `spawn_actor` with the intent
259    // that each caller does its own resolution; this line is the CLI's.
260    let init_state = match manifest.initial_state.as_ref() {
261        Some(s) => Value::String(s.clone()),
262        None => default_init_state(),
263    };
264
265    // SpawnActor: setup + auto-init (the runtime calls actor.init before
266    // responding). SetupActor: setup only — caller drives init separately
267    // (or a handler like ReplayHandler does it from the chain).
268    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, // Using global subscription instead
277        }
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, // Using global subscription instead
287        }
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    // Wait for the actor to start (and, for SpawnActor, for init to complete).
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    // Now wait for either:
316    // - The actor to exit (supervisor notification)
317    // - Ctrl+C
318    // - Shutdown token cancellation
319    //
320    // Output modes:
321    // - Default: print only log messages as [actor-id] message
322    // - --events: print all chain events as JSON
323    // - --chain-dir: also persist events to files
324    loop {
325        tokio::select! {
326            // Actor result (exit/error)
327            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                                    // Write actor result to stdout
335                                    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                        // Supervisor channel closed, actor is done
351                        debug!("Supervisor channel closed");
352                        break;
353                    }
354                }
355            }
356
357            // Global event subscription (all actors)
358            event = global_events_rx.recv() => {
359                if let Some((event_actor_id, chain_event)) = event {
360                    // Output events if --events mode is enabled
361                    // (Actor logs are printed directly by RuntimeHandler, not extracted here)
362                    if args.events {
363                        match args.events_format {
364                            EventFormat::Json => {
365                                println!("{}", format_event_json(&chain_event, &event_actor_id));
366                            }
367                            EventFormat::Short => {
368                                print!("{}", format_event_short(&chain_event, &event_actor_id));
369                            }
370                            EventFormat::Full => {
371                                print!("{}", format_event_full(&chain_event, &event_actor_id));
372                            }
373                        }
374                    }
375
376                    // Check for root actor shutdown
377                    if event_actor_id == actor_id && chain_event.event_type == "shutdown" {
378                        break;
379                    }
380                }
381            }
382
383            // Ctrl+C
384            _ = tokio::signal::ctrl_c() => {
385                debug!("Received Ctrl+C, stopping actor {}", actor_id);
386                eprintln!("\nStopping actor...");
387
388                let (stop_tx, stop_rx) = tokio::sync::oneshot::channel();
389                let _ = theater_tx.send(TheaterCommand::StopActor {
390                    actor_id,
391                    response_tx: stop_tx,
392                }).await;
393
394                // Wait briefly for graceful shutdown
395                match tokio::time::timeout(
396                    tokio::time::Duration::from_secs(5),
397                    stop_rx,
398                ).await {
399                    Ok(Ok(Ok(()))) => debug!("Actor stopped gracefully"),
400                    _ => debug!("Actor stop timed out or failed"),
401                }
402                break;
403            }
404
405            // Shutdown token
406            _ = ctx.shutdown_token.cancelled() => {
407                debug!("Shutdown token cancelled");
408                break;
409            }
410        }
411    }
412
413    // Drop the theater_tx to signal the runtime to stop
414    drop(theater_tx);
415
416    // Wait for runtime to finish (with timeout)
417    let _ = tokio::time::timeout(tokio::time::Duration::from_secs(5), runtime_handle).await;
418
419    Ok(())
420}