Skip to main content

trusty_console/detect/
analyze.rs

1//! `ServiceConnector` implementation for `trusty-analyze`.
2//!
3//! Why: trusty-analyze stores its runtime state under `~/.trusty-analyze/`.
4//! The daemon PID file is `~/.trusty-analyze/daemon.pid` and the default port
5//! is 7879. For P0 we use a written `http_addr` file in the same directory —
6//! if absent we fall back to probing the fixed default port.
7//! What: `AnalyzeConnector` implements `detect()` checking
8//! `~/.trusty-analyze/http_addr`, then falling back to `http://127.0.0.1:7879`
9//! TCP probe when the file is absent.
10//! Test: `test_analyze_connector_*` in the module below. Run with
11//! `cargo test -p trusty-console`.
12
13use std::path::PathBuf;
14
15use crate::connector::{ServiceConnector, ServiceInfo, ServiceStatus};
16
17use super::helpers::{binary_on_path, fetch_health_version, read_addr_file, tcp_probe};
18
19/// ServiceConnector for `trusty-analyze`.
20///
21/// Why: trusty-analyze stores its data under `~/.trusty-analyze/`. An
22/// `http_addr` file there (if written by the running daemon) gives the exact
23/// address; otherwise we probe the default 7879.
24/// What: Implements `detect()` checking `~/.trusty-analyze/http_addr`, then
25/// falling back to `http://127.0.0.1:7879` TCP probe when the file is absent.
26/// Test: `test_analyze_connector_with_stale_addr_file`,
27/// `test_analyze_connector_no_addr_file` below.
28pub struct AnalyzeConnector {
29    home_dir: Option<PathBuf>,
30}
31
32impl AnalyzeConnector {
33    /// Create a new `AnalyzeConnector`.
34    ///
35    /// Why: Matches the other connector constructors.
36    /// What: No-op.
37    /// Test: Created in `all_connectors()`.
38    pub fn new() -> Self {
39        Self { home_dir: None }
40    }
41
42    /// Create a connector that uses `home_dir` instead of the real home.
43    ///
44    /// Why: Unit tests must not read or write the real user's `~/.trusty-*`
45    /// directories. Injecting a temp dir keeps tests hermetic.
46    /// What: Stores `home_dir` for use in `addr_file_path()`.
47    /// Test: `test_analyze_connector_with_stale_addr_file`,
48    /// `test_analyze_connector_no_addr_file`.
49    #[cfg(test)]
50    pub fn with_home(home_dir: PathBuf) -> Self {
51        Self {
52            home_dir: Some(home_dir),
53        }
54    }
55
56    fn data_dir(&self) -> PathBuf {
57        let home = self
58            .home_dir
59            .clone()
60            .or_else(dirs::home_dir)
61            .unwrap_or_else(|| PathBuf::from("/tmp"));
62        home.join(".trusty-analyze")
63    }
64
65    fn addr_file_path(&self) -> PathBuf {
66        self.data_dir().join("http_addr")
67    }
68
69    /// Default address string for trusty-analyze.
70    ///
71    /// Why: trusty-analyze's fixed default port is 7879. Used as a fallback
72    /// when no http_addr file is found.
73    /// What: Returns the address string `"127.0.0.1:7879"`.
74    /// Test: Covered by the detect() fallback path and
75    /// `test_analyze_connector_*` tests that branch on whether the default
76    /// port has a daemon running.
77    pub(crate) fn default_addr() -> &'static str {
78        "127.0.0.1:7879"
79    }
80}
81
82impl Default for AnalyzeConnector {
83    fn default() -> Self {
84        Self::new()
85    }
86}
87
88impl ServiceConnector for AnalyzeConnector {
89    fn id(&self) -> &'static str {
90        "trusty-analyze"
91    }
92
93    fn display_name(&self) -> &'static str {
94        "Trusty Analyze"
95    }
96
97    /// Detect trusty-analyze status.
98    ///
99    /// Why: trusty-analyze stores its data under `~/.trusty-analyze/`. An
100    /// `http_addr` file there (if written by the running daemon) gives the
101    /// exact address; otherwise we probe the default 7879.
102    /// What: Binary check → addr file + TCP → fallback default-port TCP → status.
103    /// Test: `test_analyze_connector_with_stale_addr_file`,
104    /// `test_analyze_connector_no_addr_file`.
105    fn detect(&self) -> ServiceInfo {
106        // Why: this method intentionally re-implements detection rather than
107        // delegating to the shared `detect_service()` helper in helpers.rs.
108        // `detect_service()` only tries the addr-file path; it has no fallback
109        // to a well-known default port. trusty-analyze may be running on its
110        // fixed default port (7879) without having written an http_addr file
111        // (e.g. launched manually or upgraded in place). The extra
112        // default-port probe below covers that case. Do not "simplify" this
113        // by calling detect_service() — the fallback step would be silently
114        // dropped and the daemon would appear as Available when it is Running.
115        if !binary_on_path("trusty-analyze") {
116            return ServiceInfo {
117                id: self.id().to_string(),
118                display_name: self.display_name().to_string(),
119                status: ServiceStatus::Absent,
120                version: None,
121                url: None,
122                hint: None,
123            };
124        }
125
126        // Try the discovery file first.
127        if let Some(addr) = read_addr_file(&self.addr_file_path())
128            && tcp_probe(&addr)
129        {
130            let base_url = format!("http://{addr}");
131            let version = fetch_health_version(&addr);
132            return ServiceInfo {
133                id: self.id().to_string(),
134                display_name: self.display_name().to_string(),
135                status: ServiceStatus::Running,
136                version,
137                url: Some(base_url),
138                hint: None,
139            };
140        }
141
142        // Fallback: probe the well-known default port.
143        let default_addr = Self::default_addr();
144        if tcp_probe(default_addr) {
145            let base_url = format!("http://{default_addr}");
146            let version = fetch_health_version(default_addr);
147            return ServiceInfo {
148                id: self.id().to_string(),
149                display_name: self.display_name().to_string(),
150                status: ServiceStatus::Running,
151                version,
152                url: Some(base_url),
153                hint: None,
154            };
155        }
156
157        ServiceInfo {
158            id: self.id().to_string(),
159            display_name: self.display_name().to_string(),
160            status: ServiceStatus::Available,
161            version: None,
162            url: None,
163            hint: None,
164        }
165    }
166}
167
168// ─── tests ────────────────────────────────────────────────────────────────────
169
170#[cfg(test)]
171mod tests {
172    use super::super::helpers::tcp_probe;
173    use super::*;
174    use std::fs;
175    use tempfile::TempDir;
176
177    fn make_home_with_addr(rel_path: &str, content: &str) -> TempDir {
178        let tmp = TempDir::new().expect("tempdir");
179        let path = tmp.path().join(rel_path);
180        fs::create_dir_all(path.parent().expect("parent")).expect("mkdir");
181        fs::write(&path, content).expect("write addr file");
182        tmp
183    }
184
185    /// Why: stale addr file must yield Available (not Running) because TCP on
186    /// port 14996 fails. However, the AnalyzeConnector also probes the default
187    /// port 7879 as a fallback. If `trusty-analyze` is running on 7879 in the
188    /// test environment this test returns Running — which is correct behaviour.
189    /// What: creates `.trusty-analyze/http_addr = 127.0.0.1:14996` and calls
190    /// detect(); branches on whether the binary is present and the default port
191    /// is reachable, producing a deterministic assertion regardless of environment.
192    /// Test: this test itself.
193    #[test]
194    fn test_analyze_connector_with_stale_addr_file() {
195        let tmp = make_home_with_addr(".trusty-analyze/http_addr", "127.0.0.1:14996");
196        let connector = AnalyzeConnector::with_home(tmp.path().to_path_buf());
197        let info = connector.detect();
198        let binary_present = which::which("trusty-analyze").is_ok();
199        let default_running = tcp_probe(AnalyzeConnector::default_addr());
200        if !binary_present {
201            assert_eq!(info.status, ServiceStatus::Absent, "binary absent → Absent");
202        } else if default_running {
203            assert_eq!(
204                info.status,
205                ServiceStatus::Running,
206                "binary present, default port running → Running"
207            );
208        } else {
209            assert_eq!(
210                info.status,
211                ServiceStatus::Available,
212                "binary present, no running daemon → Available"
213            );
214        }
215        assert_eq!(info.id, "trusty-analyze");
216        assert_eq!(info.display_name, "Trusty Analyze");
217    }
218
219    /// Why: no addr file; result depends on whether the binary is present and
220    /// the daemon is running on the default port.
221    /// What: empty temp HOME; branches deterministically on
222    /// `which::which("trusty-analyze")` and a probe of the default port.
223    /// Test: this test itself.
224    #[test]
225    fn test_analyze_connector_no_addr_file() {
226        let tmp = TempDir::new().expect("tempdir");
227        let connector = AnalyzeConnector::with_home(tmp.path().to_path_buf());
228        let info = connector.detect();
229        let binary_present = which::which("trusty-analyze").is_ok();
230        let default_running = tcp_probe(AnalyzeConnector::default_addr());
231        if !binary_present {
232            assert_eq!(info.status, ServiceStatus::Absent, "binary absent → Absent");
233        } else if default_running {
234            assert_eq!(
235                info.status,
236                ServiceStatus::Running,
237                "binary present, default port running → Running"
238            );
239        } else {
240            assert_eq!(
241                info.status,
242                ServiceStatus::Available,
243                "binary present, no running daemon → Available"
244            );
245        }
246        assert!(
247            info.status != ServiceStatus::Absent || info.version.is_none(),
248            "Absent must have no version"
249        );
250    }
251}