Skip to main content

omnyssh_core/ssh/
discovery.rs

1//! Service discovery orchestrator.
2//!
3//! Runs a Quick Scan to detect services on remote servers.
4//!
5//! Architecture:
6//! - Quick Scan runs once per connection, discovers which services exist
7//! - All operations are async and use the existing SSH session
8//! - Results are sent via CoreEvent to the main loop
9
10use anyhow::Result;
11use tokio::sync::mpsc;
12
13use crate::event::{CoreEvent, DetectedService};
14use crate::ssh::probe::{generate_quick_scan_script, ProbeOutput};
15use crate::ssh::services::ServiceRegistry;
16use crate::ssh::session::SshSession;
17
18/// Performs a Quick Scan on the given SSH session.
19///
20/// Quick Scan:
21/// 1. Executes the probe script (single SSH command)
22/// 2. Parses the output into sections
23/// 3. Detects which services are present using the service registry
24/// 4. Sends DiscoveryQuickScanDone event with minimal service info
25///
26/// This is fast (~2-3 seconds) and designed to run on first connection.
27///
28/// # Errors
29/// Returns an error if the SSH command fails or times out.
30/// Parsing errors are handled gracefully.
31pub async fn quick_scan(
32    session: &SshSession,
33    host_id: String,
34    tx: mpsc::Sender<CoreEvent>,
35) -> Result<()> {
36    tracing::debug!(host = %host_id, "starting quick scan");
37
38    // Execute probe script
39    let probe_script = generate_quick_scan_script();
40    let output = session.run_command(probe_script).await?;
41
42    // Parse output
43    let probe_output = ProbeOutput::parse(&output)?;
44
45    // Detect services using registry
46    let registry = ServiceRegistry::new();
47    let detected_kinds = registry.detect_services(&probe_output);
48
49    tracing::debug!(
50        host = %host_id,
51        services = ?detected_kinds,
52        "quick scan detected {} services",
53        detected_kinds.len()
54    );
55
56    // Create service info with basic quick metrics
57    let mut services = Vec::new();
58    for kind in detected_kinds {
59        // Try to extract quick metrics from probe output
60        let quick_metrics = if let Some(provider) = registry.get_provider(&kind) {
61            provider.quick_metrics(&probe_output)
62        } else {
63            Vec::new()
64        };
65
66        services.push(DetectedService {
67            kind: kind.clone(),
68            metrics: quick_metrics,
69        });
70    }
71
72    // Extract OS information and send via MetricsUpdate
73    if let Some(os_info) = probe_output.parse_os_info() {
74        use crate::event::Metrics;
75
76        // Send a partial metrics update with just OS info; the merge logic in
77        // the main loop preserves the other fields.
78        let metrics = Metrics {
79            os_info: Some(os_info),
80            ..Metrics::default()
81        };
82
83        tx.send(CoreEvent::MetricsUpdate(host_id.clone(), metrics))
84            .await
85            .ok(); // Don't fail discovery if we can't send OS info
86    }
87
88    // Send event to main loop
89    tx.send(CoreEvent::DiscoveryQuickScanDone(host_id, services))
90        .await
91        .map_err(|_| anyhow::anyhow!("event channel closed"))?;
92
93    Ok(())
94}
95
96// Note: Quick Scan is called directly from pool.rs
97// It runs in a spawned tokio task there to avoid blocking the metrics loop
98
99// ---------------------------------------------------------------------------
100// Tests
101// ---------------------------------------------------------------------------
102
103#[cfg(test)]
104mod tests {
105    use super::*;
106
107    #[test]
108    fn test_quick_scan_script_generation() {
109        let script = generate_quick_scan_script();
110        assert!(script.contains("===OMNYSSH:OS==="));
111        assert!(script.contains("===OMNYSSH:DOCKER==="));
112        assert!(script.contains("===OMNYSSH:SERVICES==="));
113    }
114
115    // Integration tests with mock SSH would go here
116    // For now, we rely on the probe and service provider unit tests
117}