Skip to main content

ready_set/
metadata.rs

1//! Plugin metadata resolution.
2//!
3//! Resolution order:
4//! 1. Cache hit → use it.
5//! 2. Sidecar manifest TOML next to the binary → parse it, cache it.
6//! 3. Spawn `<binary> __describe` with a 100 ms wall-clock timeout, parse
7//!    one line of JSON, cache it.
8//! 4. None of the above → return `None`. The dispatcher still lists the
9//!    plugin under "metadata unavailable".
10
11use std::io::Read;
12use std::process::{Command, Stdio};
13use std::time::{Duration, Instant};
14
15use ready_set_sdk::manifest::Manifest;
16
17use crate::cache::{CacheKey, PluginCache};
18use crate::discovery::PluginEntry;
19
20const DESCRIBE_TIMEOUT: Duration = Duration::from_millis(100);
21const POLL_INTERVAL: Duration = Duration::from_millis(2);
22
23/// Resolve metadata for `entry` using cache → sidecar → `__describe`.
24///
25/// Returns `None` if every method failed.
26#[must_use]
27pub fn resolve_metadata(entry: &PluginEntry, cache: &mut PluginCache) -> Option<Manifest> {
28    if let Ok(key) = CacheKey::for_binary(&entry.binary_path) {
29        if let Some(cached) = cache.get(&key) {
30            return Some(cached.clone());
31        }
32
33        if let Some(m) = entry.manifest.clone() {
34            drop(cache.insert(&key, m.clone()));
35            return Some(m);
36        }
37
38        if let Some(m) = invoke_describe(entry) {
39            drop(cache.insert(&key, m.clone()));
40            return Some(m);
41        }
42    } else if let Some(m) = entry.manifest.clone() {
43        return Some(m);
44    }
45    None
46}
47
48fn invoke_describe(entry: &PluginEntry) -> Option<Manifest> {
49    let mut child = Command::new(&entry.binary_path)
50        .arg("__describe")
51        .stdin(Stdio::null())
52        .stdout(Stdio::piped())
53        .stderr(Stdio::null())
54        .spawn()
55        .ok()?;
56
57    let start = Instant::now();
58    loop {
59        match child.try_wait() {
60            Ok(Some(status)) => {
61                if !status.success() {
62                    return None;
63                }
64                break;
65            },
66            Ok(None) => {},
67            Err(_) => return None,
68        }
69        if start.elapsed() >= DESCRIBE_TIMEOUT {
70            drop(child.kill());
71            drop(child.wait());
72            return None;
73        }
74        std::thread::sleep(POLL_INTERVAL);
75    }
76
77    let mut stdout = child.stdout.take()?;
78    let mut buf = String::new();
79    stdout.read_to_string(&mut buf).ok()?;
80    let line = buf.lines().next()?.trim();
81    let raw_value = serde_json::from_str::<serde_json::Value>(line).ok()?;
82    let manifest: Manifest = serde_json::from_value(raw_value).ok()?;
83    Some(manifest)
84}