Skip to main content

maolan_engine/plugins/
mod.rs

1pub mod clap_proc;
2pub mod ipc;
3#[cfg(all(unix, not(target_os = "macos")))]
4pub mod lv2_proc;
5pub mod types;
6pub mod vst3_proc;
7
8pub use types::*;
9
10use serde::de::DeserializeOwned;
11
12#[derive(serde::Deserialize)]
13struct ScanDiagnostic {
14    message: String,
15    plugin_uri: Option<String>,
16    plugin_name: Option<String>,
17    bundle_uri: Option<String>,
18}
19
20#[derive(serde::Deserialize)]
21struct ScanOutput<T> {
22    data: T,
23    errors: Vec<ScanDiagnostic>,
24    warnings: Vec<ScanDiagnostic>,
25}
26
27pub fn scan_plugins<T: DeserializeOwned>(format: &str) -> Result<Vec<T>, String> {
28    let host_bin = ipc::find_plugin_host_binary().ok_or("maolan-plugin-host binary not found")?;
29
30    let mut cmd = std::process::Command::new(&host_bin);
31    cmd.arg("--scan")
32        .arg("--format")
33        .arg(format)
34        .arg("--path")
35        .arg("--system");
36    ipc::append_parent_log_level(&mut cmd);
37
38    let output = cmd
39        .output()
40        .map_err(|e| format!("failed to spawn plugin-host scanner: {e}"))?;
41
42    if !output.status.success() {
43        let stderr = String::from_utf8_lossy(&output.stderr);
44        return Err(format!(
45            "plugin-host scanner exited with code {:?}: {stderr}",
46            output.status.code()
47        ));
48    }
49
50    let json = String::from_utf8_lossy(&output.stdout);
51    let parsed: ScanOutput<Vec<T>> =
52        serde_json::from_str(&json).map_err(|e| format!("failed to parse scan JSON: {e}"))?;
53
54    for error in &parsed.errors {
55        tracing::error!(
56            message = %error.message,
57            plugin_uri = ?error.plugin_uri,
58            plugin_name = ?error.plugin_name,
59            bundle_uri = ?error.bundle_uri,
60            "plugin scan error"
61        );
62    }
63    for warning in &parsed.warnings {
64        tracing::warn!(
65            message = %warning.message,
66            plugin_uri = ?warning.plugin_uri,
67            plugin_name = ?warning.plugin_name,
68            bundle_uri = ?warning.bundle_uri,
69            "plugin scan warning"
70        );
71    }
72
73    Ok(parsed.data)
74}
75
76#[cfg(test)]
77mod tests {
78    use super::ScanOutput;
79
80    #[test]
81    fn scan_output_parses_wrapper() {
82        let json = r#"{
83            "errors": [
84                {
85                    "message": "error: failed to open manifest.ttl",
86                    "bundle_uri": "file:///tmp/broken.lv2/"
87                }
88            ],
89            "warnings": [
90                {
91                    "message": "warning: duplicate version",
92                    "plugin_uri": "http://example.com/plugin"
93                }
94            ],
95            "data": [{"name": "Test", "path": "/tmp/test.clap", "capabilities": null}]
96        }"#;
97        let output: ScanOutput<Vec<serde_json::Value>> = serde_json::from_str(json).unwrap();
98        assert_eq!(output.errors.len(), 1);
99        assert_eq!(
100            output.errors[0].message,
101            "error: failed to open manifest.ttl"
102        );
103        assert_eq!(
104            output.errors[0].bundle_uri,
105            Some("file:///tmp/broken.lv2/".to_string())
106        );
107        assert_eq!(output.warnings.len(), 1);
108        assert_eq!(
109            output.warnings[0].plugin_uri,
110            Some("http://example.com/plugin".to_string())
111        );
112        assert_eq!(output.data.len(), 1);
113        assert_eq!(output.data[0]["name"], "Test");
114    }
115}