use std::collections::BTreeMap;
use std::path::PathBuf;
use std::sync::Mutex;
use std::time::Duration;
use wsb::plugin::{Answer, Call, PluginService, Registration, MAX_BODY};
pub const CALL_BOUND: Duration = Duration::from_secs(30);
pub enum Lookup {
NotOffered,
Gone,
Offered(Registration),
}
#[derive(Debug)]
pub struct CallFailure(pub String);
pub struct PluginRegistry {
machine_dir: PathBuf,
registered: Mutex<BTreeMap<PluginService, Registration>>,
}
impl PluginRegistry {
pub fn new(machine_dir: PathBuf) -> Self {
Self {
machine_dir,
registered: Mutex::new(BTreeMap::new()),
}
}
pub fn register(&self, registration: Registration) -> Result<(), String> {
registration.check()?;
let enabled = wsb::plugin::enabled(&self.machine_dir)?;
if !enabled.contains(®istration.plugin) {
return Err(format!(
"plugin {} is not enabled on this machine; its owner enables it with \
`wsb plugin add {}`",
registration.plugin, registration.plugin
));
}
if !wsb::ops::process::is_alive(registration.pid) {
return Err(format!(
"plugin {} registered for process {}, which is not running",
registration.plugin, registration.pid
));
}
let mut registered = self.registered.lock().map_err(|_| "the plugin registry is poisoned")?;
registered.retain(|_, held| held.plugin != registration.plugin);
for service in ®istration.services {
registered.insert(service.clone(), registration.clone());
}
Ok(())
}
pub fn running(&self) -> Result<Vec<Registration>, String> {
let enabled = wsb::plugin::enabled(&self.machine_dir)?;
let registered = self.registered.lock().map_err(|_| "the plugin registry is poisoned")?;
let mut seen = BTreeMap::new();
for registration in registered.values() {
if enabled.contains(®istration.plugin) && wsb::ops::process::is_alive(registration.pid) {
seen.insert(registration.plugin.clone(), registration.clone());
}
}
Ok(seen.into_values().collect())
}
pub fn lookup(&self, service: &PluginService) -> Lookup {
let enabled = match wsb::plugin::enabled(&self.machine_dir) {
Ok(enabled) => enabled,
Err(error) => {
tracing::warn!(%error, "the enabled plugin list could not be read; answering for none");
return Lookup::NotOffered;
}
};
if !enabled.contains(&service.plugin()) {
return Lookup::NotOffered;
}
let Ok(registered) = self.registered.lock() else {
return Lookup::NotOffered;
};
match registered.get(service) {
None => Lookup::NotOffered,
Some(registration) if !wsb::ops::process::is_alive(registration.pid) => Lookup::Gone,
Some(registration) => Lookup::Offered(registration.clone()),
}
}
pub fn call(registration: &Registration, call: &Call) -> Result<Answer, CallFailure> {
let client = reqwest::blocking::Client::builder()
.connect_timeout(Duration::from_secs(5))
.timeout(CALL_BOUND)
.redirect(reqwest::redirect::Policy::none())
.no_proxy()
.build()
.map_err(|error| CallFailure(format!("cannot make a client for the plugin: {error}")))?;
let response = client
.post(®istration.endpoint)
.bearer_auth(®istration.token)
.json(call)
.send()
.map_err(|error| {
CallFailure(format!(
"plugin {} did not answer at {}: {error}",
registration.plugin, registration.endpoint
))
})?;
let status = response.status();
if !status.is_success() {
return Err(CallFailure(format!(
"plugin {} answered {status} at {}",
registration.plugin, registration.endpoint
)));
}
let text = response.text().map_err(|error| {
CallFailure(format!("plugin {}'s answer could not be read: {error}", registration.plugin))
})?;
if text.len() > encoded_bound() + 1024 {
return Err(CallFailure(format!(
"plugin {} answered {} bytes, more than a plugin reply may carry",
registration.plugin,
text.len()
)));
}
let answer: Answer = serde_json::from_str(&text).map_err(|error| {
CallFailure(format!("plugin {}'s answer is not one: {error}", registration.plugin))
})?;
if let Answer::Reply { body } = &answer {
if body.len() > encoded_bound() {
return Err(CallFailure(format!(
"plugin {} replied with more than a plugin reply may carry",
registration.plugin
)));
}
}
Ok(answer)
}
}
pub fn encoded_bound() -> usize {
MAX_BODY.div_ceil(3) * 4
}