theater_cli/commands/
subscribe.rs

1use anyhow::Result;
2use clap::Parser;
3use console::style;
4use std::net::SocketAddr;
5use std::str::FromStr;
6use std::time::Duration;
7use tokio::time;
8use tracing::{debug, info};
9
10use crate::client::ManagementResponse;
11use crate::utils::event_display::{display_events, display_single_event, EventDisplayOptions};
12use crate::{error::CliError, output::formatters::EventSubscription, CommandContext};
13use theater::id::TheaterId;
14
15#[derive(Debug, Parser)]
16pub struct SubscribeArgs {
17    /// ID of the actor to subscribe to events from (use "-" to read from stdin)
18    #[arg(required = true)]
19    pub actor_id: String,
20
21    /// Address of the theater server
22    #[arg(short, long)]
23    pub address: Option<SocketAddr>,
24
25    /// Filter events by type (e.g., http.request, filesystem.read)
26    #[arg(short, long)]
27    pub event_type: Option<String>,
28
29    /// Show detailed event information
30    #[arg(short, long)]
31    pub detailed: bool,
32
33    /// Maximum number of events to show (0 for unlimited)
34    #[arg(short, long, default_value = "0")]
35    pub limit: usize,
36
37    /// Exit after timeout seconds with no events (0 for no timeout)
38    #[arg(short, long, default_value = "0")]
39    pub timeout: u64,
40
41    /// Output format (pretty, compact, json)
42    #[arg(short, long, default_value = "compact")]
43    pub format: String,
44
45    /// Show historical events before subscribing to new events
46    #[arg(short = 'H', long)]
47    pub history: bool,
48
49    /// Number of historical events to show (0 for all)
50    #[arg(long, default_value = "0")]
51    pub history_limit: usize,
52}
53
54/// Execute the subscribe command asynchronously (modernized)
55pub async fn execute_async(args: &SubscribeArgs, ctx: &CommandContext) -> Result<(), CliError> {
56    // Read actor ID from stdin if "-" is specified
57    let actor_id_str = if args.actor_id == "-" {
58        let mut input = String::new();
59        std::io::stdin().read_line(&mut input).map_err(|e| {
60            CliError::invalid_input("actor_id", "-", format!("Failed to read from stdin: {}", e))
61        })?;
62        input.trim().to_string()
63    } else {
64        args.actor_id.clone()
65    };
66
67    debug!("Subscribing to events for actor: {}", actor_id_str);
68
69    // Parse the actor ID
70    let actor_id = TheaterId::from_str(&actor_id_str)
71        .map_err(|_| CliError::invalid_actor_id(&actor_id_str))?;
72
73    // Get server address from args or config
74    let address = ctx.server_address(args.address);
75    debug!("Connecting to server at: {}", address);
76
77    // Create client and connect
78    let client = ctx.create_client();
79    client
80        .connect()
81        .await
82        .map_err(|e| CliError::connection_failed(address, e))?;
83
84    // Set up display options
85    let display_options = EventDisplayOptions {
86        format: args.format.clone(),
87        detailed: args.detailed,
88        json: ctx.json,
89    };
90
91    // Initialize event counter and subscription info
92    let mut events_count = 0;
93    let mut subscription_info = EventSubscription {
94        actor_id: actor_id.clone(),
95        address: address.to_string(),
96        event_type_filter: args.event_type.clone(),
97        limit: args.limit,
98        timeout: args.timeout,
99        format: args.format.clone(),
100        show_history: args.history,
101        history_limit: args.history_limit,
102        detailed: args.detailed,
103        events_received: 0,
104        subscription_id: None,
105        is_active: false,
106    };
107
108    // Display subscription start info
109    if !ctx.json {
110        ctx.output.output(&subscription_info, None)?;
111    }
112
113    // If history flag is set, get and display historical events
114    if args.history {
115        let mut events = client
116            .get_actor_events(&actor_id.to_string())
117            .await
118            .map_err(|e| {
119                CliError::actor_not_found(format!(
120                    "Failed to get events for actor {}: {}",
121                    actor_id, e
122                ))
123            })?;
124
125        // Apply event type filter if specified
126        if let Some(filter) = &args.event_type {
127            events.retain(|e| e.event_type.contains(filter));
128        }
129
130        // Limit the number of historical events if requested
131        if args.history_limit > 0 && events.len() > args.history_limit {
132            let skip_count = events.len() - args.history_limit;
133            events = events.into_iter().skip(skip_count).collect();
134        }
135
136        // Display historical events
137        if !events.is_empty() {
138            events_count = display_events(&events, Some(&actor_id), &display_options, 0)
139                .map_err(|e| CliError::invalid_input("event_display", "events", e.to_string()))?;
140        }
141    }
142
143    // Subscribe to the actor's events
144    let event_stream = client
145        .subscribe_to_events(&actor_id.to_string())
146        .await
147        .map_err(|e| {
148            CliError::actor_not_found(format!("Failed to subscribe to actor {}: {}", actor_id, e))
149        })?;
150
151    let subscription_id = event_stream.subscription_id();
152    subscription_info.subscription_id = Some(subscription_id.to_string());
153    subscription_info.is_active = true;
154
155    info!(
156        "Subscribed to actor events with subscription ID: {}",
157        subscription_id
158    );
159
160    // If history was not requested or no events were found, print headers
161    if !args.history || events_count == 0 {
162        if display_options.format == "compact" && !display_options.json {
163            println!(
164                "{:<12} {:<12} {:<25} {}",
165                "HASH", "PARENT", "EVENT TYPE", "DESCRIPTION"
166            );
167            println!("{}", style("─".repeat(100)).dim());
168        }
169
170        if !args.history && !ctx.json {
171            println!("{} Waiting for events...\n", style("⏳").yellow().bold());
172        }
173    }
174
175    let mut last_event_time = std::time::Instant::now();
176
177    // Listen for events
178    loop {
179        // Check for timeout if enabled
180        if args.timeout > 0 {
181            let timeout_duration = Duration::from_secs(args.timeout);
182            if last_event_time.elapsed() > timeout_duration {
183                if !ctx.json {
184                    println!(
185                        "\n{} No events received for {} seconds, exiting.",
186                        style("⏱").yellow().bold(),
187                        args.timeout
188                    );
189                }
190                break;
191            }
192        }
193
194        // Try to receive an event with a timeout
195        let response = match time::timeout(Duration::from_secs(1), client.next_response()).await {
196            Ok(result) => match result {
197                Ok(response) => response,
198                Err(e) => {
199                    if !e.to_string().contains("Connection closed") {
200                        return Err(CliError::connection_failed(address, e));
201                    }
202                    continue;
203                }
204            },
205            Err(_) => continue, // Timeout, continue to check global timeout
206        };
207
208        // Process the event
209        match response {
210            ManagementResponse::ActorEvent { event } => {
211                // Skip if doesn't match filter
212                if let Some(filter) = &args.event_type {
213                    if !event.event_type.contains(filter) {
214                        continue;
215                    }
216                }
217
218                last_event_time = std::time::Instant::now();
219                events_count += 1;
220                subscription_info.events_received = events_count;
221
222                // Display the event
223                display_single_event(
224                    &event,
225                    if display_options.json {
226                        "json"
227                    } else {
228                        &display_options.format
229                    },
230                )
231                .map_err(|e| CliError::invalid_input("event_display", "event", e.to_string()))?;
232
233                // Check if we've hit the limit
234                if args.limit > 0 && events_count >= args.limit {
235                    if !ctx.json {
236                        println!(
237                            "\n{} Reached event limit ({}), exiting.",
238                            style("ℹ").blue().bold(),
239                            args.limit
240                        );
241                    }
242                    break;
243                }
244            }
245            ManagementResponse::ActorError { error } => {
246                if ctx.json {
247                    let output = serde_json::json!({
248                        "actor_id": actor_id.to_string(),
249                        "error": error,
250                    });
251                    println!(
252                        "{}",
253                        serde_json::to_string_pretty(&output).map_err(|e| {
254                            CliError::invalid_input("json_output", "error", e.to_string())
255                        })?
256                    );
257                } else {
258                    println!("{} Actor error: {}", style("ERROR").bold().red(), error);
259                }
260            }
261            ManagementResponse::Error { error } => {
262                return Err(CliError::actor_not_found(format!(
263                    "Server error: {:?}",
264                    error
265                )));
266            }
267            _ => {
268                debug!("Received unexpected response: {:?}", response);
269            }
270        }
271    }
272
273    // Unsubscribe before exiting
274    if let Err(e) = client
275        .unsubscribe_from_actor(&actor_id.to_string(), subscription_id)
276        .await
277    {
278        debug!("Failed to unsubscribe: {}", e);
279    }
280
281    subscription_info.is_active = false;
282
283    // Final status output if not JSON
284    if !ctx.json && !args.history {
285        subscription_info.events_received = events_count;
286        // Could output final status here if desired
287    }
288
289    Ok(())
290}