1use serde::{Deserialize, Serialize};
7use std::io::{BufRead, BufReader, Write};
8use std::process::{Child, ChildStdin, Command, Stdio};
9use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
10use std::thread::JoinHandle;
11use std::time::Duration;
12
13pub(crate) const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
15
16#[derive(Debug, Clone, Serialize, Deserialize)]
22pub enum HostCommand {
23 LoadPlugin {
25 path: String,
27 sample_rate: f64,
29 block_size: u32,
31 tempo: f64,
33 time_sig_numerator: i32,
35 time_sig_denominator: i32,
37 },
38 UnloadPlugin,
40 CreateGui,
42 CloseGui,
44 StartProcessing,
46 StopProcessing,
48 SetParameter {
50 id: u32,
52 value: f64,
54 },
55 SetParameterAt {
57 id: u32,
59 value: f64,
61 offset: i32,
63 },
64 GetParameter {
66 id: u32,
68 },
69 GetAllParameters,
71 FormatParameter {
73 id: u32,
75 normalized: f64,
77 },
78 SendMidi {
80 event: crate::midi::MidiEvent,
82 },
83 Process {
85 inputs: Vec<Vec<f32>>,
87 frames: u32,
89 },
90 SaveState,
92 LoadState {
94 data: Vec<u8>,
96 },
97 NoteOn {
100 channel: u8,
102 note: u8,
104 velocity: u8,
106 sample_offset: i32,
108 },
109 NoteOff {
111 note_id: i32,
113 sample_offset: i32,
115 },
116 SendNoteExpression {
119 note_id: i32,
121 kind: crate::midi::NoteExpressionType,
123 value: f64,
125 sample_offset: i32,
127 },
128 NoteExpressions {
130 bus: i32,
132 channel: i16,
134 },
135 Shutdown,
137}
138
139#[derive(Debug, Serialize, Deserialize)]
141pub enum HostResponse {
142 Success {
144 message: String,
146 },
147 Error {
149 message: String,
151 },
152 Crashed {
154 message: String,
156 },
157 AudioOutput {
160 outputs: Vec<Vec<f32>>,
162 output_midi: Vec<crate::midi::MidiEvent>,
164 },
165 ParameterValue {
167 value: f64,
169 },
170 ParameterString {
172 value: String,
174 },
175 Parameters {
177 params: Vec<crate::parameters::Parameter>,
179 },
180 State {
182 data: Vec<u8>,
184 },
185 GuiCreated {
188 width: i32,
190 height: i32,
192 },
193 PluginInfo {
195 vendor: String,
197 name: String,
199 version: String,
201 category: String,
203 uid: String,
205 has_gui: bool,
207 audio_inputs: i32,
209 audio_outputs: i32,
211 output_channels: i32,
213 has_midi_input: bool,
215 has_midi_output: bool,
217 },
218 NoteStarted {
220 note_id: i32,
222 },
223 NoteExpressions {
225 expressions: Vec<crate::midi::NoteExpressionInfo>,
227 },
228}
229
230pub struct PluginHostProcess {
237 process: Option<Child>,
238 stdin: Option<ChildStdin>,
239 responses: Receiver<String>,
241 reader: Option<JoinHandle<()>>,
243 timeout: Duration,
245 dead: bool,
247}
248
249impl PluginHostProcess {
250 pub fn new(
252 helper_override: Option<std::path::PathBuf>,
253 timeout: Duration,
254 ) -> Result<Self, String> {
255 let override_path = helper_override
258 .or_else(|| std::env::var_os("VST3_HOST_HELPER_PATH").map(std::path::PathBuf::from));
259 if let Some(p) = override_path {
260 if !p.exists() {
261 return Err(format!(
262 "Configured helper path does not exist: {}",
263 p.display()
264 ));
265 }
266 return Self::spawn(p, timeout);
267 }
268
269 let exe_path =
271 std::env::current_exe().map_err(|e| format!("Failed to get current exe: {}", e))?;
272
273 let exe_dir = exe_path.parent().ok_or("Failed to get exe directory")?;
274
275 let helper_names = ["vst3-host-helper", "vst3-inspector-helper"];
277 let mut helper_path = None;
278
279 for name in &helper_names {
281 let path = exe_dir.join(name);
282 if path.exists() {
283 helper_path = Some(path);
284 break;
285 }
286 }
287
288 if helper_path.is_none() && exe_dir.file_name() == Some(std::ffi::OsStr::new("examples")) {
290 if let Some(parent_dir) = exe_dir.parent() {
291 for name in &helper_names {
292 let path = parent_dir.join(name);
293 if path.exists() {
294 helper_path = Some(path);
295 break;
296 }
297 }
298 }
299 }
300
301 if helper_path.is_none() {
303 let mut current_dir = exe_dir;
305 while let Some(parent) = current_dir.parent() {
306 let debug_path = parent.join("target").join("debug").join("vst3-host-helper");
307 let release_path = parent
308 .join("target")
309 .join("release")
310 .join("vst3-host-helper");
311
312 if debug_path.exists() {
313 helper_path = Some(debug_path);
314 break;
315 } else if release_path.exists() {
316 helper_path = Some(release_path);
317 break;
318 }
319
320 if parent.join("Cargo.toml").exists() {
322 break;
323 }
324 current_dir = parent;
325 }
326 }
327
328 let helper_path = helper_path
329 .ok_or_else(|| format!("Helper executable not found. Searched in {:?} and parent directories. Make sure to build with --bins flag.", exe_dir))?;
330
331 Self::spawn(helper_path, timeout)
332 }
333
334 fn spawn(helper_path: std::path::PathBuf, timeout: Duration) -> Result<Self, String> {
336 let mut child = Command::new(&helper_path)
337 .stdin(Stdio::piped())
338 .stdout(Stdio::piped())
339 .stderr(Stdio::inherit())
340 .spawn()
341 .map_err(|e| format!("Failed to spawn helper process: {}", e))?;
342
343 let stdin = child.stdin.take().ok_or("Failed to get stdin")?;
344 let stdout = child.stdout.take().ok_or("Failed to get stdout")?;
345
346 let (tx, rx) = mpsc::channel::<String>();
350 let reader = std::thread::spawn(move || {
351 let mut reader = BufReader::new(stdout);
352 let mut line = String::new();
353 loop {
354 line.clear();
355 match reader.read_line(&mut line) {
356 Ok(0) => break, Ok(_) => {
358 if tx.send(std::mem::take(&mut line)).is_err() {
359 break; }
361 }
362 Err(_) => break,
363 }
364 }
365 });
366
367 Ok(Self {
368 process: Some(child),
369 stdin: Some(stdin),
370 responses: rx,
371 reader: Some(reader),
372 timeout,
373 dead: false,
374 })
375 }
376
377 pub fn set_timeout(&mut self, timeout: Duration) {
379 self.timeout = timeout;
380 }
381
382 pub fn send_command(&mut self, command: HostCommand) -> Result<HostResponse, String> {
387 if self.dead {
388 return Err("Helper process is no longer running".to_string());
389 }
390
391 let command_json = serde_json::to_string(&command)
392 .map_err(|e| format!("Failed to serialize command: {}", e))?;
393
394 {
395 let stdin = self.stdin.as_mut().ok_or("No stdin available")?;
396 writeln!(stdin, "{}", command_json).map_err(|e| {
397 self.dead = true;
398 format!("Failed to write command (helper gone?): {}", e)
399 })?;
400 stdin.flush().map_err(|e| {
401 self.dead = true;
402 format!("Failed to flush stdin (helper gone?): {}", e)
403 })?;
404 }
405
406 match self.responses.recv_timeout(self.timeout) {
407 Ok(line) => {
408 serde_json::from_str(&line).map_err(|e| format!("Failed to parse response: {}", e))
409 }
410 Err(RecvTimeoutError::Timeout) => {
411 self.dead = true;
413 if let Some(ref mut process) = self.process {
414 let _ = process.kill();
415 }
416 Err(format!(
417 "Timed out after {:?} waiting for helper response (plugin may have hung)",
418 self.timeout
419 ))
420 }
421 Err(RecvTimeoutError::Disconnected) => {
422 self.dead = true;
424 match self.check_process_status() {
425 Err(status) => Err(format!("Helper process crashed: {}", status)),
426 Ok(()) => Err("Helper process exited unexpectedly".to_string()),
427 }
428 }
429 }
430 }
431
432 pub fn is_alive(&self) -> bool {
434 !self.dead
435 }
436
437 pub fn helper_pid(&self) -> Option<u32> {
440 self.process.as_ref().map(|c| c.id())
441 }
442
443 pub fn check_process_status(&mut self) -> Result<(), String> {
445 if let Some(ref mut process) = self.process {
446 match process.try_wait() {
447 Ok(Some(status)) => {
448 if !status.success() {
449 return Err(format!("Helper process exited with status: {}", status));
450 }
451 }
452 Ok(None) => {
453 return Ok(());
455 }
456 Err(e) => {
457 return Err(format!("Failed to check process status: {}", e));
458 }
459 }
460 }
461 Ok(())
462 }
463
464 pub fn shutdown(&mut self) {
466 if !self.dead {
469 if let (Some(stdin), Ok(json)) = (
470 self.stdin.as_mut(),
471 serde_json::to_string(&HostCommand::Shutdown),
472 ) {
473 let _ = writeln!(stdin, "{}", json);
474 let _ = stdin.flush();
475 }
476 }
477
478 self.stdin = None;
481
482 if let Some(mut process) = self.process.take() {
483 let deadline = std::time::Instant::now() + Duration::from_secs(2);
487 loop {
488 match process.try_wait() {
489 Ok(Some(_)) => break,
490 Ok(None) if std::time::Instant::now() >= deadline => {
491 let _ = process.kill();
492 let _ = process.wait();
493 break;
494 }
495 Ok(None) => std::thread::sleep(Duration::from_millis(10)),
496 Err(_) => {
497 let _ = process.kill();
498 break;
499 }
500 }
501 }
502 }
503 if let Some(reader) = self.reader.take() {
504 let _ = reader.join();
505 }
506 self.dead = true;
507 }
508}
509
510impl Drop for PluginHostProcess {
511 fn drop(&mut self) {
512 self.shutdown();
513 }
514}
515
516pub type IsolationResult<T> = std::result::Result<T, IsolationError>;
518
519#[derive(Debug, thiserror::Error)]
521pub enum IsolationError {
522 #[error("IO error: {0}")]
524 Io(#[from] std::io::Error),
525
526 #[error("Serialization error: {0}")]
528 Serialization(#[from] serde_json::Error),
529
530 #[error("Plugin error: {0}")]
532 Plugin(String),
533
534 #[error("Plugin crashed: {0}")]
536 Crashed(String),
537
538 #[error("Helper process not running")]
540 NotRunning,
541
542 #[error("Unexpected response from helper")]
544 UnexpectedResponse,
545}
546
547#[cfg(test)]
548mod wire_tests {
549 use super::*;
550 use crate::midi::{MidiChannel, MidiEvent};
551
552 #[test]
553 fn audio_output_carries_midi_across_the_wire() {
554 let resp = HostResponse::AudioOutput {
557 outputs: vec![vec![0.0, 0.5], vec![-0.5, 0.0]],
558 output_midi: vec![
559 MidiEvent::NoteOn {
560 channel: MidiChannel::Ch1,
561 note: 60,
562 velocity: 100,
563 },
564 MidiEvent::NoteOff {
565 channel: MidiChannel::Ch1,
566 note: 60,
567 velocity: 0,
568 },
569 ],
570 };
571 let json = serde_json::to_string(&resp).expect("serialize");
572 let back: HostResponse = serde_json::from_str(&json).expect("deserialize");
573 match back {
574 HostResponse::AudioOutput {
575 outputs,
576 output_midi,
577 } => {
578 assert_eq!(outputs, vec![vec![0.0, 0.5], vec![-0.5, 0.0]]);
579 assert_eq!(output_midi.len(), 2);
580 assert_eq!(
581 output_midi[0],
582 MidiEvent::NoteOn {
583 channel: MidiChannel::Ch1,
584 note: 60,
585 velocity: 100
586 }
587 );
588 }
589 other => panic!("round-trip changed the variant: {other:?}"),
590 }
591 }
592
593 #[test]
594 fn state_commands_round_trip_across_the_wire() {
595 let blob: Vec<u8> = vec![0, 1, 2, 250, 255, 42];
597
598 let save = serde_json::to_string(&HostCommand::SaveState).expect("serialize SaveState");
599 assert!(matches!(
600 serde_json::from_str::<HostCommand>(&save).expect("deserialize SaveState"),
601 HostCommand::SaveState
602 ));
603
604 let load = HostCommand::LoadState { data: blob.clone() };
605 let load_json = serde_json::to_string(&load).expect("serialize LoadState");
606 match serde_json::from_str::<HostCommand>(&load_json).expect("deserialize LoadState") {
607 HostCommand::LoadState { data } => assert_eq!(data, blob),
608 other => panic!("LoadState round-trip changed the variant: {other:?}"),
609 }
610
611 let state = HostResponse::State { data: blob.clone() };
612 let state_json = serde_json::to_string(&state).expect("serialize State");
613 match serde_json::from_str::<HostResponse>(&state_json).expect("deserialize State") {
614 HostResponse::State { data } => assert_eq!(data, blob),
615 other => panic!("State round-trip changed the variant: {other:?}"),
616 }
617 }
618
619 #[test]
620 fn set_parameter_at_round_trips_across_the_wire() {
621 let cmd = HostCommand::SetParameterAt {
624 id: 42,
625 value: 0.75,
626 offset: 256,
627 };
628 let json = serde_json::to_string(&cmd).expect("serialize SetParameterAt");
629 match serde_json::from_str::<HostCommand>(&json).expect("deserialize SetParameterAt") {
630 HostCommand::SetParameterAt { id, value, offset } => {
631 assert_eq!(id, 42);
632 assert_eq!(value, 0.75);
633 assert_eq!(offset, 256);
634 }
635 other => panic!("round-trip changed the variant: {other:?}"),
636 }
637 }
638
639 #[test]
640 fn note_expression_commands_round_trip_across_the_wire() {
641 use crate::midi::{NoteExpressionInfo, NoteExpressionType};
643
644 let on = HostCommand::NoteOn {
645 channel: 0,
646 note: 60,
647 velocity: 100,
648 sample_offset: 0,
649 };
650 let on_json = serde_json::to_string(&on).expect("serialize NoteOn");
651 match serde_json::from_str::<HostCommand>(&on_json).expect("deserialize NoteOn") {
652 HostCommand::NoteOn {
653 channel,
654 note,
655 velocity,
656 sample_offset,
657 } => {
658 assert_eq!((channel, note, velocity, sample_offset), (0, 60, 100, 0));
659 }
660 other => panic!("NoteOn round-trip changed the variant: {other:?}"),
661 }
662
663 let expr = HostCommand::SendNoteExpression {
664 note_id: 7,
665 kind: NoteExpressionType::Tuning,
666 value: 1.0,
667 sample_offset: 0,
668 };
669 let expr_json = serde_json::to_string(&expr).expect("serialize SendNoteExpression");
670 match serde_json::from_str::<HostCommand>(&expr_json).expect("deserialize") {
671 HostCommand::SendNoteExpression {
672 note_id,
673 kind,
674 value,
675 ..
676 } => {
677 assert_eq!(note_id, 7);
678 assert_eq!(kind, NoteExpressionType::Tuning);
679 assert_eq!(value, 1.0);
680 }
681 other => panic!("SendNoteExpression round-trip changed the variant: {other:?}"),
682 }
683
684 let started = HostResponse::NoteStarted { note_id: 42 };
685 let started_json = serde_json::to_string(&started).expect("serialize NoteStarted");
686 match serde_json::from_str::<HostResponse>(&started_json).expect("deserialize") {
687 HostResponse::NoteStarted { note_id } => assert_eq!(note_id, 42),
688 other => panic!("NoteStarted round-trip changed the variant: {other:?}"),
689 }
690
691 let info = NoteExpressionInfo {
692 kind: NoteExpressionType::Tuning,
693 title: "Tuning".to_string(),
694 short_title: "Tun".to_string(),
695 units: String::new(),
696 default_value: 0.5,
697 min: 0.0,
698 max: 1.0,
699 step_count: 0,
700 is_bipolar: true,
701 is_one_shot: false,
702 is_absolute: false,
703 };
704 let resp = HostResponse::NoteExpressions {
705 expressions: vec![info.clone()],
706 };
707 let resp_json = serde_json::to_string(&resp).expect("serialize NoteExpressions");
708 match serde_json::from_str::<HostResponse>(&resp_json).expect("deserialize") {
709 HostResponse::NoteExpressions { expressions } => {
710 assert_eq!(expressions, vec![info]);
711 }
712 other => panic!("NoteExpressions round-trip changed the variant: {other:?}"),
713 }
714 }
715
716 #[test]
717 fn explicit_helper_override_missing_path_reports_clearly() {
718 let bogus = std::path::PathBuf::from("/nonexistent/vst3-host-helper-xyz");
722 let err = match PluginHostProcess::new(Some(bogus.clone()), DEFAULT_RESPONSE_TIMEOUT) {
723 Ok(_) => panic!("a missing override path must error, not spawn"),
724 Err(e) => e,
725 };
726 assert!(
727 err.contains("does not exist"),
728 "error should explain the missing path, got: {err}"
729 );
730 assert!(
731 err.contains("vst3-host-helper-xyz"),
732 "error should name the offending path, got: {err}"
733 );
734 }
735
736 #[cfg(unix)]
739 #[test]
740 fn hung_helper_times_out_and_is_killed_not_blocking() {
741 use std::io::Write;
742 use std::os::unix::fs::PermissionsExt;
743 use std::time::{Duration, Instant};
744
745 let dir = std::env::temp_dir().join(format!("vst3_hang_{}", std::process::id()));
747 std::fs::create_dir_all(&dir).unwrap();
748 let fake = dir.join("hung-helper");
749 let mut f = std::fs::File::create(&fake).unwrap();
750 writeln!(f, "#!/bin/sh\nexec sleep 30").unwrap();
753 drop(f);
754 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
755
756 let mut proc =
757 PluginHostProcess::spawn(fake.clone(), Duration::from_millis(200)).expect("spawn");
758 let started = Instant::now();
759 let res = proc.send_command(HostCommand::Shutdown);
760 let elapsed = started.elapsed();
761
762 assert!(
763 res.is_err(),
764 "a hung helper must yield an error, got {res:?}"
765 );
766 assert!(
767 elapsed < Duration::from_secs(3),
768 "send_command must return promptly on timeout, took {elapsed:?}"
769 );
770 assert!(proc.send_command(HostCommand::Shutdown).is_err());
772
773 let _ = std::fs::remove_dir_all(&dir);
774 }
775}
776
777pub mod crash_protection {
779 use std::panic::catch_unwind;
780 use std::panic::UnwindSafe;
781 use std::time::Duration;
782
783 #[derive(Debug, Clone, PartialEq)]
785 pub enum PluginStatus {
786 Ok,
788 Crashed(String),
790 Timeout(Duration),
792 }
793
794 pub fn protected_call<F, R>(f: F) -> Result<R, String>
796 where
797 F: FnOnce() -> R + UnwindSafe,
798 {
799 catch_unwind(f).map_err(|e| {
800 if let Some(s) = e.downcast_ref::<&str>() {
801 format!("Plugin panicked: {}", s)
802 } else if let Some(s) = e.downcast_ref::<String>() {
803 format!("Plugin panicked: {}", s)
804 } else {
805 "Plugin panicked with unknown error".to_string()
806 }
807 })
808 }
809}