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}