1use crate::suite::Suite;
2use serde::{Deserialize, Serialize};
3use std::collections::{BTreeMap, BTreeSet};
4use std::path::{Path, PathBuf};
5
6pub const PROTOCOL_MAJOR: u32 = 1;
9pub const PROTOCOL_MINOR: u32 = 0;
10
11pub const RANK_UNKNOWN: u8 = u8::MAX;
16
17fn is_zero(v: &u64) -> bool {
18 *v == 0
19}
20
21fn rank_unknown() -> u8 {
22 RANK_UNKNOWN
23}
24
25#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
32pub struct ServiceRecord {
33 pub protocol_major: u32,
34 pub protocol_minor: u32,
35 #[serde(default)]
36 pub service: String,
37 pub role: String,
39 #[serde(default = "rank_unknown")]
41 pub rank: u8,
42 pub pid: u32,
43 pub endpoint: String,
45 #[serde(default)]
46 pub version: String,
47 #[serde(default)]
50 pub started_ms: u64,
51 #[serde(default, skip_serializing_if = "is_zero")]
55 pub process_started_ms: u64,
56 #[serde(default)]
58 pub engine_version: String,
59}
60
61impl ServiceRecord {
62 pub fn host_version(&self) -> &str {
64 if self.version.is_empty() {
65 &self.engine_version
66 } else {
67 &self.version
68 }
69 }
70}
71
72#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75pub struct TakeoverRequest {
76 pub role: String,
77 #[serde(default = "rank_unknown")]
79 pub rank: u8,
80 pub pid: u32,
81 pub requested_at_ms: u64,
82}
83
84pub fn record_path(suite: &Suite, service: &str) -> PathBuf {
85 suite.service_dir(service).join("service.json")
86}
87pub fn takeover_path(suite: &Suite, service: &str) -> PathBuf {
88 suite.service_dir(service).join("takeover.json")
89}
90
91fn read_json<T: serde::de::DeserializeOwned>(path: &Path) -> std::io::Result<Option<T>> {
97 match rightkit_fs::read_replaced(path) {
98 Ok(bytes) => Ok(serde_json::from_slice(&bytes).ok()),
99 Err(e) if e.kind() == std::io::ErrorKind::NotFound => Ok(None),
100 Err(e) => Err(e),
101 }
102}
103
104pub fn try_read_record(suite: &Suite, service: &str) -> std::io::Result<Option<ServiceRecord>> {
110 read_json(&record_path(suite, service))
111}
112
113pub fn try_read_takeover(suite: &Suite, service: &str) -> std::io::Result<Option<TakeoverRequest>> {
116 read_json(&takeover_path(suite, service))
117}
118
119pub fn read_record(suite: &Suite, service: &str) -> Option<ServiceRecord> {
122 try_read_record(suite, service).ok().flatten()
123}
124pub fn read_takeover(suite: &Suite, service: &str) -> Option<TakeoverRequest> {
127 try_read_takeover(suite, service).ok().flatten()
128}
129
130pub fn read_live_record(suite: &Suite, service: &str) -> Option<ServiceRecord> {
135 try_read_live_record(suite, service).ok().flatten()
136}
137
138pub fn try_read_live_record(
140 suite: &Suite,
141 service: &str,
142) -> std::io::Result<Option<ServiceRecord>> {
143 Ok(try_read_record(suite, service)?.filter(crate::discovery::record_process_matches))
144}
145
146pub fn write_takeover(suite: &Suite, service: &str, req: &TakeoverRequest) -> std::io::Result<()> {
151 crate::suite::make_private_dir(&suite.service_dir(service)).map_err(std::io::Error::other)?;
152 let path = takeover_path(suite, service);
153 write_atomic(&path, &serde_json::to_vec_pretty(req).unwrap_or_default())?;
154 crate::takeover_watch::poke(&path);
155 Ok(())
156}
157
158pub fn clear_takeover_if_owner(suite: &Suite, service: &str, pid: u32) {
160 if read_takeover(suite, service).is_some_and(|t| t.pid == pid) {
161 let _ = std::fs::remove_file(takeover_path(suite, service));
162 }
163}
164
165pub fn write_atomic(path: &Path, body: &[u8]) -> std::io::Result<()> {
168 let deadline = std::time::Instant::now() + std::time::Duration::from_secs(2);
169 loop {
170 match rightkit_fs::atomic_write_private(path, body) {
171 Ok(()) => return Ok(()),
172 Err(e)
176 if cfg!(windows)
177 && e.kind() == Some(std::io::ErrorKind::PermissionDenied)
178 && std::time::Instant::now() < deadline =>
179 {
180 std::thread::sleep(std::time::Duration::from_millis(10));
181 }
182 Err(rightkit_fs::FsError::Io { source, .. }) => return Err(source),
183 Err(e) => return Err(std::io::Error::other(e)),
184 }
185 }
186}
187
188#[derive(Debug, Clone, Default, PartialEq, Eq)]
192pub struct Allowlist {
193 apps: BTreeMap<String, BTreeSet<String>>,
194}
195
196impl Allowlist {
197 pub fn new() -> Self {
198 Self::default()
199 }
200 pub fn allow<I, S>(mut self, app: &str, methods: I) -> Self
201 where
202 I: IntoIterator<Item = S>,
203 S: Into<String>,
204 {
205 self.apps
206 .entry(app.to_string())
207 .or_default()
208 .extend(methods.into_iter().map(Into::into));
209 self
210 }
211 fn entry(&self, app: &str) -> Option<&BTreeSet<String>> {
212 self.apps.get(app).or_else(|| self.apps.get("*"))
213 }
214 pub fn app_known(&self, app: &str) -> bool {
215 self.entry(app).is_some()
216 }
217 pub fn permits(&self, app: &str, method: &str) -> bool {
218 method == "ping"
219 || self
220 .entry(app)
221 .is_some_and(|m| m.contains(method) || m.contains("*"))
222 }
223 pub fn methods_for(&self, app: &str) -> Vec<String> {
224 self.entry(app)
225 .map(|m| m.iter().cloned().collect())
226 .unwrap_or_default()
227 }
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233
234 fn suite() -> (tempfile::TempDir, Suite) {
235 let d = tempfile::tempdir().unwrap();
236 let s = Suite::at(d.path().join("suite")).unwrap();
237 (d, s)
238 }
239
240 fn unreadable(path: &Path) {
243 std::fs::create_dir_all(path).unwrap();
244 }
245
246 #[test]
247 fn absent_files_are_none_not_errors() {
248 let (_d, s) = suite();
249 assert!(try_read_record(&s, "demo").unwrap().is_none());
250 assert!(try_read_takeover(&s, "demo").unwrap().is_none());
251 assert!(try_read_live_record(&s, "demo").unwrap().is_none());
252 assert!(read_record(&s, "demo").is_none() && read_takeover(&s, "demo").is_none());
253 }
254
255 #[test]
256 fn unparseable_files_stay_none() {
257 let (_d, s) = suite();
258 std::fs::create_dir_all(s.service_dir("demo")).unwrap();
259 std::fs::write(record_path(&s, "demo"), b"not json").unwrap();
260 std::fs::write(takeover_path(&s, "demo"), b"{").unwrap();
261 assert!(try_read_record(&s, "demo").unwrap().is_none());
262 assert!(try_read_takeover(&s, "demo").unwrap().is_none());
263 }
264
265 #[test]
266 fn unreadable_files_are_errors_in_the_try_readers() {
267 let (_d, s) = suite();
268 unreadable(&record_path(&s, "demo"));
269 unreadable(&takeover_path(&s, "demo"));
270 let e = try_read_record(&s, "demo").unwrap_err();
271 assert_ne!(e.kind(), std::io::ErrorKind::NotFound);
272 let e = try_read_takeover(&s, "demo").unwrap_err();
273 assert_ne!(e.kind(), std::io::ErrorKind::NotFound);
274 assert!(try_read_live_record(&s, "demo").is_err());
275 assert!(read_record(&s, "demo").is_none() && read_takeover(&s, "demo").is_none());
277 clear_takeover_if_owner(&s, "demo", std::process::id());
279 assert!(takeover_path(&s, "demo").is_dir());
280 }
281}