1use linsight_core::{Category, HardwareDevice, HardwareDeviceKey, SensorId, SensorKind, Unit};
5use stabby::option::Option as SOption;
6use stabby::string::String as SString;
7use stabby::vec::Vec as SVec;
8
9use crate::mirror::{RCategory, RHardwareDevice, RSensorId, RSensorKind, RUnit};
10use crate::plugin::PluginError;
11
12pub const MIN_RATE_HZ: f32 = 0.1;
16
17pub const MAX_RATE_HZ: f32 = 20.0;
21
22#[derive(Clone, Debug, PartialEq)]
28pub struct PluginManifest {
29 pub plugin_id: String,
30 pub display_name: String,
31 pub version: String,
32 pub sensors: Vec<SensorDescriptor>,
33 pub devices: Vec<HardwareDevice>,
39}
40
41#[derive(Clone, Debug, PartialEq)]
42pub struct PluginMetadata {
43 pub plugin_id: String,
44 pub display_name: String,
45 pub version: String,
46}
47
48#[derive(Clone, Debug, PartialEq)]
49pub struct SensorDescriptor {
50 pub id: SensorId,
51 pub display_name: String,
52 pub unit: Unit,
53 pub kind: SensorKind,
54 pub category: Category,
55 pub native_rate_hz: f32,
56 pub min: Option<f64>,
57 pub max: Option<f64>,
58 pub device_id: Option<String>,
59 pub device_key: Option<HardwareDeviceKey>,
64 pub tags: Vec<String>,
66}
67
68impl SensorDescriptor {
69 pub fn clamped_rate_hz(&self) -> f32 {
71 self.native_rate_hz.clamp(MIN_RATE_HZ, MAX_RATE_HZ)
72 }
73}
74
75#[stabby::stabby]
81#[derive(Clone, Debug)]
82pub struct RSensorDescriptor {
83 pub id: RSensorId,
84 pub display_name: SString,
85 pub unit: RUnit,
86 pub kind: RSensorKind,
87 pub category: RCategory,
88 pub native_rate_hz: f32,
89 pub min: SOption<f64>,
90 pub max: SOption<f64>,
91 pub device_id: SOption<SString>,
92 pub device_key: SOption<SString>,
95 pub tags: SVec<SString>,
97}
98
99#[stabby::stabby]
100#[derive(Clone, Debug)]
101pub struct RPluginManifest {
102 pub plugin_id: SString,
103 pub display_name: SString,
104 pub version: SString,
105 pub sensors: SVec<RSensorDescriptor>,
106 pub devices: SVec<RHardwareDevice>,
108}
109
110#[stabby::stabby]
111#[derive(Clone, Debug)]
112pub struct RPluginMetadata {
113 pub plugin_id: SString,
114 pub display_name: SString,
115 pub version: SString,
116}
117
118impl From<SensorDescriptor> for RSensorDescriptor {
123 fn from(d: SensorDescriptor) -> Self {
124 Self {
125 id: d.id.into(),
126 display_name: d.display_name.as_str().into(),
127 unit: d.unit.into(),
128 kind: d.kind.into(),
129 category: d.category.into(),
130 native_rate_hz: d.native_rate_hz,
131 min: d.min.into(),
132 max: d.max.into(),
133 device_id: d.device_id.map(|s| SString::from(s.as_str())).into(),
134 device_key: d.device_key.map(|k| SString::from(k.as_str())).into(),
135 tags: d.tags.iter().map(|t| t.as_str().into()).collect::<SVec<_>>(),
136 }
137 }
138}
139
140impl From<RSensorDescriptor> for SensorDescriptor {
141 fn from(r: RSensorDescriptor) -> Self {
142 let device_id: Option<SString> = r.device_id.into();
143 let device_key: Option<HardwareDeviceKey> = Option::from(r.device_key).map(|s: SString| {
144 HardwareDeviceKey::try_new(s.as_str().to_owned()).expect("validated by host_init")
145 });
146 Self {
147 id: r.id.into(),
148 display_name: r.display_name.as_str().to_owned(),
149 unit: r.unit.into(),
150 kind: r.kind.into(),
151 category: r.category.into(),
152 native_rate_hz: r.native_rate_hz,
153 min: r.min.into(),
154 max: r.max.into(),
155 device_id: device_id.map(|s| s.as_str().to_owned()),
156 device_key,
157 tags: crate::mirror::svec_into_std(r.tags),
158 }
159 }
160}
161
162impl From<PluginManifest> for RPluginManifest {
163 fn from(m: PluginManifest) -> Self {
164 let mut sensors = SVec::with_capacity(m.sensors.len());
165 for s in m.sensors {
166 sensors.push(s.into());
167 }
168 let mut devices = SVec::with_capacity(m.devices.len());
169 for d in m.devices {
170 devices.push(d.into());
171 }
172 Self {
173 plugin_id: m.plugin_id.as_str().into(),
174 display_name: m.display_name.as_str().into(),
175 version: m.version.as_str().into(),
176 sensors,
177 devices,
178 }
179 }
180}
181
182impl From<RPluginManifest> for PluginManifest {
183 fn from(r: RPluginManifest) -> Self {
184 let sensors: Vec<SensorDescriptor> = crate::mirror::svec_into_std(r.sensors);
187 let devices: Vec<HardwareDevice> = crate::mirror::svec_into_std(r.devices);
188 Self {
189 plugin_id: r.plugin_id.as_str().to_owned(),
190 display_name: r.display_name.as_str().to_owned(),
191 version: r.version.as_str().to_owned(),
192 sensors,
193 devices,
194 }
195 }
196}
197
198impl From<PluginMetadata> for RPluginMetadata {
199 fn from(m: PluginMetadata) -> Self {
200 Self {
201 plugin_id: m.plugin_id.as_str().into(),
202 display_name: m.display_name.as_str().into(),
203 version: m.version.as_str().into(),
204 }
205 }
206}
207
208impl From<RPluginMetadata> for PluginMetadata {
209 fn from(r: RPluginMetadata) -> Self {
210 Self {
211 plugin_id: r.plugin_id.as_str().to_owned(),
212 display_name: r.display_name.as_str().to_owned(),
213 version: r.version.as_str().to_owned(),
214 }
215 }
216}
217
218pub(crate) fn validate_manifest(m: &RPluginManifest) -> Result<(), PluginError> {
234 use std::collections::HashSet;
235 let mut keys = HashSet::new();
236 for dev in m.devices.iter() {
237 let key_str = dev.key.as_str();
238 if let Err(e) = HardwareDeviceKey::try_new(key_str.to_owned()) {
239 return Err(PluginError::Manifest(format!("invalid device key {:?}: {}", key_str, e)));
240 }
241 if !keys.insert(key_str.to_owned()) {
242 return Err(PluginError::Manifest(format!(
243 "duplicate device key in manifest: {:?}",
244 key_str
245 )));
246 }
247 }
248 for s in m.sensors.iter() {
249 let opt: Option<&SString> = s.device_key.as_ref();
256 if let Some(key_s) = opt {
257 let k = key_s.as_str();
258 if !keys.contains(k) {
259 return Err(PluginError::Manifest(format!(
260 "sensor {} references device_key {:?} not in manifest.devices",
261 s.id.value.as_str(),
262 k
263 )));
264 }
265 }
266 }
267 Ok(())
268}
269
270#[cfg(test)]
271mod tests {
272 use super::*;
273
274 #[test]
275 fn sensor_descriptor_clamps_native_rate() {
276 let d = SensorDescriptor {
277 id: SensorId::new("foo"),
278 display_name: "Foo".into(),
279 unit: Unit::Percent,
280 kind: SensorKind::Scalar,
281 category: Category::Cpu,
282 native_rate_hz: 99.0,
283 min: None,
284 max: None,
285 device_id: None,
286 device_key: None,
287 tags: vec![],
288 };
289 assert_eq!(d.clamped_rate_hz(), 20.0);
290 }
291
292 #[test]
293 fn sensor_descriptor_clamps_low_rate() {
294 let d = SensorDescriptor {
295 id: SensorId::new("foo"),
296 display_name: "Foo".into(),
297 unit: Unit::Percent,
298 kind: SensorKind::Scalar,
299 category: Category::Cpu,
300 native_rate_hz: 0.001,
301 min: None,
302 max: None,
303 device_id: None,
304 device_key: None,
305 tags: vec![],
306 };
307 assert_eq!(d.clamped_rate_hz(), 0.1);
308 }
309
310 #[test]
311 fn descriptor_round_trips_through_r_mirror() {
312 let d = SensorDescriptor {
313 id: SensorId::new("net.eth0.rx_bytes"),
314 display_name: "rx".into(),
315 unit: Unit::Custom("Mb/s".into()),
316 kind: SensorKind::Counter,
317 category: Category::Network,
318 native_rate_hz: 2.0,
319 min: Some(0.0),
320 max: None,
321 device_id: Some("eth0".into()),
322 device_key: None,
323 tags: vec!["network".into()],
324 };
325 let r: RSensorDescriptor = d.clone().into();
326 let back: SensorDescriptor = r.into();
327 assert_eq!(d, back);
328 }
329
330 #[test]
331 fn manifest_round_trips_through_r_mirror() {
332 let m = PluginManifest {
333 plugin_id: "io.example".into(),
334 display_name: "Example".into(),
335 version: "1.2.3".into(),
336 sensors: vec![SensorDescriptor {
337 id: SensorId::new("ex.s"),
338 display_name: "s".into(),
339 unit: Unit::Percent,
340 kind: SensorKind::Scalar,
341 category: Category::Custom,
342 native_rate_hz: 1.0,
343 min: Some(0.0),
344 max: Some(100.0),
345 device_id: None,
346 device_key: None,
347 tags: vec![],
348 }],
349 devices: vec![],
350 };
351 let r: RPluginManifest = m.clone().into();
352 let back: PluginManifest = r.into();
353 assert_eq!(m, back);
354 }
355
356 #[test]
357 fn manifest_with_many_sensors_preserves_order() {
358 let sensors: Vec<SensorDescriptor> = (0..10)
363 .map(|i| SensorDescriptor {
364 id: SensorId::new(format!("sensor.{i}")),
365 display_name: format!("Sensor {i}"),
366 unit: Unit::Count,
367 kind: SensorKind::Scalar,
368 category: Category::Custom,
369 native_rate_hz: 1.0,
370 min: None,
371 max: None,
372 device_id: None,
373 device_key: None,
374 tags: vec![],
375 })
376 .collect();
377 let m = PluginManifest {
378 plugin_id: "io.example".into(),
379 display_name: "Example".into(),
380 version: "1.2.3".into(),
381 sensors: sensors.clone(),
382 devices: vec![],
383 };
384 let r: RPluginManifest = m.into();
385 let back: PluginManifest = r.into();
386 let back_ids: Vec<String> = back.sensors.iter().map(|s| s.id.to_string()).collect();
387 let expect_ids: Vec<String> = sensors.iter().map(|s| s.id.to_string()).collect();
388 assert_eq!(back_ids, expect_ids, "sensor order must be preserved through FFI mirror");
389 }
390
391 use crate::mirror::{RHardwareCategoryKind, RHardwareDevice};
402
403 fn make_minimal_manifest() -> RPluginManifest {
404 RPluginManifest {
405 plugin_id: "test.minimal".into(),
406 display_name: "Minimal".into(),
407 version: "0.0.1".into(),
408 sensors: SVec::new(),
409 devices: SVec::new(),
410 }
411 }
412
413 fn r_dev(key: &str, category: RHardwareCategoryKind, model: &str) -> RHardwareDevice {
414 RHardwareDevice {
415 key: key.into(),
416 category_kind: category,
417 model: model.into(),
418 vendor: SOption::None(),
419 location: SOption::None(),
420 plugin_device_id: "dev0".into(),
421 }
422 }
423
424 fn make_minimal_manifest_with_one_device() -> RPluginManifest {
425 let mut m = make_minimal_manifest();
426 let mut devs = SVec::new();
427 devs.push(r_dev("pci:0000:06:00.0", RHardwareCategoryKind::Gpu, "Arc B-series"));
428 m.devices = devs;
429 m
430 }
431
432 #[test]
433 fn host_init_rejects_invalid_device_key() {
434 let mut m = make_minimal_manifest();
435 let mut devs = SVec::new();
436 devs.push(r_dev("BAD_KEY", RHardwareCategoryKind::Gpu, "bogus"));
437 m.devices = devs;
438 let err = validate_manifest(&m).unwrap_err();
439 match err {
440 PluginError::Manifest(msg) => {
441 assert!(msg.contains("BAD_KEY"), "expected error to name the bad key; got: {msg}",);
442 }
443 other => panic!("expected PluginError::Manifest, got {other:?}"),
444 }
445 }
446
447 #[test]
448 fn host_init_rejects_sensor_pointing_at_absent_device() {
449 let mut m = make_minimal_manifest_with_one_device();
450 let mut sensors = SVec::new();
451 sensors.push(RSensorDescriptor {
452 id: RSensorId { value: "test.dangling".into() },
453 display_name: "Dangling".into(),
454 unit: crate::mirror::RUnit {
455 kind: crate::mirror::RUnitKind::Count,
456 custom: SOption::None(),
457 },
458 kind: RSensorKind::Scalar,
459 category: RCategory::Custom,
460 native_rate_hz: 1.0,
461 min: SOption::None(),
462 max: SOption::None(),
463 device_id: SOption::None(),
464 device_key: SOption::Some(SString::from("pci:0000:99:00.0")),
465 tags: SVec::new(),
466 });
467 m.sensors = sensors;
468 let err = validate_manifest(&m).unwrap_err();
469 match err {
470 PluginError::Manifest(msg) => {
471 assert!(
472 msg.contains("test.dangling") && msg.contains("pci:0000:99:00.0"),
473 "expected error to name the sensor and the dangling key; got: {msg}",
474 );
475 }
476 other => panic!("expected PluginError::Manifest, got {other:?}"),
477 }
478 }
479
480 #[test]
481 fn host_init_rejects_duplicate_device_keys_within_manifest() {
482 let mut m = make_minimal_manifest();
483 let mut devs = SVec::new();
484 devs.push(r_dev("pci:0000:06:00.0", RHardwareCategoryKind::Gpu, "first"));
485 devs.push(r_dev("pci:0000:06:00.0", RHardwareCategoryKind::Gpu, "duplicate"));
486 m.devices = devs;
487 let err = validate_manifest(&m).unwrap_err();
488 match err {
489 PluginError::Manifest(msg) => {
490 assert!(
491 msg.contains("duplicate") && msg.contains("pci:0000:06:00.0"),
492 "expected error to flag duplicate and the key; got: {msg}",
493 );
494 }
495 other => panic!("expected PluginError::Manifest, got {other:?}"),
496 }
497 }
498
499 #[test]
500 fn host_init_accepts_valid_v4_manifest_with_devices_and_sensor_reference() {
501 let mut m = make_minimal_manifest_with_one_device();
504 let mut sensors = SVec::new();
505 sensors.push(RSensorDescriptor {
506 id: RSensorId { value: "test.linked".into() },
507 display_name: "Linked".into(),
508 unit: crate::mirror::RUnit {
509 kind: crate::mirror::RUnitKind::Percent,
510 custom: SOption::None(),
511 },
512 kind: RSensorKind::Scalar,
513 category: RCategory::Gpu,
514 native_rate_hz: 1.0,
515 min: SOption::None(),
516 max: SOption::None(),
517 device_id: SOption::None(),
518 device_key: SOption::Some(SString::from("pci:0000:06:00.0")),
519 tags: SVec::new(),
520 });
521 m.sensors = sensors;
522 assert!(validate_manifest(&m).is_ok());
523 }
524}