1use std::io::{self, BufRead, IsTerminal, Write};
4use std::path::PathBuf;
5use std::sync::{Arc, Mutex};
6
7use schemars::JsonSchema;
8use serde::de::DeserializeOwned;
9use serde_json::Value;
10
11use crate::protocol::*;
12use crate::schema;
13
14pub trait Plugin: Sized {
18 type Config: DeserializeOwned + JsonSchema;
25 type SystemConfig: DeserializeOwned + JsonSchema;
28
29 fn manifest() -> Manifest;
32
33 fn start(host: Host, setup: Setup<Self::Config, Self::SystemConfig>) -> Result<Self, String>;
36
37 fn call_start(&mut self, _call: CallInfo) {}
38 fn call_end(&mut self, _call: CallInfo) {}
39 fn call_concluded(&mut self, _call: ConcludedCall) {}
40 fn unit(&mut self, _event: UnitEvent) {}
41 fn audio(&mut self, _chunk: AudioChunk) {}
42 fn status(&mut self, _status: Status) {}
43 fn tone_pattern(&mut self, _t: TonePatternEvent) {}
45 fn action(&mut self, _a: Action, reply: Reply) {
49 reply.fail("This plugin has no such action");
50 }
51 fn settings(&mut self, _setup: Setup<Self::Config, Self::SystemConfig>) -> Result<(), String> {
55 Err("it can't take new settings while it runs".into())
56 }
57
58 fn shutdown(&mut self, _grace: std::time::Duration) {}
61}
62
63pub use crate::schema::NoConfig;
64
65#[derive(Clone, Debug)]
67pub struct Setup<C, S> {
68 pub config: C,
71 pub systems: Vec<System<C, S>>,
73 pub host: HostInfo,
74 pub capture_dir: PathBuf,
75 pub data_dir: PathBuf,
77 pub audio_formats: Vec<String>,
79}
80
81impl<C, S> Setup<C, S> {
82 pub fn system(&self, index: u16) -> Option<&System<C, S>> {
85 self.systems.iter().find(|s| s.index == index)
86 }
87 pub fn system_named(&self, short_name: &str) -> Option<&System<C, S>> {
90 self.systems.iter().find(|s| s.short_name == short_name)
91 }
92 pub fn has_format(&self, format: &str) -> bool {
94 self.audio_formats.iter().any(|f| f == format)
95 }
96}
97
98#[derive(Clone, Debug)]
99pub struct System<C, S> {
100 pub index: u16,
102 pub short_name: String,
104 pub kind: String,
106 pub name: String,
108 pub fcc: std::collections::BTreeMap<String, FccLicense>,
111 pub fcc_call_sign: String,
112 pub fcc_name: String,
113 pub sites: Vec<SiteInfo>,
115 pub config: Option<S>,
117 pub settings: C,
120}
121
122#[derive(Clone)]
126pub struct Host {
127 out: Arc<Mutex<Box<dyn Write + Send>>>,
128}
129
130impl Host {
131 pub fn to_writer(w: impl Write + Send + 'static) -> Host {
133 Host { out: Arc::new(Mutex::new(Box::new(w))) }
134 }
135
136 pub fn send(&self, m: &PluginMessage) {
137 let mut line = serde_json::to_string(m).unwrap_or_default();
138 line.push('\n');
139 let mut o = self.out.lock().unwrap_or_else(|e| e.into_inner());
140 let _ = o.write_all(line.as_bytes()).and_then(|_| o.flush());
142 }
143
144 pub fn log(&self, level: Level, message: impl Into<String>) {
145 self.send(&PluginMessage::Log { level, message: message.into() });
146 }
147 pub fn error(&self, message: impl Into<String>) {
148 self.log(Level::Error, message)
149 }
150 pub fn warn(&self, message: impl Into<String>) {
151 self.log(Level::Warn, message)
152 }
153 pub fn info(&self, message: impl Into<String>) {
154 self.log(Level::Info, message)
155 }
156 pub fn debug(&self, message: impl Into<String>) {
157 self.log(Level::Debug, message)
158 }
159
160 pub fn status(&self, state: State, message: impl Into<String>) {
162 self.send(&PluginMessage::Status { state, message: message.into() });
163 }
164
165 pub fn metrics(&self, m: &Metrics) {
168 self.send(&PluginMessage::Metrics(m.clone()));
169 }
170
171 pub fn call_result(&self, path: &str, outcome: Outcome, message: impl Into<String>, url: impl Into<String>) {
173 self.send(&PluginMessage::CallResult { path: path.to_string(), outcome, message: message.into(), url: url.into(), took_ms: None });
174 }
175
176 pub fn call_result_took(&self, path: &str, outcome: Outcome, message: impl Into<String>, url: impl Into<String>, took: std::time::Duration) {
180 let took_ms = Some(took.as_millis() as u64);
181 self.send(&PluginMessage::CallResult { path: path.to_string(), outcome, message: message.into(), url: url.into(), took_ms });
182 }
183}
184
185pub struct Reply {
188 host: Host,
189 request: u64,
190 sent: bool,
191}
192
193impl Reply {
194 pub fn ok(mut self, message: impl Into<String>) {
195 self.send(true, message.into());
196 }
197 pub fn fail(mut self, message: impl Into<String>) {
198 self.send(false, message.into());
199 }
200 fn send(&mut self, ok: bool, message: String) {
201 self.sent = true;
202 self.host.send(&PluginMessage::ActionResult { request: self.request, ok, message });
203 }
204}
205
206impl Drop for Reply {
207 fn drop(&mut self) {
208 if !self.sent {
209 self.send(false, "The plugin didn't answer".into());
210 }
211 }
212}
213
214pub fn describe<P: Plugin>() -> Manifest {
216 let mut m = P::manifest();
217 m.api = API_VERSION;
218 let c = schema::schema_for::<P::Config>();
219 m.config = (!schema::is_empty(&c)).then_some(c);
220 let s = schema::schema_for::<P::SystemConfig>();
221 m.system_config = (!schema::is_empty(&s)).then_some(s);
222 m
223}
224
225pub fn run<P: Plugin>() {
228 let arg = std::env::args().nth(1);
229 match arg.as_deref() {
230 Some("--describe") => {
231 println!("{}", serde_json::to_string_pretty(&describe::<P>()).unwrap_or_default());
232 return;
233 }
234 Some("--version" | "-V") => {
235 println!("{}", P::manifest().version);
236 return;
237 }
238 Some(a) => {
239 eprintln!("unknown argument {a} (a plugin takes --describe or --version; the recorder runs it with none)");
240 std::process::exit(2);
241 }
242 None => {}
243 }
244 let stdin = io::stdin();
245 if stdin.is_terminal() {
246 let m = P::manifest();
247 eprintln!(
248 "{} {} is a Trunk Recorder Pro plugin: the recorder runs it and talks to it over stdin/stdout.\n\
249 `--describe` prints its manifest. Try it with `trunk-pro plugin run <this binary>`,\n\
250 or paste protocol lines here (a hello first).",
251 m.name, m.version
252 );
253 }
254 let code = serve::<P>(stdin.lock(), Host { out: Arc::new(Mutex::new(Box::new(io::stdout()))) });
255 std::process::exit(code);
256}
257
258pub fn serve<P: Plugin>(input: impl BufRead, host: Host) -> i32 {
260 let mut plugin: Option<P> = None;
261 let mut grace = std::time::Duration::from_secs(10);
262 for line in input.lines() {
263 let Ok(line) = line else { break };
264 if line.trim().is_empty() {
265 continue;
266 }
267 let msg: HostMessage = match serde_json::from_str(&line) {
268 Ok(m) => m,
269 Err(e) => {
270 host.warn(format!("unreadable message from the recorder ({e}): {line}"));
271 continue;
272 }
273 };
274 let Some(p) = plugin.as_mut() else {
275 let HostMessage::Hello(hello) = msg else {
276 host.warn("a message before hello, ignored");
277 continue;
278 };
279 match start::<P>(host.clone(), hello) {
280 Ok(p) => {
281 plugin = Some(p);
282 host.send(&PluginMessage::Ready);
283 }
284 Err(e) => {
285 host.status(State::Error, e.clone());
286 host.error(e);
287 return EXIT_CONFIG;
288 }
289 }
290 continue;
291 };
292 match msg {
293 HostMessage::Hello(_) => host.warn("a second hello, ignored"),
294 HostMessage::CallStart(c) => p.call_start(c),
295 HostMessage::CallEnd(c) => p.call_end(c),
296 HostMessage::CallConcluded(c) => p.call_concluded(c),
297 HostMessage::Unit(u) => p.unit(u),
298 HostMessage::Audio(a) => p.audio(a),
299 HostMessage::Status(s) => p.status(s),
300 HostMessage::TonePattern(t) => p.tone_pattern(t),
301 HostMessage::Action(a) => {
302 let reply = Reply { host: host.clone(), request: a.request, sent: false };
303 p.action(a, reply)
304 }
305 HostMessage::Settings(h) => match setup::<P>(h).and_then(|s| p.settings(s)) {
306 Ok(()) => host.status(State::Ok, ""),
307 Err(e) => host.status(State::Error, format!("new settings not taken: {e}")),
308 },
309 HostMessage::Shutdown(s) => {
310 grace = std::time::Duration::from_secs_f64(s.grace_s.max(0.0));
311 break;
312 }
313 HostMessage::Unknown => {}
314 }
315 }
316 if let Some(p) = plugin.as_mut() {
317 p.shutdown(grace);
318 }
319 0
320}
321
322fn start<P: Plugin>(host: Host, hello: Hello) -> Result<P, String> {
323 P::start(host, setup::<P>(hello)?)
324}
325
326fn setup<P: Plugin>(hello: Hello) -> Result<Setup<P::Config, P::SystemConfig>, String> {
328 if hello.api != 0 && hello.api < API_VERSION {
329 return Err(format!("this plugin needs a newer recorder (plugin API {API_VERSION}, the recorder has {})", hello.api));
330 }
331 let config: P::Config = parse(hello.config.clone(), "settings")?;
332 let mut systems = Vec::new();
333 for s in hello.systems {
334 let settings = match s.settings {
335 Value::Null => parse::<P::Config>(hello.config.clone(), "settings")?,
336 v => parse::<P::Config>(v, &format!("settings for {}", s.short_name))?,
337 };
338 let config = match s.config {
339 Value::Null => None,
340 v => Some(parse::<P::SystemConfig>(v, &format!("settings for {}", s.short_name))?),
341 };
342 systems.push(System { index: s.index, short_name: s.short_name, kind: s.kind, name: s.name, fcc: s.fcc, fcc_call_sign: s.fcc_call_sign, fcc_name: s.fcc_name, sites: s.sites, config, settings });
343 }
344 Ok(Setup { config, systems, host: hello.host, capture_dir: hello.capture_dir, data_dir: hello.data_dir, audio_formats: hello.audio_formats })
345}
346
347fn parse<T: DeserializeOwned>(v: Value, what: &str) -> Result<T, String> {
349 let v = if v.is_null() { Value::Object(Default::default()) } else { v };
350 serde_json::from_value(v).map_err(|e| format!("bad {what}: {e}"))
351}
352
353#[macro_export]
362macro_rules! manifest {
363 () => {{
364 let authors: Vec<String> = env!("CARGO_PKG_AUTHORS").split(':').filter(|a| !a.is_empty()).map(String::from).collect();
365 $crate::Manifest {
366 id: env!("CARGO_PKG_NAME").to_string(),
367 name: env!("CARGO_PKG_NAME").to_string(),
368 version: env!("CARGO_PKG_VERSION").to_string(),
369 description: env!("CARGO_PKG_DESCRIPTION").to_string(),
370 api: $crate::API_VERSION,
371 homepage: env!("CARGO_PKG_HOMEPAGE").to_string(),
372 repository: env!("CARGO_PKG_REPOSITORY").to_string(),
373 authors,
374 license: env!("CARGO_PKG_LICENSE").to_string(),
375 ..Default::default()
376 }
377 }};
378}
379
380#[cfg(test)]
381mod tests {
382 use super::*;
383 use crate::testing;
384 use serde_json::json;
385
386 #[derive(serde::Deserialize, schemars::JsonSchema, Default)]
387 #[serde(default)]
388 struct Config {
389 server: String,
390 retries: u32,
391 }
392
393 struct Servers;
395
396 impl Plugin for Servers {
397 type Config = Config;
398 type SystemConfig = NoConfig;
399 fn manifest() -> Manifest {
400 Manifest { id: "servers".into(), ..Default::default() }
401 }
402 fn start(host: Host, setup: Setup<Config, NoConfig>) -> Result<Self, String> {
403 for s in &setup.systems {
404 host.info(format!("{} {} {}", s.short_name, s.settings.server, s.settings.retries));
405 }
406 Ok(Servers)
407 }
408 }
409
410 #[test]
412 fn a_system_can_override_the_plugins_settings() {
413 let dir = testing::temp_dir("sdk");
414 let mut h = testing::hello(&dir, json!({ "server": "https://a", "retries": 3 }));
415 h.systems.push(SystemInfo { index: 1, short_name: "sys2".into(), kind: "p25".into(), settings: json!({ "server": "https://b", "retries": 3 }), ..Default::default() });
416 let out = testing::run::<Servers>([HostMessage::Hello(h)]);
417 assert!(out.ready());
418 let said: Vec<String> = out.logs().into_iter().map(|(_, t)| t).collect();
419 assert_eq!(said, ["sys1 https://a 3", "sys2 https://b 3"]);
420 }
421}