1use linsight_core::{Reading, SensorId};
5use stabby::result::Result as SResult;
6use stabby::string::String as SString;
7use thiserror::Error;
8
9use crate::manifest::{PluginManifest, RPluginManifest};
10use crate::mirror::{RReading, RSensorId};
11
12#[derive(Debug, Error, Clone)]
17pub enum PluginError {
18 #[error("io: {0}")]
19 Io(String),
20 #[error("parse: {0}")]
21 Parse(String),
22 #[error("unsupported sensor: {0}")]
23 Unsupported(String),
24 #[error("transient: {0}")]
25 Transient(String),
26 #[error("timeout: {0}")]
32 Timeout(String),
33 #[error("manifest: {0}")]
40 Manifest(String),
41 #[error("plugin panicked: {0}")]
44 Panic(String),
45}
46
47#[stabby::stabby]
54#[repr(u8)]
55#[derive(Clone, Copy, Debug)]
56pub enum RPluginErrorKind {
57 Io,
58 Parse,
59 Unsupported,
60 Transient,
61}
62
63#[stabby::stabby]
64#[derive(Clone, Debug)]
65pub struct RPluginError {
66 pub kind: RPluginErrorKind,
67 pub message: SString,
68}
69
70impl From<PluginError> for RPluginError {
71 fn from(e: PluginError) -> Self {
72 let (kind, message) = match e {
73 PluginError::Io(s) => (RPluginErrorKind::Io, s),
74 PluginError::Parse(s) => (RPluginErrorKind::Parse, s),
75 PluginError::Unsupported(s) => (RPluginErrorKind::Unsupported, s),
76 PluginError::Transient(s) => (RPluginErrorKind::Transient, s),
77 PluginError::Timeout(s) => (RPluginErrorKind::Transient, format!("timeout: {s}")),
81 PluginError::Manifest(s) => (RPluginErrorKind::Io, format!("manifest: {s}")),
87 PluginError::Panic(s) => (RPluginErrorKind::Io, format!("plugin panicked: {s}")),
88 };
89 Self { kind, message: message.as_str().into() }
90 }
91}
92
93impl From<RPluginError> for PluginError {
94 fn from(r: RPluginError) -> Self {
95 let msg = r.message.as_str().to_owned();
96 match r.kind {
97 RPluginErrorKind::Io => PluginError::Io(msg),
98 RPluginErrorKind::Parse => PluginError::Parse(msg),
99 RPluginErrorKind::Unsupported => PluginError::Unsupported(msg),
100 RPluginErrorKind::Transient => PluginError::Transient(msg),
101 }
102 }
103}
104
105#[derive(Clone, Debug, Default)]
120pub struct PluginCtx {
121 sysroot: Option<std::path::PathBuf>,
122 config: serde_json::Value,
123}
124
125#[derive(Debug, thiserror::Error)]
126pub enum PluginCtxError {
127 #[error("sysroot path is not valid UTF-8: {0:?}")]
128 NonUtf8Sysroot(std::path::PathBuf),
129}
130
131impl PluginCtx {
132 pub fn new_with_sysroot(path: std::path::PathBuf) -> Result<Self, PluginCtxError> {
133 if path.to_str().is_none() {
134 return Err(PluginCtxError::NonUtf8Sysroot(path));
135 }
136 Ok(Self { sysroot: Some(path), config: serde_json::Value::Null })
137 }
138
139 pub fn sysroot(&self) -> Option<&std::path::Path> {
140 self.sysroot.as_deref()
141 }
142
143 pub fn config(&self) -> &serde_json::Value {
144 &self.config
145 }
146
147 pub fn with_config(mut self, config: serde_json::Value) -> Self {
148 self.config = config;
149 self
150 }
151}
152
153#[stabby::stabby]
164#[derive(Clone, Debug, Default)]
165pub struct RPluginCtx {
166 pub sysroot: SString,
167 pub sysroot_set: u8,
168 pub config_json: SString,
169}
170
171impl From<&PluginCtx> for RPluginCtx {
172 fn from(ctx: &PluginCtx) -> Self {
173 let sysroot = match &ctx.sysroot {
174 Some(p) => {
175 let s: SString = p.to_str().expect("PluginCtx invariant: sysroot is UTF-8").into();
176 (s, 1u8)
177 }
178 None => (SString::default(), 0u8),
179 };
180 let config_json = match &ctx.config {
181 serde_json::Value::Null => SString::default(),
182 v => v.to_string().into(),
183 };
184 Self { sysroot: sysroot.0, sysroot_set: sysroot.1, config_json }
185 }
186}
187
188impl From<&RPluginCtx> for PluginCtx {
189 fn from(r: &RPluginCtx) -> Self {
190 let sysroot = if r.sysroot_set != 0 {
191 Some(std::path::PathBuf::from(r.sysroot.as_str()))
192 } else {
193 None
194 };
195 let config = if r.config_json.as_str().is_empty() {
196 serde_json::Value::Null
197 } else {
198 serde_json::from_str(r.config_json.as_str()).unwrap_or(serde_json::Value::Null)
199 };
200 Self { sysroot, config }
201 }
202}
203
204pub type RInitResult = SResult<RPluginManifest, RPluginError>;
219pub type RSampleResult = SResult<RReading, RPluginError>;
220
221#[stabby::stabby]
222pub trait LinsightPlugin: Send + Sync {
223 extern "C-unwind" fn init(&self, ctx: &RPluginCtx) -> RInitResult;
224 extern "C-unwind" fn sample(&self, sensor: RSensorId) -> RSampleResult;
225 extern "C-unwind" fn shutdown(&self) {}
226}
227
228#[must_use = "host_init returns Result<PluginManifest, PluginError>; ignoring the manifest discards the plugin's sensor catalogue"]
243pub fn host_init(
244 plugin: &dyn LinsightPlugin,
245 ctx: &PluginCtx,
246) -> Result<PluginManifest, PluginError> {
247 let rctx: RPluginCtx = ctx.into();
248 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plugin.init(&rctx)))
249 .map_err(|_| PluginError::Panic("plugin init panicked".into()))?;
250 let std_res: core::result::Result<RPluginManifest, RPluginError> = r.into();
251 let r_manifest = std_res.map_err(PluginError::from)?;
252 let plugin_id_for_err = r_manifest.plugin_id.as_str().to_owned();
259 for i in 0..r_manifest.sensors.len() {
260 let raw = r_manifest.sensors.as_slice()[i].id.value.as_str();
263 SensorId::try_new(raw).map_err(|e| {
264 PluginError::Parse(format!(
265 "plugin `{plugin_id_for_err}` returned invalid sensor id `{raw}`: {e}",
266 ))
267 })?;
268 }
269 crate::manifest::validate_manifest(&r_manifest)?;
275 Ok(r_manifest.into())
276}
277
278#[must_use = "host_sample returns the sampled Reading; ignoring it drops the value the plugin produced"]
281pub fn host_sample(plugin: &dyn LinsightPlugin, id: &SensorId) -> Result<Reading, PluginError> {
282 let rid: RSensorId = id.into();
283 let r = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| plugin.sample(rid)))
284 .map_err(|_| PluginError::Panic("plugin sample panicked".into()))?;
285 let std_res: core::result::Result<RReading, RPluginError> = r.into();
286 match std_res {
287 Ok(r) => Ok(r.into()),
288 Err(e) => Err(e.into()),
289 }
290}
291
292#[cfg(test)]
293mod tests {
294 use linsight_core::SensorId;
295 use stabby::result::Result as SResult;
296
297 use super::*;
298 use crate::manifest::{PluginManifest, RPluginManifest};
299
300 struct NoopPlugin;
301
302 impl LinsightPlugin for NoopPlugin {
303 extern "C-unwind" fn init(&self, _ctx: &RPluginCtx) -> RInitResult {
304 let m = PluginManifest {
305 plugin_id: "test".into(),
306 display_name: "Test".into(),
307 version: "0.0.1".into(),
308 sensors: vec![],
309 devices: vec![],
310 };
311 let r: RPluginManifest = m.into();
312 SResult::Ok(r)
313 }
314
315 extern "C-unwind" fn sample(&self, _sensor: RSensorId) -> RSampleResult {
316 let e: RPluginError = PluginError::Unsupported("no sensors".into()).into();
317 SResult::Err(e)
318 }
319 }
320
321 #[test]
322 fn noop_plugin_init_runs() {
323 let p = NoopPlugin;
324 let m = host_init(&p, &PluginCtx::default()).unwrap();
325 assert_eq!(m.plugin_id, "test");
326 }
327
328 #[test]
329 fn noop_plugin_sample_errors() {
330 let p = NoopPlugin;
331 let err = host_sample(&p, &SensorId::new("foo")).unwrap_err();
332 assert!(matches!(err, PluginError::Unsupported(_)));
333 }
334
335 struct BadIdPlugin;
341
342 impl LinsightPlugin for BadIdPlugin {
343 extern "C-unwind" fn init(&self, _ctx: &RPluginCtx) -> RInitResult {
344 let bad: SString = "bad id".into(); let r_desc = crate::manifest::RSensorDescriptor {
350 id: RSensorId { value: bad },
351 display_name: "Bad".into(),
352 unit: crate::mirror::RUnit {
353 kind: crate::mirror::RUnitKind::Count,
354 custom: stabby::option::Option::None(),
355 },
356 kind: crate::mirror::RSensorKind::Scalar,
357 category: crate::mirror::RCategory::Custom,
358 native_rate_hz: 1.0,
359 min: stabby::option::Option::None(),
360 max: stabby::option::Option::None(),
361 device_id: stabby::option::Option::None(),
362 device_key: stabby::option::Option::None(),
363 tags: stabby::vec::Vec::new(),
364 };
365 let mut sensors = stabby::vec::Vec::new();
366 sensors.push(r_desc);
367 let r = RPluginManifest {
368 plugin_id: "test.bad".into(),
369 display_name: "Bad".into(),
370 version: "0.0.1".into(),
371 sensors,
372 devices: stabby::vec::Vec::new(),
373 };
374 SResult::Ok(r)
375 }
376
377 extern "C-unwind" fn sample(&self, _: RSensorId) -> RSampleResult {
378 let e: RPluginError = PluginError::Unsupported("no".into()).into();
379 SResult::Err(e)
380 }
381 }
382
383 #[test]
384 fn host_init_rejects_plugin_with_invalid_sensor_id() {
385 let p = BadIdPlugin;
390 let err = host_init(&p, &PluginCtx::default()).unwrap_err();
391 match err {
392 PluginError::Parse(msg) => {
393 assert!(
394 msg.contains("bad id") || msg.contains("whitespace"),
395 "expected error to name the bad id or invariant; got: {msg}",
396 );
397 }
398 other => panic!("expected PluginError::Parse, got {other:?}"),
399 }
400 }
401
402 struct PanicPlugin;
408
409 impl LinsightPlugin for PanicPlugin {
410 extern "C-unwind" fn init(&self, _ctx: &RPluginCtx) -> RInitResult {
411 panic!("boom in init");
412 }
413
414 extern "C-unwind" fn sample(&self, _: RSensorId) -> RSampleResult {
415 panic!("boom in sample");
416 }
417 }
418
419 #[test]
420 fn host_init_catches_plugin_panic() {
421 let err = host_init(&PanicPlugin, &PluginCtx::default()).unwrap_err();
422 assert!(matches!(err, PluginError::Panic(_)), "expected Panic, got {err:?}");
423 }
424
425 #[test]
426 fn host_sample_catches_plugin_panic() {
427 let err = host_sample(&PanicPlugin, &SensorId::new("x")).unwrap_err();
428 assert!(matches!(err, PluginError::Panic(_)), "expected Panic, got {err:?}");
429 }
430
431 #[test]
432 fn plugin_ctx_rejects_non_utf8_sysroot() {
433 use std::os::unix::ffi::OsStringExt;
437 let bytes: Vec<u8> = vec![b'/', 0xff, 0xfe, b'/', b'x'];
438 let bad = std::path::PathBuf::from(std::ffi::OsString::from_vec(bytes));
439 let err = PluginCtx::new_with_sysroot(bad).unwrap_err();
440 assert!(matches!(err, PluginCtxError::NonUtf8Sysroot(_)));
441 }
442}