use serde::{Deserialize, Serialize};
use std::io::{BufRead, BufReader, Write};
use std::process::{Child, ChildStdin, Command, Stdio};
use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
use std::thread::JoinHandle;
use std::time::Duration;
const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
#[derive(Debug, Serialize, Deserialize)]
pub enum HostCommand {
LoadPlugin {
path: String,
sample_rate: f64,
block_size: u32,
},
UnloadPlugin,
CreateGui,
CloseGui,
StartProcessing,
StopProcessing,
SetParameter {
id: u32,
value: f64,
},
GetParameter {
id: u32,
},
GetAllParameters,
FormatParameter {
id: u32,
normalized: f64,
},
SendMidi {
event: crate::midi::MidiEvent,
},
Process {
inputs: Vec<Vec<f32>>,
frames: u32,
},
SaveState,
LoadState {
data: Vec<u8>,
},
Shutdown,
}
#[derive(Debug, Serialize, Deserialize)]
pub enum HostResponse {
Success {
message: String,
},
Error {
message: String,
},
Crashed {
message: String,
},
AudioOutput {
outputs: Vec<Vec<f32>>,
output_midi: Vec<crate::midi::MidiEvent>,
},
ParameterValue {
value: f64,
},
ParameterString {
value: String,
},
Parameters {
params: Vec<crate::parameters::Parameter>,
},
State {
data: Vec<u8>,
},
PluginInfo {
vendor: String,
name: String,
version: String,
category: String,
uid: String,
has_gui: bool,
audio_inputs: i32,
audio_outputs: i32,
output_channels: i32,
has_midi_input: bool,
has_midi_output: bool,
},
}
pub struct PluginHostProcess {
process: Option<Child>,
stdin: Option<ChildStdin>,
responses: Receiver<String>,
reader: Option<JoinHandle<()>>,
timeout: Duration,
dead: bool,
}
impl PluginHostProcess {
pub fn new() -> Result<Self, String> {
let exe_path =
std::env::current_exe().map_err(|e| format!("Failed to get current exe: {}", e))?;
let exe_dir = exe_path.parent().ok_or("Failed to get exe directory")?;
let helper_names = ["vst3-host-helper", "vst3-inspector-helper"];
let mut helper_path = None;
for name in &helper_names {
let path = exe_dir.join(name);
if path.exists() {
helper_path = Some(path);
break;
}
}
if helper_path.is_none() && exe_dir.file_name() == Some(std::ffi::OsStr::new("examples")) {
if let Some(parent_dir) = exe_dir.parent() {
for name in &helper_names {
let path = parent_dir.join(name);
if path.exists() {
helper_path = Some(path);
break;
}
}
}
}
if helper_path.is_none() {
let mut current_dir = exe_dir;
while let Some(parent) = current_dir.parent() {
let debug_path = parent.join("target").join("debug").join("vst3-host-helper");
let release_path = parent
.join("target")
.join("release")
.join("vst3-host-helper");
if debug_path.exists() {
helper_path = Some(debug_path);
break;
} else if release_path.exists() {
helper_path = Some(release_path);
break;
}
if parent.join("Cargo.toml").exists() {
break;
}
current_dir = parent;
}
}
let helper_path = helper_path
.ok_or_else(|| format!("Helper executable not found. Searched in {:?} and parent directories. Make sure to build with --bins flag.", exe_dir))?;
let mut child = Command::new(&helper_path)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::inherit())
.spawn()
.map_err(|e| format!("Failed to spawn helper process: {}", e))?;
let stdin = child.stdin.take().ok_or("Failed to get stdin")?;
let stdout = child.stdout.take().ok_or("Failed to get stdout")?;
let (tx, rx) = mpsc::channel::<String>();
let reader = std::thread::spawn(move || {
let mut reader = BufReader::new(stdout);
let mut line = String::new();
loop {
line.clear();
match reader.read_line(&mut line) {
Ok(0) => break, Ok(_) => {
if tx.send(std::mem::take(&mut line)).is_err() {
break; }
}
Err(_) => break,
}
}
});
Ok(Self {
process: Some(child),
stdin: Some(stdin),
responses: rx,
reader: Some(reader),
timeout: DEFAULT_RESPONSE_TIMEOUT,
dead: false,
})
}
pub fn set_timeout(&mut self, timeout: Duration) {
self.timeout = timeout;
}
pub fn send_command(&mut self, command: HostCommand) -> Result<HostResponse, String> {
if self.dead {
return Err("Helper process is no longer running".to_string());
}
let command_json = serde_json::to_string(&command)
.map_err(|e| format!("Failed to serialize command: {}", e))?;
{
let stdin = self.stdin.as_mut().ok_or("No stdin available")?;
writeln!(stdin, "{}", command_json).map_err(|e| {
self.dead = true;
format!("Failed to write command (helper gone?): {}", e)
})?;
stdin.flush().map_err(|e| {
self.dead = true;
format!("Failed to flush stdin (helper gone?): {}", e)
})?;
}
match self.responses.recv_timeout(self.timeout) {
Ok(line) => {
serde_json::from_str(&line).map_err(|e| format!("Failed to parse response: {}", e))
}
Err(RecvTimeoutError::Timeout) => {
self.dead = true;
if let Some(ref mut process) = self.process {
let _ = process.kill();
}
Err(format!(
"Timed out after {:?} waiting for helper response (plugin may have hung)",
self.timeout
))
}
Err(RecvTimeoutError::Disconnected) => {
self.dead = true;
match self.check_process_status() {
Err(status) => Err(format!("Helper process crashed: {}", status)),
Ok(()) => Err("Helper process exited unexpectedly".to_string()),
}
}
}
}
pub fn is_alive(&self) -> bool {
!self.dead
}
pub fn helper_pid(&self) -> Option<u32> {
self.process.as_ref().map(|c| c.id())
}
pub fn check_process_status(&mut self) -> Result<(), String> {
if let Some(ref mut process) = self.process {
match process.try_wait() {
Ok(Some(status)) => {
if !status.success() {
return Err(format!("Helper process exited with status: {}", status));
}
}
Ok(None) => {
return Ok(());
}
Err(e) => {
return Err(format!("Failed to check process status: {}", e));
}
}
}
Ok(())
}
pub fn shutdown(&mut self) {
if !self.dead {
if let (Some(stdin), Ok(json)) = (
self.stdin.as_mut(),
serde_json::to_string(&HostCommand::Shutdown),
) {
let _ = writeln!(stdin, "{}", json);
let _ = stdin.flush();
}
}
self.stdin = None;
if let Some(mut process) = self.process.take() {
let _ = process.wait();
}
if let Some(reader) = self.reader.take() {
let _ = reader.join();
}
self.dead = true;
}
}
impl Drop for PluginHostProcess {
fn drop(&mut self) {
self.shutdown();
}
}
pub type IsolationResult<T> = std::result::Result<T, IsolationError>;
#[derive(Debug, thiserror::Error)]
pub enum IsolationError {
#[error("IO error: {0}")]
Io(#[from] std::io::Error),
#[error("Serialization error: {0}")]
Serialization(#[from] serde_json::Error),
#[error("Plugin error: {0}")]
Plugin(String),
#[error("Plugin crashed: {0}")]
Crashed(String),
#[error("Helper process not running")]
NotRunning,
#[error("Unexpected response from helper")]
UnexpectedResponse,
}
#[cfg(test)]
mod wire_tests {
use super::*;
use crate::midi::{MidiChannel, MidiEvent};
#[test]
fn audio_output_carries_midi_across_the_wire() {
let resp = HostResponse::AudioOutput {
outputs: vec![vec![0.0, 0.5], vec![-0.5, 0.0]],
output_midi: vec![
MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: 60,
velocity: 100,
},
MidiEvent::NoteOff {
channel: MidiChannel::Ch1,
note: 60,
velocity: 0,
},
],
};
let json = serde_json::to_string(&resp).expect("serialize");
let back: HostResponse = serde_json::from_str(&json).expect("deserialize");
match back {
HostResponse::AudioOutput {
outputs,
output_midi,
} => {
assert_eq!(outputs, vec![vec![0.0, 0.5], vec![-0.5, 0.0]]);
assert_eq!(output_midi.len(), 2);
assert_eq!(
output_midi[0],
MidiEvent::NoteOn {
channel: MidiChannel::Ch1,
note: 60,
velocity: 100
}
);
}
other => panic!("round-trip changed the variant: {other:?}"),
}
}
}
pub mod crash_protection {
use std::panic::catch_unwind;
use std::panic::UnwindSafe;
use std::time::Duration;
#[derive(Debug, Clone, PartialEq)]
pub enum PluginStatus {
Ok,
Crashed(String),
Timeout(Duration),
}
pub fn protected_call<F, R>(f: F) -> Result<R, String>
where
F: FnOnce() -> R + UnwindSafe,
{
catch_unwind(f).map_err(|e| {
if let Some(s) = e.downcast_ref::<&str>() {
format!("Plugin panicked: {}", s)
} else if let Some(s) = e.downcast_ref::<String>() {
format!("Plugin panicked: {}", s)
} else {
"Plugin panicked with unknown error".to_string()
}
})
}
}