1use serde::{Deserialize, Deserializer, Serialize, Serializer};
7use std::io::{BufRead, BufReader, Read, Write};
8use std::process::{Child, ChildStdin, Command, Stdio};
9use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
10use std::sync::mpsc::{self, Receiver, RecvTimeoutError};
11use std::sync::Arc;
12use std::thread::JoinHandle;
13use std::time::{Duration, Instant};
14
15pub(crate) const DEFAULT_RESPONSE_TIMEOUT: Duration = Duration::from_secs(5);
17
18pub(crate) const DEFAULT_SLOW_COMMAND_TIMEOUT: Duration = Duration::from_secs(30);
25
26const MAX_STATE_BASE64_BYTES: usize = crate::plugin::MAX_STATE_SNAPSHOT_BYTES.div_ceil(3) * 4;
30const MAX_RESPONSE_LINE_BYTES: usize = MAX_STATE_BASE64_BYTES + 1024 * 1024;
31
32const MAX_QUEUED_RESPONSES: usize = 64;
35
36const MAX_WIRE_CHANNELS: usize = 256;
38
39const MAX_WIRE_FRAMES: usize = 1 << 20;
41
42const MAX_WIRE_BUSES: i32 = 256;
44
45const MAX_WIRE_PARAMETER_CHANGES: usize = 8192;
49
50pub(crate) fn is_slow_command(command: &HostCommand) -> bool {
53 matches!(
54 command,
55 HostCommand::LoadPlugin { .. }
56 | HostCommand::SaveState
57 | HostCommand::LoadState { .. }
58 | HostCommand::GetProgramData { .. }
59 | HostCommand::SetProgramData { .. }
60 | HostCommand::GetUnitData { .. }
61 | HostCommand::SetUnitData { .. }
62 )
63}
64
65pub(crate) mod audio_codec {
73 use super::{Deserialize, Deserializer, Serializer, MAX_WIRE_CHANNELS, MAX_WIRE_FRAMES};
74
75 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
76
77 pub(super) fn encode_channel(samples: &[f32]) -> String {
79 let mut bytes = Vec::with_capacity(samples.len() * 4);
80 for s in samples {
81 bytes.extend_from_slice(&s.to_bits().to_le_bytes());
82 }
83 let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4);
84 for chunk in bytes.chunks(3) {
85 let b1 = chunk.get(1).copied().unwrap_or(0);
86 let b2 = chunk.get(2).copied().unwrap_or(0);
87 let n = (u32::from(chunk[0]) << 16) | (u32::from(b1) << 8) | u32::from(b2);
88 out.push(ALPHABET[(n >> 18) as usize & 63] as char);
89 out.push(ALPHABET[(n >> 12) as usize & 63] as char);
90 out.push(if chunk.len() > 1 {
91 ALPHABET[(n >> 6) as usize & 63] as char
92 } else {
93 '='
94 });
95 out.push(if chunk.len() > 2 {
96 ALPHABET[n as usize & 63] as char
97 } else {
98 '='
99 });
100 }
101 out
102 }
103
104 fn sextet(c: u8) -> Option<u32> {
105 let v = match c {
106 b'A'..=b'Z' => c - b'A',
107 b'a'..=b'z' => c - b'a' + 26,
108 b'0'..=b'9' => c - b'0' + 52,
109 b'+' => 62,
110 b'/' => 63,
111 _ => return None,
112 };
113 Some(u32::from(v))
114 }
115
116 pub(super) fn decode_channel(encoded: &str) -> Option<Vec<f32>> {
118 let bytes = encoded.as_bytes();
119 if bytes.len() % 4 != 0 {
120 return None;
121 }
122 let mut raw = Vec::with_capacity(bytes.len() / 4 * 3);
123 for chunk in bytes.chunks(4) {
124 let pad = chunk.iter().rev().take_while(|&&c| c == b'=').count();
125 if pad > 2 {
126 return None;
127 }
128 let mut n = 0u32;
129 for (i, &c) in chunk.iter().enumerate() {
130 if c == b'=' {
131 if i < 4 - pad {
132 return None; }
134 continue;
135 }
136 n |= sextet(c)? << (18 - 6 * i);
137 }
138 raw.push((n >> 16) as u8);
139 if pad < 2 {
140 raw.push((n >> 8) as u8);
141 }
142 if pad < 1 {
143 raw.push(n as u8);
144 }
145 }
146 if raw.len() % 4 != 0 {
147 return None;
148 }
149 Some(
150 raw.chunks_exact(4)
151 .map(|b| f32::from_bits(u32::from_le_bytes([b[0], b[1], b[2], b[3]])))
152 .collect(),
153 )
154 }
155
156 pub(crate) fn serialize<S: Serializer>(
157 channels: &[Vec<f32>],
158 serializer: S,
159 ) -> Result<S::Ok, S::Error> {
160 serializer.collect_seq(channels.iter().map(|c| encode_channel(c)))
161 }
162
163 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
164 deserializer: D,
165 ) -> Result<Vec<Vec<f32>>, D::Error> {
166 let encoded = Vec::<String>::deserialize(deserializer)?;
167 if encoded.len() > MAX_WIRE_CHANNELS {
168 log::warn!(
169 "isolation: clamping {} wire channels to {MAX_WIRE_CHANNELS}",
170 encoded.len()
171 );
172 }
173 encoded
174 .iter()
175 .take(MAX_WIRE_CHANNELS)
176 .map(|c| {
177 let mut samples = decode_channel(c).ok_or_else(|| {
178 serde::de::Error::custom("malformed base64 audio channel payload")
179 })?;
180 if samples.len() > MAX_WIRE_FRAMES {
181 log::warn!(
182 "isolation: clamping {} wire frames to {MAX_WIRE_FRAMES}",
183 samples.len()
184 );
185 samples.truncate(MAX_WIRE_FRAMES);
186 }
187 Ok(samples)
188 })
189 .collect()
190 }
191}
192
193pub(crate) mod state_codec {
200 use super::{Deserialize, Deserializer, Serializer, MAX_STATE_BASE64_BYTES};
201
202 const ALPHABET: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
203 const MAX_STATE_BYTES: usize = crate::plugin::MAX_STATE_SNAPSHOT_BYTES;
204
205 pub(crate) fn serialize<S: Serializer>(state: &[u8], serializer: S) -> Result<S::Ok, S::Error> {
206 if state.len() > MAX_STATE_BYTES {
207 return Err(serde::ser::Error::custom("plugin state exceeds wire limit"));
208 }
209 let mut out = String::with_capacity(state.len().div_ceil(3) * 4);
210 for chunk in state.chunks(3) {
211 let b1 = chunk.get(1).copied().unwrap_or(0);
212 let b2 = chunk.get(2).copied().unwrap_or(0);
213 let n = (u32::from(chunk[0]) << 16) | (u32::from(b1) << 8) | u32::from(b2);
214 out.push(ALPHABET[(n >> 18) as usize & 63] as char);
215 out.push(ALPHABET[(n >> 12) as usize & 63] as char);
216 out.push(if chunk.len() > 1 {
217 ALPHABET[(n >> 6) as usize & 63] as char
218 } else {
219 '='
220 });
221 out.push(if chunk.len() > 2 {
222 ALPHABET[n as usize & 63] as char
223 } else {
224 '='
225 });
226 }
227 serializer.serialize_str(&out)
228 }
229
230 fn sextet(byte: u8) -> Option<u32> {
231 Some(u32::from(match byte {
232 b'A'..=b'Z' => byte - b'A',
233 b'a'..=b'z' => byte - b'a' + 26,
234 b'0'..=b'9' => byte - b'0' + 52,
235 b'+' => 62,
236 b'/' => 63,
237 _ => return None,
238 }))
239 }
240
241 fn decode(encoded: &str) -> Option<Vec<u8>> {
242 let bytes = encoded.as_bytes();
243 if bytes.len() > MAX_STATE_BASE64_BYTES || bytes.len() % 4 != 0 {
244 return None;
245 }
246 let mut raw = Vec::with_capacity(bytes.len() / 4 * 3);
247 let chunk_count = bytes.len() / 4;
248 for (chunk_index, chunk) in bytes.chunks(4).enumerate() {
249 let pad = chunk.iter().rev().take_while(|&&byte| byte == b'=').count();
250 if pad > 2 || (pad != 0 && chunk_index + 1 != chunk_count) {
251 return None;
252 }
253 let mut n = 0u32;
254 for (index, &byte) in chunk.iter().enumerate() {
255 if byte == b'=' {
256 if index < 4 - pad {
257 return None;
258 }
259 } else {
260 n |= sextet(byte)? << (18 - 6 * index);
261 }
262 }
263 raw.push((n >> 16) as u8);
264 if pad < 2 {
265 raw.push((n >> 8) as u8);
266 }
267 if pad == 0 {
268 raw.push(n as u8);
269 }
270 }
271 (raw.len() <= MAX_STATE_BYTES).then_some(raw)
272 }
273
274 #[derive(Deserialize)]
275 #[serde(untagged)]
276 enum StateWire {
277 Base64(String),
278 Legacy(Vec<u8>),
279 }
280
281 pub(crate) fn deserialize<'de, D: Deserializer<'de>>(
282 deserializer: D,
283 ) -> Result<Vec<u8>, D::Error> {
284 match StateWire::deserialize(deserializer)? {
285 StateWire::Base64(encoded) => decode(&encoded)
286 .ok_or_else(|| serde::de::Error::custom("malformed or oversized plugin state")),
287 StateWire::Legacy(state) if state.len() <= MAX_STATE_BYTES => Ok(state),
288 StateWire::Legacy(_) => {
289 Err(serde::de::Error::custom("plugin state exceeds wire limit"))
290 }
291 }
292 }
293}
294
295mod lossless_f64 {
301 use super::{Deserializer, Serializer};
302 use std::fmt;
303
304 pub(super) fn serialize<S: Serializer>(value: &f64, serializer: S) -> Result<S::Ok, S::Error> {
305 if value.is_finite() {
306 serializer.serialize_f64(*value)
307 } else if value.is_nan() {
308 serializer.serialize_str("NaN")
309 } else if *value > 0.0 {
310 serializer.serialize_str("inf")
311 } else {
312 serializer.serialize_str("-inf")
313 }
314 }
315
316 struct AnyF64;
317
318 impl serde::de::Visitor<'_> for AnyF64 {
319 type Value = f64;
320
321 fn expecting(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
322 f.write_str("a number or a non-finite float spelled as a string")
323 }
324
325 fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<f64, E> {
326 Ok(v)
327 }
328
329 fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<f64, E> {
330 Ok(v as f64)
331 }
332
333 fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<f64, E> {
334 Ok(v as f64)
335 }
336
337 fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<f64, E> {
338 v.parse::<f64>()
339 .map_err(|_| E::custom(format!("not a float: {v}")))
340 }
341
342 fn visit_unit<E: serde::de::Error>(self) -> Result<f64, E> {
345 Ok(f64::NAN)
346 }
347 }
348
349 pub(super) fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result<f64, D::Error> {
350 deserializer.deserialize_any(AnyF64)
351 }
352}
353
354mod parameter_changes_codec {
359 use super::{Deserializer, Serializer, MAX_WIRE_PARAMETER_CHANGES};
360 use serde::de::{SeqAccess, Visitor};
361 use serde::ser::SerializeSeq;
362 use std::fmt;
363
364 pub(super) fn serialize<S: Serializer>(
365 changes: &[(u32, f64)],
366 serializer: S,
367 ) -> Result<S::Ok, S::Error> {
368 if changes.len() > MAX_WIRE_PARAMETER_CHANGES {
369 return Err(serde::ser::Error::custom(
370 "parameter feedback exceeds wire limit",
371 ));
372 }
373 let mut sequence = serializer.serialize_seq(Some(changes.len()))?;
374 for &(id, value) in changes {
375 sequence.serialize_element(&(id, value.to_bits()))?;
376 }
377 sequence.end()
378 }
379
380 struct ParameterChangesVisitor;
381
382 impl<'de> Visitor<'de> for ParameterChangesVisitor {
383 type Value = Vec<(u32, f64)>;
384
385 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
386 write!(
387 formatter,
388 "at most {MAX_WIRE_PARAMETER_CHANGES} parameter-id/value-bit pairs"
389 )
390 }
391
392 fn visit_seq<A: SeqAccess<'de>>(self, mut sequence: A) -> Result<Self::Value, A::Error> {
393 let capacity = sequence
394 .size_hint()
395 .unwrap_or(0)
396 .min(MAX_WIRE_PARAMETER_CHANGES);
397 let mut changes = Vec::with_capacity(capacity);
398 while let Some((id, bits)) = sequence.next_element::<(u32, u64)>()? {
399 if changes.len() >= MAX_WIRE_PARAMETER_CHANGES {
400 return Err(serde::de::Error::custom(
401 "parameter feedback exceeds wire limit",
402 ));
403 }
404 changes.push((id, f64::from_bits(bits)));
405 }
406 Ok(changes)
407 }
408 }
409
410 pub(super) fn deserialize<'de, D: Deserializer<'de>>(
411 deserializer: D,
412 ) -> Result<Vec<(u32, f64)>, D::Error> {
413 deserializer.deserialize_seq(ParameterChangesVisitor)
414 }
415}
416
417fn clamped_channel_count<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i32, D::Error> {
419 let raw = i32::deserialize(deserializer)?;
420 Ok(raw.clamp(0, MAX_WIRE_CHANNELS as i32))
421}
422
423fn clamped_bus_count<'de, D: Deserializer<'de>>(deserializer: D) -> Result<i32, D::Error> {
425 let raw = i32::deserialize(deserializer)?;
426 Ok(raw.clamp(0, MAX_WIRE_BUSES))
427}
428
429#[derive(Debug, Clone, Serialize, Deserialize)]
435pub enum HostCommand {
436 LoadPlugin {
438 path: String,
440 #[serde(with = "lossless_f64")]
442 sample_rate: f64,
443 block_size: u32,
445 #[serde(with = "lossless_f64")]
447 tempo: f64,
448 time_sig_numerator: i32,
450 time_sig_denominator: i32,
452 #[serde(default)]
454 class_id: Option<String>,
455 },
456 UnloadPlugin,
458 CreateGui,
460 CloseGui,
462 StartProcessing,
464 StopProcessing,
466 Reconfigure {
468 #[serde(with = "lossless_f64")]
470 sample_rate: f64,
471 block_size: u32,
473 },
474 SetProcessMode {
476 mode: crate::plugin::ProcessMode,
478 },
479 SetParameter {
481 id: u32,
483 #[serde(with = "lossless_f64")]
485 value: f64,
486 },
487 SetParameterAt {
489 id: u32,
491 #[serde(with = "lossless_f64")]
493 value: f64,
494 offset: i32,
496 },
497 SetTempo {
500 #[serde(with = "lossless_f64")]
502 bpm: f64,
503 },
504 SetTimeSignature {
507 numerator: i32,
509 denominator: i32,
511 },
512 SetPlaying {
515 playing: bool,
517 },
518 GetParameter {
520 id: u32,
522 },
523 GetAllParameters,
525 FormatParameter {
527 id: u32,
529 #[serde(with = "lossless_f64")]
531 normalized: f64,
532 },
533 SendMidi {
535 event: crate::midi::MidiEvent,
537 },
538 SendMidiAt {
540 event: crate::midi::MidiEvent,
542 sample_offset: i32,
544 },
545 SendPluginEvent {
547 event: crate::midi::PluginEvent,
549 },
550 MidiPanic,
552 Process {
554 #[serde(with = "audio_codec")]
556 inputs: Vec<Vec<f32>>,
557 frames: u32,
559 },
560 ProcessBuses {
562 inputs: Vec<crate::audio::AudioBusBuffer>,
564 outputs: Vec<crate::audio::AudioBusConfig>,
566 frames: u32,
568 },
569 AudioBusLayout,
571 SaveState,
573 LoadState {
575 #[serde(with = "state_codec")]
577 data: Vec<u8>,
578 #[serde(default)]
585 context: crate::plugin::StateContext,
586 },
587 NoteOn {
590 channel: u8,
592 note: u8,
594 velocity: u8,
596 sample_offset: i32,
598 },
599 NoteOff {
601 note_id: i32,
603 sample_offset: i32,
605 },
606 SendNoteExpression {
609 note_id: i32,
611 kind: crate::midi::NoteExpressionType,
613 #[serde(with = "lossless_f64")]
615 value: f64,
616 sample_offset: i32,
618 },
619 NoteExpressions {
621 bus: i32,
623 channel: i16,
625 },
626 SelectProgram {
628 unit_id: i32,
630 program_index: i32,
632 },
633 SetBusActive {
635 media_type: crate::audio::MediaType,
637 direction: crate::audio::BusDirection,
639 bus_index: i32,
641 active: bool,
643 },
644 BusArrangements,
646 SetBusArrangements {
648 inputs: Vec<crate::audio::SpeakerArrangement>,
650 outputs: Vec<crate::audio::SpeakerArrangement>,
652 },
653 GetUnits,
655 GetSelectedUnit,
657 SelectUnit {
659 unit_id: i32,
661 },
662 ProgramPitchNames {
664 program_list_id: i32,
666 program_index: i32,
668 },
669 GetProgramData {
671 program_list_id: i32,
673 program_index: i32,
675 },
676 SetProgramData {
678 program_list_id: i32,
680 program_index: i32,
682 #[serde(with = "state_codec")]
684 data: Vec<u8>,
685 },
686 GetUnitData {
688 unit_id: i32,
690 },
691 SetUnitData {
693 unit_id: i32,
695 #[serde(with = "state_codec")]
697 data: Vec<u8>,
698 },
699 BeginHostEdit {
701 parameter_id: u32,
703 },
704 EndHostEdit {
706 parameter_id: u32,
708 },
709 SendMidiLearn {
711 bus: i32,
713 channel: i16,
715 controller: u16,
717 },
718 SetAutomationState {
720 state: crate::plugin::AutomationState,
722 },
723 RemapParameterId {
725 old_plugin_uid: String,
727 old_param_id: u32,
729 },
730 LatencySamples,
733 TailSamples,
735 MidiCcToParameter {
737 bus: i32,
739 channel: i16,
741 cc: u16,
743 },
744 TakeParameterEdits,
747 TakeParameterChanges,
749 TakeHostNotifications,
751 TakeDataExchangeBlocks,
753 ExecuteContextMenuItem {
755 menu_id: u64,
757 item_id: u32,
759 },
760 DismissContextMenu {
762 menu_id: u64,
764 },
765 TakeRestartFlags,
767 ServiceHostRequests,
769 Shutdown,
771}
772
773#[derive(Debug, Serialize, Deserialize)]
775pub enum HostResponse {
776 Success {
778 message: String,
780 },
781 Error {
783 message: String,
785 },
786 Crashed {
788 message: String,
790 },
791 AudioOutput {
794 #[serde(with = "audio_codec")]
796 outputs: Vec<Vec<f32>>,
797 output_events: Vec<crate::midi::PluginEvent>,
799 },
800 BusAudioOutput {
802 outputs: Vec<crate::audio::AudioBusBuffer>,
804 output_events: Vec<crate::midi::PluginEvent>,
806 },
807 AudioBusLayout {
809 layout: crate::audio::AudioBusLayout,
811 },
812 ParameterValue {
814 #[serde(with = "lossless_f64")]
816 value: f64,
817 },
818 ParameterString {
820 value: String,
822 },
823 Parameters {
825 params: Vec<crate::parameters::Parameter>,
827 },
828 State {
830 #[serde(with = "state_codec")]
832 data: Vec<u8>,
833 },
834 GuiCreated {
837 width: i32,
839 height: i32,
841 },
842 PluginInfo {
844 vendor: String,
846 name: String,
848 version: String,
850 category: String,
852 uid: String,
854 has_gui: bool,
856 #[serde(deserialize_with = "clamped_bus_count")]
858 audio_inputs: i32,
859 #[serde(deserialize_with = "clamped_bus_count")]
861 audio_outputs: i32,
862 #[serde(deserialize_with = "clamped_channel_count")]
865 output_channels: i32,
866 has_midi_input: bool,
868 has_midi_output: bool,
870 #[serde(default)]
872 compatibility: Vec<crate::discovery::ClassCompatibility>,
873 },
874 NoteStarted {
876 note_id: i32,
878 },
879 NoteExpressions {
881 expressions: Vec<crate::midi::NoteExpressionInfo>,
883 },
884 ParameterEdits {
887 edits: Vec<crate::plugin::ParameterEdit>,
889 },
890 ParameterChanges {
892 #[serde(with = "parameter_changes_codec")]
894 changes: Vec<(u32, f64)>,
895 },
896 HostNotifications {
898 notifications: Vec<crate::plugin::HostNotification>,
900 },
901 DataExchangeBlocks {
903 blocks: Vec<crate::plugin::DataExchangeBlock>,
905 },
906 RestartFlags {
908 bits: i32,
910 },
911 BusArrangements {
913 arrangements: crate::audio::BusArrangements,
915 },
916 Units {
918 units: Vec<crate::plugin::PluginUnit>,
920 },
921 SelectedUnit {
923 unit_id: Option<i32>,
925 },
926 ProgramPitchNames {
928 names: Vec<crate::plugin::ProgramPitchName>,
930 },
931 OpaqueData {
933 supported: bool,
935 #[serde(with = "state_codec")]
937 data: Vec<u8>,
938 },
939 LatencySamples {
941 samples: u32,
943 },
944 TailSamples {
946 samples: u32,
948 },
949 MidiParameterMapping {
951 id: Option<u32>,
953 },
954 RemappedParameter {
956 id: Option<u32>,
958 },
959}
960
961pub struct ProtocolChannel {
976 inner: ProtocolChannelInner,
977}
978
979#[cfg(unix)]
980type ProtocolChannelInner = std::fs::File;
981#[cfg(not(unix))]
982type ProtocolChannelInner = std::io::Stdout;
983
984impl ProtocolChannel {
985 #[cfg(unix)]
987 pub fn claim() -> Self {
988 use std::os::fd::FromRawFd;
989
990 let fd = unsafe {
994 let private = match libc::fcntl(libc::STDOUT_FILENO, libc::F_DUPFD_CLOEXEC, 3) {
995 fd if fd >= 0 => Some(fd),
996 _ => match libc::dup(libc::STDOUT_FILENO) {
997 fd if fd >= 0 => Some(fd),
998 _ => None,
999 },
1000 };
1001 match private {
1002 Some(fd) => {
1003 libc::dup2(libc::STDERR_FILENO, libc::STDOUT_FILENO);
1005 fd
1006 }
1007 None => {
1008 eprintln!("helper: could not privatise the protocol channel; plugin writes to stdout may corrupt it");
1011 libc::STDOUT_FILENO
1012 }
1013 }
1014 };
1015 Self {
1018 inner: unsafe { std::fs::File::from_raw_fd(fd) },
1019 }
1020 }
1021
1022 #[cfg(not(unix))]
1024 pub fn claim() -> Self {
1025 Self {
1026 inner: std::io::stdout(),
1027 }
1028 }
1029}
1030
1031impl Write for ProtocolChannel {
1032 fn write(&mut self, buf: &[u8]) -> std::io::Result<usize> {
1033 self.inner.write(buf)
1034 }
1035
1036 fn flush(&mut self) -> std::io::Result<()> {
1037 self.inner.flush()
1038 }
1039}
1040
1041pub struct PluginHostProcess {
1048 process: Option<Child>,
1049 stdin: Option<ChildStdin>,
1050 responses: Receiver<String>,
1052 reader: Option<JoinHandle<()>>,
1054 reader_finished: Arc<AtomicBool>,
1057 queued: Arc<AtomicUsize>,
1059 discarded_by_reader: Arc<AtomicU64>,
1061 unparsed_lines: u64,
1063 timeout: Duration,
1065 slow_timeout: Duration,
1067 dead: bool,
1069 helper_path: std::path::PathBuf,
1073}
1074
1075const LOAD_CRASH_RETRIES: u32 = 1;
1089
1090const LOAD_CRASH_RETRY_BACKOFF: Duration = Duration::from_millis(250);
1093
1094enum ExchangeError {
1102 AlreadyDead(String),
1104 DiedDuringCommand(String),
1106 TimedOut(String),
1108 Encoding(String),
1110}
1111
1112impl From<ExchangeError> for String {
1113 fn from(error: ExchangeError) -> String {
1114 match error {
1115 ExchangeError::AlreadyDead(message)
1116 | ExchangeError::DiedDuringCommand(message)
1117 | ExchangeError::TimedOut(message)
1118 | ExchangeError::Encoding(message) => message,
1119 }
1120 }
1121}
1122
1123enum ReadLine {
1125 Line(Vec<u8>),
1127 Oversized,
1129 Eof,
1131}
1132
1133fn read_bounded_line(reader: &mut impl BufRead, max: usize) -> ReadLine {
1136 let mut line = Vec::new();
1137 let mut oversized = false;
1138 loop {
1139 let budget = (max + 1 - line.len()) as u64;
1140 let mut chunk = Vec::new();
1141 let read = match reader.by_ref().take(budget).read_until(b'\n', &mut chunk) {
1142 Ok(n) => n,
1143 Err(_) => return ReadLine::Eof,
1144 };
1145 let complete = chunk.last() == Some(&b'\n');
1146 if !oversized {
1147 line.extend_from_slice(&chunk);
1148 if line.len() > max {
1149 oversized = true;
1150 line = Vec::new();
1151 }
1152 }
1153 if read == 0 {
1154 return if oversized {
1156 ReadLine::Oversized
1157 } else if line.is_empty() {
1158 ReadLine::Eof
1159 } else {
1160 ReadLine::Line(line)
1161 };
1162 }
1163 if complete {
1164 return if oversized {
1165 ReadLine::Oversized
1166 } else {
1167 ReadLine::Line(line)
1168 };
1169 }
1170 }
1171}
1172
1173impl PluginHostProcess {
1174 pub fn new(
1176 helper_override: Option<std::path::PathBuf>,
1177 timeout: Duration,
1178 ) -> Result<Self, String> {
1179 let override_path = helper_override
1182 .or_else(|| std::env::var_os("VST3_HOST_HELPER_PATH").map(std::path::PathBuf::from));
1183 if let Some(p) = override_path {
1184 if !p.exists() {
1185 return Err(format!(
1186 "Configured helper path does not exist: {}",
1187 p.display()
1188 ));
1189 }
1190 return Self::spawn(p, timeout);
1191 }
1192
1193 let exe_path =
1195 std::env::current_exe().map_err(|e| format!("Failed to get current exe: {}", e))?;
1196
1197 let exe_dir = exe_path.parent().ok_or("Failed to get exe directory")?;
1198
1199 let helper_names = ["vst3-host-helper", "vst3-inspector-helper"];
1201 let mut helper_path = None;
1202
1203 for name in &helper_names {
1205 let path = exe_dir.join(name);
1206 if path.exists() {
1207 helper_path = Some(path);
1208 break;
1209 }
1210 }
1211
1212 if helper_path.is_none() && exe_dir.file_name() == Some(std::ffi::OsStr::new("examples")) {
1214 if let Some(parent_dir) = exe_dir.parent() {
1215 for name in &helper_names {
1216 let path = parent_dir.join(name);
1217 if path.exists() {
1218 helper_path = Some(path);
1219 break;
1220 }
1221 }
1222 }
1223 }
1224
1225 if helper_path.is_none() && crate::discovery::running_from_cargo_target(exe_dir) {
1234 let mut current_dir = exe_dir;
1236 while let Some(parent) = current_dir.parent() {
1237 let debug_path = parent.join("target").join("debug").join("vst3-host-helper");
1238 let release_path = parent
1239 .join("target")
1240 .join("release")
1241 .join("vst3-host-helper");
1242
1243 if debug_path.exists() {
1244 helper_path = Some(debug_path);
1245 break;
1246 } else if release_path.exists() {
1247 helper_path = Some(release_path);
1248 break;
1249 }
1250
1251 if parent.join("Cargo.toml").exists() {
1253 break;
1254 }
1255 current_dir = parent;
1256 }
1257 }
1258
1259 let helper_path = helper_path
1260 .ok_or_else(|| format!("Helper executable not found. Searched in {:?} and parent directories. Make sure to build with --bins flag.", exe_dir))?;
1261
1262 Self::spawn(helper_path, timeout)
1263 }
1264
1265 fn spawn(helper_path: std::path::PathBuf, timeout: Duration) -> Result<Self, String> {
1267 let mut child = Command::new(&helper_path)
1268 .stdin(Stdio::piped())
1269 .stdout(Stdio::piped())
1270 .stderr(Stdio::inherit())
1271 .spawn()
1272 .map_err(|e| format!("Failed to spawn helper process: {}", e))?;
1273
1274 let stdin = child.stdin.take().ok_or("Failed to get stdin")?;
1275 let stdout = child.stdout.take().ok_or("Failed to get stdout")?;
1276
1277 let (tx, rx) = mpsc::channel::<String>();
1285 let queued = Arc::new(AtomicUsize::new(0));
1286 let discarded = Arc::new(AtomicU64::new(0));
1287 let finished = Arc::new(AtomicBool::new(false));
1288 let reader = std::thread::spawn({
1289 let queued = Arc::clone(&queued);
1290 let discarded = Arc::clone(&discarded);
1291 let finished = Arc::clone(&finished);
1292 move || {
1293 let mut reader = BufReader::new(stdout);
1294 loop {
1295 match read_bounded_line(&mut reader, MAX_RESPONSE_LINE_BYTES) {
1296 ReadLine::Eof => break,
1297 ReadLine::Oversized => {
1298 discarded.fetch_add(1, Ordering::Relaxed);
1299 }
1300 ReadLine::Line(bytes) => {
1301 if queued.load(Ordering::Relaxed) >= MAX_QUEUED_RESPONSES {
1302 discarded.fetch_add(1, Ordering::Relaxed);
1303 continue;
1304 }
1305 queued.fetch_add(1, Ordering::Relaxed);
1306 let line = String::from_utf8_lossy(&bytes).into_owned();
1309 if tx.send(line).is_err() {
1310 break; }
1312 }
1313 }
1314 }
1315 finished.store(true, Ordering::Release);
1316 }
1317 });
1318
1319 Ok(Self {
1320 process: Some(child),
1321 stdin: Some(stdin),
1322 responses: rx,
1323 reader: Some(reader),
1324 reader_finished: finished,
1325 queued,
1326 discarded_by_reader: discarded,
1327 unparsed_lines: 0,
1328 timeout,
1329 slow_timeout: DEFAULT_SLOW_COMMAND_TIMEOUT.max(timeout),
1330 dead: false,
1331 helper_path,
1332 })
1333 }
1334
1335 fn respawn(&mut self) -> Result<(), String> {
1338 self.shutdown();
1339 let slow_timeout = self.slow_timeout;
1340 let unparsed_lines = self.unparsed_lines;
1341 let discarded = self.discarded_by_reader.load(Ordering::Relaxed);
1342
1343 *self = Self::spawn(self.helper_path.clone(), self.timeout)?;
1344
1345 self.slow_timeout = slow_timeout;
1346 self.unparsed_lines = unparsed_lines;
1347 self.discarded_by_reader
1348 .fetch_add(discarded, Ordering::Relaxed);
1349 Ok(())
1350 }
1351
1352 pub fn set_timeout(&mut self, timeout: Duration) {
1354 self.timeout = timeout;
1355 }
1356
1357 pub fn set_slow_command_timeout(&mut self, timeout: Duration) {
1360 self.slow_timeout = timeout;
1361 }
1362
1363 fn timeout_for(&self, command: &HostCommand) -> Duration {
1365 if is_slow_command(command) {
1366 self.slow_timeout.max(self.timeout)
1367 } else {
1368 self.timeout
1369 }
1370 }
1371
1372 fn drop_stale_lines(&mut self) {
1375 let mut stale = 0u64;
1376 while self.responses.try_recv().is_ok() {
1377 self.queued.fetch_sub(1, Ordering::Relaxed);
1378 stale += 1;
1379 }
1380 if stale > 0 {
1381 self.unparsed_lines += stale;
1382 log::warn!("isolation: dropped {stale} unsolicited line(s) from the helper");
1383 }
1384 }
1385
1386 fn exit_status(&mut self) -> Option<std::process::ExitStatus> {
1388 self.process
1389 .as_mut()
1390 .and_then(|p| p.try_wait().ok().flatten())
1391 }
1392
1393 pub fn discarded_line_count(&self) -> u64 {
1398 self.unparsed_lines + self.discarded_by_reader.load(Ordering::Relaxed)
1399 }
1400
1401 pub fn send_command(&mut self, command: HostCommand) -> Result<HostResponse, String> {
1420 if !matches!(command, HostCommand::LoadPlugin { .. }) {
1421 return self.exchange(command).map_err(String::from);
1422 }
1423
1424 let mut retries_left = LOAD_CRASH_RETRIES;
1425 loop {
1426 match self.exchange(command.clone()) {
1427 Ok(response) => return Ok(response),
1428 Err(ExchangeError::DiedDuringCommand(detail)) if retries_left > 0 => {
1429 retries_left -= 1;
1430 log::warn!(
1431 "isolation: the helper died while loading the plugin ({detail}); \
1432 retrying once with a fresh helper"
1433 );
1434 std::thread::sleep(LOAD_CRASH_RETRY_BACKOFF);
1435 if let Err(spawn_error) = self.respawn() {
1436 log::warn!("isolation: could not respawn the helper: {spawn_error}");
1438 return Err(detail);
1439 }
1440 }
1441 Err(other) => return Err(String::from(other)),
1442 }
1443 }
1444 }
1445
1446 fn exchange(&mut self, command: HostCommand) -> Result<HostResponse, ExchangeError> {
1448 if self.dead {
1449 return Err(ExchangeError::AlreadyDead(
1450 "Helper process is no longer running".to_string(),
1451 ));
1452 }
1453
1454 let command_json = serde_json::to_string(&command)
1455 .map_err(|e| ExchangeError::Encoding(format!("Failed to serialize command: {}", e)))?;
1456
1457 self.drop_stale_lines();
1459
1460 {
1461 let Some(stdin) = self.stdin.as_mut() else {
1462 return Err(ExchangeError::AlreadyDead("No stdin available".to_string()));
1463 };
1464 if let Err(e) = writeln!(stdin, "{}", command_json).and_then(|()| stdin.flush()) {
1465 self.dead = true;
1466 return Err(ExchangeError::DiedDuringCommand(format!(
1467 "Failed to write command (helper gone?): {}",
1468 e
1469 )));
1470 }
1471 }
1472
1473 let timeout = self.timeout_for(&command);
1474 let deadline = Instant::now() + timeout;
1475 loop {
1476 let remaining = deadline.saturating_duration_since(Instant::now());
1477 match self.responses.recv_timeout(remaining) {
1478 Ok(line) => {
1479 self.queued.fetch_sub(1, Ordering::Relaxed);
1480 match serde_json::from_str::<HostResponse>(&line) {
1481 Ok(response) => return Ok(response),
1482 Err(parse_error) => {
1483 if let Some(status) = self.exit_status() {
1486 self.dead = true;
1487 return Err(ExchangeError::DiedDuringCommand(format!(
1488 "Helper process crashed: exited with {status} while writing a response ({parse_error})"
1489 )));
1490 }
1491 self.unparsed_lines += 1;
1492 log::warn!(
1493 "isolation: dropping unparseable line from the helper ({parse_error})"
1494 );
1495 }
1496 }
1497 }
1498 Err(RecvTimeoutError::Timeout) => {
1499 self.dead = true;
1501 if let Some(ref mut process) = self.process {
1502 let _ = process.kill();
1503 }
1504 return Err(ExchangeError::TimedOut(format!(
1505 "Timed out after {:?} waiting for helper response (plugin may have hung)",
1506 timeout
1507 )));
1508 }
1509 Err(RecvTimeoutError::Disconnected) => {
1510 self.dead = true;
1512 let detail = match self.check_process_status() {
1513 Err(status) => format!("Helper process crashed: {}", status),
1514 Ok(()) => "Helper process exited unexpectedly".to_string(),
1515 };
1516 return Err(ExchangeError::DiedDuringCommand(detail));
1517 }
1518 }
1519 }
1520 }
1521
1522 pub fn is_alive(&self) -> bool {
1524 !self.dead
1525 }
1526
1527 pub fn helper_pid(&self) -> Option<u32> {
1530 self.process.as_ref().map(|c| c.id())
1531 }
1532
1533 pub fn check_process_status(&mut self) -> Result<(), String> {
1535 if let Some(ref mut process) = self.process {
1536 match process.try_wait() {
1537 Ok(Some(status)) => {
1538 if !status.success() {
1539 return Err(format!("Helper process exited with status: {}", status));
1540 }
1541 }
1542 Ok(None) => {
1543 return Ok(());
1545 }
1546 Err(e) => {
1547 return Err(format!("Failed to check process status: {}", e));
1548 }
1549 }
1550 }
1551 Ok(())
1552 }
1553
1554 pub fn shutdown(&mut self) {
1556 if !self.dead {
1559 if let (Some(stdin), Ok(json)) = (
1560 self.stdin.as_mut(),
1561 serde_json::to_string(&HostCommand::Shutdown),
1562 ) {
1563 let _ = writeln!(stdin, "{}", json);
1564 let _ = stdin.flush();
1565 }
1566 }
1567
1568 self.stdin = None;
1571
1572 if let Some(mut process) = self.process.take() {
1573 let deadline = std::time::Instant::now() + Duration::from_secs(2);
1577 loop {
1578 match process.try_wait() {
1579 Ok(Some(_)) => break,
1580 Ok(None) if std::time::Instant::now() >= deadline => {
1581 let _ = process.kill();
1582 let _ = process.wait();
1583 break;
1584 }
1585 Ok(None) => std::thread::sleep(Duration::from_millis(10)),
1586 Err(_) => {
1587 let _ = process.kill();
1588 break;
1589 }
1590 }
1591 }
1592 }
1593 if let Some(reader) = self.reader.take() {
1594 let deadline = std::time::Instant::now() + Duration::from_millis(250);
1601 while !self.reader_finished.load(Ordering::Acquire) {
1602 if std::time::Instant::now() >= deadline {
1603 log::debug!("isolation: helper stdout still open, detaching reader thread");
1604 break;
1605 }
1606 std::thread::sleep(Duration::from_millis(5));
1607 }
1608 if self.reader_finished.load(Ordering::Acquire) {
1609 let _ = reader.join();
1610 }
1611 }
1612 self.dead = true;
1613 }
1614}
1615
1616impl Drop for PluginHostProcess {
1617 fn drop(&mut self) {
1618 self.shutdown();
1619 }
1620}
1621
1622pub type IsolationResult<T> = std::result::Result<T, IsolationError>;
1624
1625#[derive(Debug, thiserror::Error)]
1627pub enum IsolationError {
1628 #[error("IO error: {0}")]
1630 Io(#[from] std::io::Error),
1631
1632 #[error("Serialization error: {0}")]
1634 Serialization(#[from] serde_json::Error),
1635
1636 #[error("Plugin error: {0}")]
1638 Plugin(String),
1639
1640 #[error("Plugin crashed: {0}")]
1642 Crashed(String),
1643
1644 #[error("Helper process not running")]
1646 NotRunning,
1647
1648 #[error("Unexpected response from helper")]
1650 UnexpectedResponse,
1651}
1652
1653#[cfg(test)]
1654mod wire_tests {
1655 use super::*;
1656 use crate::midi::{MidiChannel, MidiEvent};
1657
1658 #[test]
1659 fn audio_output_carries_midi_across_the_wire() {
1660 let resp = HostResponse::AudioOutput {
1663 outputs: vec![vec![0.0, 0.5], vec![-0.5, 0.0]],
1664 output_events: vec![
1665 MidiEvent::NoteOn {
1666 channel: MidiChannel::Ch1,
1667 note: 60,
1668 velocity: 100,
1669 }
1670 .into(),
1671 MidiEvent::NoteOff {
1672 channel: MidiChannel::Ch1,
1673 note: 60,
1674 velocity: 0,
1675 }
1676 .into(),
1677 ],
1678 };
1679 let json = serde_json::to_string(&resp).expect("serialize");
1680 let back: HostResponse = serde_json::from_str(&json).expect("deserialize");
1681 match back {
1682 HostResponse::AudioOutput {
1683 outputs,
1684 output_events,
1685 } => {
1686 assert_eq!(outputs, vec![vec![0.0, 0.5], vec![-0.5, 0.0]]);
1687 assert_eq!(output_events.len(), 2);
1688 assert_eq!(
1689 output_events[0].to_midi(),
1690 Some(MidiEvent::NoteOn {
1691 channel: MidiChannel::Ch1,
1692 note: 60,
1693 velocity: 100
1694 })
1695 );
1696 }
1697 other => panic!("round-trip changed the variant: {other:?}"),
1698 }
1699 }
1700
1701 #[test]
1702 fn state_commands_round_trip_across_the_wire() {
1703 let blob: Vec<u8> = vec![0, 1, 2, 250, 255, 42];
1705
1706 let save = serde_json::to_string(&HostCommand::SaveState).expect("serialize SaveState");
1707 assert!(matches!(
1708 serde_json::from_str::<HostCommand>(&save).expect("deserialize SaveState"),
1709 HostCommand::SaveState
1710 ));
1711
1712 let load = HostCommand::LoadState {
1713 data: blob.clone(),
1714 context: crate::plugin::StateContext::Project,
1715 };
1716 let load_json = serde_json::to_string(&load).expect("serialize LoadState");
1717 assert!(
1718 load_json.contains("\"data\":\""),
1719 "state should use compact base64, not a JSON integer array"
1720 );
1721 match serde_json::from_str::<HostCommand>(&load_json).expect("deserialize LoadState") {
1722 HostCommand::LoadState { data, context } => {
1723 assert_eq!(data, blob);
1724 assert_eq!(context, crate::plugin::StateContext::Project);
1725 }
1726 other => panic!("LoadState round-trip changed the variant: {other:?}"),
1727 }
1728
1729 let legacy = r#"{"LoadState":{"data":[0,1,2,250,255,42]}}"#;
1730 match serde_json::from_str::<HostCommand>(legacy).expect("deserialize legacy LoadState") {
1731 HostCommand::LoadState { data, context } => {
1732 assert_eq!(data, blob);
1733 assert_eq!(context, crate::plugin::StateContext::Project);
1734 }
1735 other => panic!("legacy LoadState changed the variant: {other:?}"),
1736 }
1737
1738 let state = HostResponse::State { data: blob.clone() };
1739 let state_json = serde_json::to_string(&state).expect("serialize State");
1740 match serde_json::from_str::<HostResponse>(&state_json).expect("deserialize State") {
1741 HostResponse::State { data } => assert_eq!(data, blob),
1742 other => panic!("State round-trip changed the variant: {other:?}"),
1743 }
1744 }
1745
1746 #[test]
1747 fn set_parameter_at_round_trips_across_the_wire() {
1748 let cmd = HostCommand::SetParameterAt {
1751 id: 42,
1752 value: 0.75,
1753 offset: 256,
1754 };
1755 let json = serde_json::to_string(&cmd).expect("serialize SetParameterAt");
1756 match serde_json::from_str::<HostCommand>(&json).expect("deserialize SetParameterAt") {
1757 HostCommand::SetParameterAt { id, value, offset } => {
1758 assert_eq!(id, 42);
1759 assert_eq!(value, 0.75);
1760 assert_eq!(offset, 256);
1761 }
1762 other => panic!("round-trip changed the variant: {other:?}"),
1763 }
1764 }
1765
1766 #[test]
1767 fn scheduled_midi_offset_ipc_round_trips() {
1768 use crate::midi::{MidiChannel, MidiEvent};
1771 let cmd = HostCommand::SendMidiAt {
1772 event: MidiEvent::NoteOn {
1773 channel: MidiChannel::Ch1,
1774 note: 60,
1775 velocity: 100,
1776 },
1777 sample_offset: 256,
1778 };
1779 let json = serde_json::to_string(&cmd).expect("serialize SendMidiAt");
1780 match serde_json::from_str::<HostCommand>(&json).expect("deserialize SendMidiAt") {
1781 HostCommand::SendMidiAt {
1782 event,
1783 sample_offset,
1784 } => {
1785 assert_eq!(
1786 event,
1787 MidiEvent::NoteOn {
1788 channel: MidiChannel::Ch1,
1789 note: 60,
1790 velocity: 100
1791 }
1792 );
1793 assert_eq!(sample_offset, 256);
1794 }
1795 other => panic!("round-trip changed the variant: {other:?}"),
1796 }
1797 }
1798
1799 #[test]
1800 fn owned_sysex_round_trips_in_commands_and_process_output() {
1801 let event = crate::midi::PluginEvent::sysex(vec![0xf0, 0x7d, 1, 2, 0xf7]).at(37);
1802 let command = HostCommand::SendPluginEvent {
1803 event: event.clone(),
1804 };
1805 let json = serde_json::to_string(&command).expect("serialize owned event");
1806 match serde_json::from_str::<HostCommand>(&json).expect("deserialize owned event") {
1807 HostCommand::SendPluginEvent { event: decoded } => assert_eq!(decoded, event),
1808 other => panic!("owned event command changed variant: {other:?}"),
1809 }
1810
1811 let response = HostResponse::AudioOutput {
1812 outputs: Vec::new(),
1813 output_events: vec![event.clone()],
1814 };
1815 let json = serde_json::to_string(&response).expect("serialize owned output");
1816 match serde_json::from_str::<HostResponse>(&json).expect("deserialize owned output") {
1817 HostResponse::AudioOutput { output_events, .. } => {
1818 assert_eq!(output_events, vec![event])
1819 }
1820 other => panic!("owned event response changed variant: {other:?}"),
1821 }
1822 }
1823
1824 #[test]
1825 fn select_program_round_trips_across_the_wire() {
1826 let cmd = HostCommand::SelectProgram {
1828 unit_id: 0,
1829 program_index: 17,
1830 };
1831 let json = serde_json::to_string(&cmd).expect("serialize SelectProgram");
1832 match serde_json::from_str::<HostCommand>(&json).expect("deserialize SelectProgram") {
1833 HostCommand::SelectProgram {
1834 unit_id,
1835 program_index,
1836 } => {
1837 assert_eq!(unit_id, 0);
1838 assert_eq!(program_index, 17);
1839 }
1840 other => panic!("round-trip changed the variant: {other:?}"),
1841 }
1842 }
1843
1844 #[test]
1845 fn transport_commands_round_trip_across_the_wire() {
1846 let tempo = HostCommand::SetTempo { bpm: 137.5 };
1849 let json = serde_json::to_string(&tempo).expect("serialize SetTempo");
1850 match serde_json::from_str::<HostCommand>(&json).expect("deserialize SetTempo") {
1851 HostCommand::SetTempo { bpm } => assert_eq!(bpm, 137.5),
1852 other => panic!("round-trip changed the variant: {other:?}"),
1853 }
1854
1855 let ts = HostCommand::SetTimeSignature {
1856 numerator: 7,
1857 denominator: 8,
1858 };
1859 let json = serde_json::to_string(&ts).expect("serialize SetTimeSignature");
1860 match serde_json::from_str::<HostCommand>(&json).expect("deserialize SetTimeSignature") {
1861 HostCommand::SetTimeSignature {
1862 numerator,
1863 denominator,
1864 } => assert_eq!((numerator, denominator), (7, 8)),
1865 other => panic!("round-trip changed the variant: {other:?}"),
1866 }
1867
1868 let playing = HostCommand::SetPlaying { playing: false };
1869 let json = serde_json::to_string(&playing).expect("serialize SetPlaying");
1870 match serde_json::from_str::<HostCommand>(&json).expect("deserialize SetPlaying") {
1871 HostCommand::SetPlaying { playing } => assert!(!playing),
1872 other => panic!("round-trip changed the variant: {other:?}"),
1873 }
1874 }
1875
1876 #[test]
1877 fn set_bus_active_round_trips_across_the_wire() {
1878 use crate::audio::{BusDirection, MediaType};
1879 let cmd = HostCommand::SetBusActive {
1880 media_type: MediaType::Audio,
1881 direction: BusDirection::Input,
1882 bus_index: 1,
1883 active: true,
1884 };
1885 let json = serde_json::to_string(&cmd).expect("serialize SetBusActive");
1886 match serde_json::from_str::<HostCommand>(&json).expect("deserialize SetBusActive") {
1887 HostCommand::SetBusActive {
1888 media_type,
1889 direction,
1890 bus_index,
1891 active,
1892 } => {
1893 assert_eq!(media_type, MediaType::Audio);
1894 assert_eq!(direction, BusDirection::Input);
1895 assert_eq!(bus_index, 1);
1896 assert!(active);
1897 }
1898 other => panic!("round-trip changed the variant: {other:?}"),
1899 }
1900 }
1901
1902 #[test]
1903 fn bus_arrangements_round_trip_across_the_wire() {
1904 use crate::audio::{BusArrangements, SpeakerArrangement};
1905
1906 let cmd = serde_json::to_string(&HostCommand::BusArrangements)
1907 .expect("serialize BusArrangements");
1908 assert!(matches!(
1909 serde_json::from_str::<HostCommand>(&cmd).expect("deserialize BusArrangements"),
1910 HostCommand::BusArrangements
1911 ));
1912
1913 let set = HostCommand::SetBusArrangements {
1914 inputs: vec![],
1915 outputs: vec![SpeakerArrangement::STEREO],
1916 };
1917 let set_json = serde_json::to_string(&set).expect("serialize SetBusArrangements");
1918 match serde_json::from_str::<HostCommand>(&set_json).expect("deserialize") {
1919 HostCommand::SetBusArrangements { inputs, outputs } => {
1920 assert!(inputs.is_empty());
1921 assert_eq!(outputs, vec![SpeakerArrangement::STEREO]);
1922 }
1923 other => panic!("SetBusArrangements round-trip changed the variant: {other:?}"),
1924 }
1925
1926 let arrangements = BusArrangements {
1927 inputs: vec![],
1928 outputs: vec![SpeakerArrangement::STEREO],
1929 };
1930 let resp = HostResponse::BusArrangements {
1931 arrangements: arrangements.clone(),
1932 };
1933 let resp_json = serde_json::to_string(&resp).expect("serialize BusArrangements response");
1934 match serde_json::from_str::<HostResponse>(&resp_json).expect("deserialize") {
1935 HostResponse::BusArrangements { arrangements: back } => {
1936 assert_eq!(back, arrangements);
1937 }
1938 other => panic!("BusArrangements response round-trip changed the variant: {other:?}"),
1939 }
1940 }
1941
1942 #[test]
1943 fn get_units_round_trips_across_the_wire() {
1944 use crate::plugin::PluginUnit;
1945
1946 let cmd = serde_json::to_string(&HostCommand::GetUnits).expect("serialize GetUnits");
1947 assert!(matches!(
1948 serde_json::from_str::<HostCommand>(&cmd).expect("deserialize GetUnits"),
1949 HostCommand::GetUnits
1950 ));
1951
1952 let units = vec![PluginUnit {
1953 id: 0,
1954 parent_id: -1,
1955 name: "Root".to_string(),
1956 program_list_id: Some(12),
1957 programs: vec!["Init".to_string(), "Lead".to_string()],
1958 }];
1959 let resp = HostResponse::Units {
1960 units: units.clone(),
1961 };
1962 let resp_json = serde_json::to_string(&resp).expect("serialize Units");
1963 match serde_json::from_str::<HostResponse>(&resp_json).expect("deserialize Units") {
1964 HostResponse::Units { units: back } => assert_eq!(back, units),
1965 other => panic!("Units round-trip changed the variant: {other:?}"),
1966 }
1967 }
1968
1969 #[test]
1970 fn latency_and_tail_round_trip_across_the_wire() {
1971 let latency_cmd = serde_json::to_string(&HostCommand::LatencySamples).expect("serialize");
1972 assert!(matches!(
1973 serde_json::from_str::<HostCommand>(&latency_cmd).expect("deserialize"),
1974 HostCommand::LatencySamples
1975 ));
1976 let tail_cmd = serde_json::to_string(&HostCommand::TailSamples).expect("serialize");
1977 assert!(matches!(
1978 serde_json::from_str::<HostCommand>(&tail_cmd).expect("deserialize"),
1979 HostCommand::TailSamples
1980 ));
1981
1982 let latency_resp = HostResponse::LatencySamples { samples: 128 };
1983 let json = serde_json::to_string(&latency_resp).expect("serialize");
1984 match serde_json::from_str::<HostResponse>(&json).expect("deserialize") {
1985 HostResponse::LatencySamples { samples } => assert_eq!(samples, 128),
1986 other => panic!("LatencySamples round-trip changed the variant: {other:?}"),
1987 }
1988
1989 let tail_resp = HostResponse::TailSamples { samples: 44100 };
1990 let json = serde_json::to_string(&tail_resp).expect("serialize");
1991 match serde_json::from_str::<HostResponse>(&json).expect("deserialize") {
1992 HostResponse::TailSamples { samples } => assert_eq!(samples, 44100),
1993 other => panic!("TailSamples round-trip changed the variant: {other:?}"),
1994 }
1995 }
1996
1997 #[test]
1998 fn midi_cc_to_parameter_round_trips_across_the_wire() {
1999 let cmd = HostCommand::MidiCcToParameter {
2000 bus: 0,
2001 channel: 1,
2002 cc: 74,
2003 };
2004 let json = serde_json::to_string(&cmd).expect("serialize MidiCcToParameter");
2005 match serde_json::from_str::<HostCommand>(&json).expect("deserialize") {
2006 HostCommand::MidiCcToParameter { bus, channel, cc } => {
2007 assert_eq!((bus, channel, cc), (0, 1, 74));
2008 }
2009 other => panic!("MidiCcToParameter round-trip changed the variant: {other:?}"),
2010 }
2011
2012 let resp = HostResponse::MidiParameterMapping { id: Some(42) };
2013 let json = serde_json::to_string(&resp).expect("serialize MidiParameterMapping");
2014 match serde_json::from_str::<HostResponse>(&json).expect("deserialize") {
2015 HostResponse::MidiParameterMapping { id } => assert_eq!(id, Some(42)),
2016 other => panic!("MidiParameterMapping round-trip changed the variant: {other:?}"),
2017 }
2018
2019 let none_resp = HostResponse::MidiParameterMapping { id: None };
2020 let json = serde_json::to_string(&none_resp).expect("serialize");
2021 match serde_json::from_str::<HostResponse>(&json).expect("deserialize") {
2022 HostResponse::MidiParameterMapping { id } => assert_eq!(id, None),
2023 other => {
2024 panic!("MidiParameterMapping (None) round-trip changed the variant: {other:?}")
2025 }
2026 }
2027 }
2028
2029 #[test]
2030 fn parameter_id_remapping_round_trips_uid_and_optional_result() {
2031 let uid = "123456789ABCDEF01122334455667788";
2032 let command = HostCommand::RemapParameterId {
2033 old_plugin_uid: uid.to_string(),
2034 old_param_id: 0xDEAD_BEEF,
2035 };
2036 let json = serde_json::to_string(&command).expect("serialize RemapParameterId");
2037 match serde_json::from_str::<HostCommand>(&json).expect("deserialize RemapParameterId") {
2038 HostCommand::RemapParameterId {
2039 old_plugin_uid,
2040 old_param_id,
2041 } => {
2042 assert_eq!(old_plugin_uid, uid);
2043 assert_eq!(old_param_id, 0xDEAD_BEEF);
2044 assert!(crate::internal::utils::parse_class_uid(&old_plugin_uid).is_some());
2045 }
2046 other => panic!("RemapParameterId round-trip changed the variant: {other:?}"),
2047 }
2048
2049 for id in [Some(42), None] {
2050 let response = HostResponse::RemappedParameter { id };
2051 let json = serde_json::to_string(&response).expect("serialize RemappedParameter");
2052 match serde_json::from_str::<HostResponse>(&json)
2053 .expect("deserialize RemappedParameter")
2054 {
2055 HostResponse::RemappedParameter { id: decoded } => assert_eq!(decoded, id),
2056 other => panic!("RemappedParameter round-trip changed the variant: {other:?}"),
2057 }
2058 }
2059
2060 let invalid = HostCommand::RemapParameterId {
2061 old_plugin_uid: "1234-not-a-uid".to_string(),
2062 old_param_id: 1,
2063 };
2064 let json = serde_json::to_string(&invalid).expect("serialize invalid UID");
2065 match serde_json::from_str::<HostCommand>(&json).expect("deserialize invalid UID") {
2066 HostCommand::RemapParameterId { old_plugin_uid, .. } => {
2067 assert!(crate::internal::utils::parse_class_uid(&old_plugin_uid).is_none());
2068 }
2069 other => panic!("invalid RemapParameterId changed the variant: {other:?}"),
2070 }
2071 }
2072
2073 #[test]
2074 fn parameter_edits_round_trip_across_the_wire() {
2075 use crate::plugin::{ParameterEdit, ParameterEditKind};
2078
2079 let cmd = serde_json::to_string(&HostCommand::TakeParameterEdits)
2080 .expect("serialize TakeParameterEdits");
2081 assert!(matches!(
2082 serde_json::from_str::<HostCommand>(&cmd).expect("deserialize TakeParameterEdits"),
2083 HostCommand::TakeParameterEdits
2084 ));
2085
2086 let edits = vec![
2087 ParameterEdit {
2088 id: 9,
2089 kind: ParameterEditKind::BeginGesture,
2090 value: None,
2091 },
2092 ParameterEdit {
2093 id: 9,
2094 kind: ParameterEditKind::ValueChange,
2095 value: Some(0.3),
2096 },
2097 ParameterEdit {
2098 id: 9,
2099 kind: ParameterEditKind::EndGesture,
2100 value: None,
2101 },
2102 ];
2103 let resp = HostResponse::ParameterEdits {
2104 edits: edits.clone(),
2105 };
2106 let resp_json = serde_json::to_string(&resp).expect("serialize ParameterEdits");
2107 match serde_json::from_str::<HostResponse>(&resp_json).expect("deserialize ParameterEdits")
2108 {
2109 HostResponse::ParameterEdits { edits: back } => assert_eq!(back, edits),
2110 other => panic!("ParameterEdits round-trip changed the variant: {other:?}"),
2111 }
2112 }
2113
2114 #[test]
2115 fn parameter_feedback_round_trips_losslessly_and_is_bounded() {
2116 let command = serde_json::to_string(&HostCommand::TakeParameterChanges)
2117 .expect("serialize TakeParameterChanges");
2118 assert!(matches!(
2119 serde_json::from_str::<HostCommand>(&command)
2120 .expect("deserialize TakeParameterChanges"),
2121 HostCommand::TakeParameterChanges
2122 ));
2123
2124 let changes = vec![
2125 (1, 0.25),
2126 (2, -0.0),
2127 (3, f64::NAN),
2128 (4, f64::INFINITY),
2129 (5, f64::NEG_INFINITY),
2130 ];
2131 let response = HostResponse::ParameterChanges {
2132 changes: changes.clone(),
2133 };
2134 let json = serde_json::to_string(&response).expect("serialize parameter feedback");
2135 let HostResponse::ParameterChanges { changes: decoded } =
2136 serde_json::from_str::<HostResponse>(&json).expect("deserialize parameter feedback")
2137 else {
2138 panic!("parameter feedback changed response variant");
2139 };
2140 assert_eq!(
2141 decoded
2142 .iter()
2143 .map(|&(id, value)| (id, value.to_bits()))
2144 .collect::<Vec<_>>(),
2145 changes
2146 .iter()
2147 .map(|&(id, value)| (id, value.to_bits()))
2148 .collect::<Vec<_>>()
2149 );
2150
2151 let over_limit = HostResponse::ParameterChanges {
2152 changes: vec![(1, 0.5); MAX_WIRE_PARAMETER_CHANGES + 1],
2153 };
2154 assert!(
2155 serde_json::to_string(&over_limit).is_err(),
2156 "the helper must not emit an oversized feedback response"
2157 );
2158
2159 let entries = (0..=MAX_WIRE_PARAMETER_CHANGES)
2160 .map(|_| "[1,0]")
2161 .collect::<Vec<_>>()
2162 .join(",");
2163 let oversized_json = format!("{{\"ParameterChanges\":{{\"changes\":[{entries}]}}}}");
2164 assert!(
2165 serde_json::from_str::<HostResponse>(&oversized_json).is_err(),
2166 "the host must reject oversized feedback before collecting it"
2167 );
2168 }
2169
2170 #[test]
2171 fn host_notifications_and_restart_requests_round_trip_across_the_wire() {
2172 use crate::plugin::HostNotification;
2173
2174 for command in [
2175 HostCommand::TakeHostNotifications,
2176 HostCommand::ExecuteContextMenuItem {
2177 menu_id: 19,
2178 item_id: 3,
2179 },
2180 HostCommand::DismissContextMenu { menu_id: 20 },
2181 HostCommand::TakeRestartFlags,
2182 HostCommand::ServiceHostRequests,
2183 ] {
2184 let json = serde_json::to_string(&command).expect("serialize host request");
2185 let decoded = serde_json::from_str::<HostCommand>(&json).expect("deserialize");
2186 assert_eq!(
2187 std::mem::discriminant(&decoded),
2188 std::mem::discriminant(&command)
2189 );
2190 }
2191
2192 let notifications = vec![
2193 HostNotification::DirtyChanged(true),
2194 HostNotification::OpenEditorRequested {
2195 name: Some("editor".to_string()),
2196 },
2197 HostNotification::GroupEditStarted,
2198 HostNotification::GroupEditFinished,
2199 HostNotification::ContextMenuRequested {
2200 menu_id: 19,
2201 parameter_id: Some(44),
2202 x: 12,
2203 y: 24,
2204 items: vec![crate::plugin::ContextMenuItem {
2205 item_id: 0,
2206 name: "Reset".to_string(),
2207 tag: 7,
2208 flags: 0,
2209 }],
2210 },
2211 ];
2212 let response = HostResponse::HostNotifications {
2213 notifications: notifications.clone(),
2214 };
2215 let json = serde_json::to_string(&response).expect("serialize notifications");
2216 match serde_json::from_str::<HostResponse>(&json).expect("deserialize notifications") {
2217 HostResponse::HostNotifications {
2218 notifications: decoded,
2219 } => assert_eq!(decoded, notifications),
2220 other => panic!("HostNotifications changed variant: {other:?}"),
2221 }
2222
2223 let response = HostResponse::RestartFlags { bits: 0x345 };
2224 let json = serde_json::to_string(&response).expect("serialize restart flags");
2225 match serde_json::from_str::<HostResponse>(&json).expect("deserialize restart flags") {
2226 HostResponse::RestartFlags { bits } => assert_eq!(bits, 0x345),
2227 other => panic!("RestartFlags changed variant: {other:?}"),
2228 }
2229 }
2230
2231 #[test]
2232 fn note_expression_commands_round_trip_across_the_wire() {
2233 use crate::midi::{NoteExpressionInfo, NoteExpressionType};
2235
2236 let on = HostCommand::NoteOn {
2237 channel: 0,
2238 note: 60,
2239 velocity: 100,
2240 sample_offset: 0,
2241 };
2242 let on_json = serde_json::to_string(&on).expect("serialize NoteOn");
2243 match serde_json::from_str::<HostCommand>(&on_json).expect("deserialize NoteOn") {
2244 HostCommand::NoteOn {
2245 channel,
2246 note,
2247 velocity,
2248 sample_offset,
2249 } => {
2250 assert_eq!((channel, note, velocity, sample_offset), (0, 60, 100, 0));
2251 }
2252 other => panic!("NoteOn round-trip changed the variant: {other:?}"),
2253 }
2254
2255 let expr = HostCommand::SendNoteExpression {
2256 note_id: 7,
2257 kind: NoteExpressionType::Tuning,
2258 value: 1.0,
2259 sample_offset: 0,
2260 };
2261 let expr_json = serde_json::to_string(&expr).expect("serialize SendNoteExpression");
2262 match serde_json::from_str::<HostCommand>(&expr_json).expect("deserialize") {
2263 HostCommand::SendNoteExpression {
2264 note_id,
2265 kind,
2266 value,
2267 ..
2268 } => {
2269 assert_eq!(note_id, 7);
2270 assert_eq!(kind, NoteExpressionType::Tuning);
2271 assert_eq!(value, 1.0);
2272 }
2273 other => panic!("SendNoteExpression round-trip changed the variant: {other:?}"),
2274 }
2275
2276 let started = HostResponse::NoteStarted { note_id: 42 };
2277 let started_json = serde_json::to_string(&started).expect("serialize NoteStarted");
2278 match serde_json::from_str::<HostResponse>(&started_json).expect("deserialize") {
2279 HostResponse::NoteStarted { note_id } => assert_eq!(note_id, 42),
2280 other => panic!("NoteStarted round-trip changed the variant: {other:?}"),
2281 }
2282
2283 let info = NoteExpressionInfo {
2284 kind: NoteExpressionType::Tuning,
2285 title: "Tuning".to_string(),
2286 short_title: "Tun".to_string(),
2287 units: String::new(),
2288 default_value: 0.5,
2289 min: 0.0,
2290 max: 1.0,
2291 step_count: 0,
2292 is_bipolar: true,
2293 is_one_shot: false,
2294 is_absolute: false,
2295 };
2296 let resp = HostResponse::NoteExpressions {
2297 expressions: vec![info.clone()],
2298 };
2299 let resp_json = serde_json::to_string(&resp).expect("serialize NoteExpressions");
2300 match serde_json::from_str::<HostResponse>(&resp_json).expect("deserialize") {
2301 HostResponse::NoteExpressions { expressions } => {
2302 assert_eq!(expressions, vec![info]);
2303 }
2304 other => panic!("NoteExpressions round-trip changed the variant: {other:?}"),
2305 }
2306 }
2307
2308 #[test]
2309 fn explicit_helper_override_missing_path_reports_clearly() {
2310 let bogus = std::path::PathBuf::from("/nonexistent/vst3-host-helper-xyz");
2314 let err = match PluginHostProcess::new(Some(bogus.clone()), DEFAULT_RESPONSE_TIMEOUT) {
2315 Ok(_) => panic!("a missing override path must error, not spawn"),
2316 Err(e) => e,
2317 };
2318 assert!(
2319 err.contains("does not exist"),
2320 "error should explain the missing path, got: {err}"
2321 );
2322 assert!(
2323 err.contains("vst3-host-helper-xyz"),
2324 "error should name the offending path, got: {err}"
2325 );
2326 }
2327
2328 #[test]
2329 fn non_finite_samples_survive_the_audio_wire_format() {
2330 let channel = vec![
2334 f32::NAN,
2335 f32::INFINITY,
2336 f32::NEG_INFINITY,
2337 -0.0,
2338 0.5,
2339 f32::MIN_POSITIVE,
2340 ];
2341 let resp = HostResponse::AudioOutput {
2342 outputs: vec![channel.clone(), vec![]],
2343 output_events: Vec::new(),
2344 };
2345 let json = serde_json::to_string(&resp).expect("serialize");
2346 assert!(
2347 !json.contains("null"),
2348 "non-finite samples must not become null"
2349 );
2350 match serde_json::from_str::<HostResponse>(&json).expect("deserialize") {
2351 HostResponse::AudioOutput { outputs, .. } => {
2352 assert_eq!(outputs.len(), 2);
2353 assert!(outputs[1].is_empty());
2354 let bits: Vec<u32> = outputs[0].iter().map(|s| s.to_bits()).collect();
2355 let want: Vec<u32> = channel.iter().map(|s| s.to_bits()).collect();
2356 assert_eq!(bits, want, "samples must round-trip bit-exactly");
2357 }
2358 other => panic!("round-trip changed the variant: {other:?}"),
2359 }
2360
2361 let cmd = HostCommand::Process {
2363 inputs: vec![vec![f32::NAN, 1.0]],
2364 frames: 2,
2365 };
2366 let json = serde_json::to_string(&cmd).expect("serialize Process");
2367 match serde_json::from_str::<HostCommand>(&json).expect("deserialize Process") {
2368 HostCommand::Process { inputs, frames } => {
2369 assert_eq!(frames, 2);
2370 assert!(inputs[0][0].is_nan());
2371 assert_eq!(inputs[0][1], 1.0);
2372 }
2373 other => panic!("Process round-trip changed the variant: {other:?}"),
2374 }
2375 }
2376
2377 #[test]
2378 fn bus_audio_wire_preserves_bus_boundaries_activation_and_sample_bits() {
2379 let command = HostCommand::ProcessBuses {
2380 inputs: vec![
2381 crate::audio::AudioBusBuffer {
2382 active: true,
2383 channels: vec![vec![f32::NAN, 1.0], vec![2.0, 3.0]],
2384 },
2385 crate::audio::AudioBusBuffer {
2386 active: false,
2387 channels: vec![vec![99.0, 99.0]],
2388 },
2389 ],
2390 outputs: vec![
2391 crate::audio::AudioBusConfig {
2392 channel_count: 2,
2393 active: true,
2394 },
2395 crate::audio::AudioBusConfig {
2396 channel_count: 1,
2397 active: false,
2398 },
2399 ],
2400 frames: 2,
2401 };
2402 let json = serde_json::to_string(&command).expect("serialize ProcessBuses");
2403 match serde_json::from_str::<HostCommand>(&json).expect("deserialize ProcessBuses") {
2404 HostCommand::ProcessBuses {
2405 inputs,
2406 outputs,
2407 frames,
2408 } => {
2409 assert_eq!(frames, 2);
2410 assert_eq!(inputs.len(), 2);
2411 assert!(inputs[0].active);
2412 assert!(!inputs[1].active);
2413 assert!(inputs[0].channels[0][0].is_nan());
2414 assert_eq!(inputs[1].channels[0], [99.0, 99.0]);
2415 assert_eq!(outputs[1].channel_count, 1);
2416 assert!(!outputs[1].active);
2417 }
2418 other => panic!("ProcessBuses round-trip changed the variant: {other:?}"),
2419 }
2420 }
2421
2422 #[test]
2423 fn non_finite_parameter_values_survive_the_wire() {
2424 for value in [f64::NAN, f64::INFINITY, f64::NEG_INFINITY] {
2427 let json = serde_json::to_string(&HostResponse::ParameterValue { value })
2428 .expect("serialize ParameterValue");
2429 match serde_json::from_str::<HostResponse>(&json).expect("deserialize") {
2430 HostResponse::ParameterValue { value: back } => {
2431 if value.is_nan() {
2432 assert!(back.is_nan(), "NaN must survive the wire");
2433 } else {
2434 assert_eq!(back, value);
2435 }
2436 }
2437 other => panic!("round-trip changed the variant: {other:?}"),
2438 }
2439 }
2440
2441 let json = serde_json::to_string(&HostResponse::ParameterValue { value: 0.25 })
2442 .expect("serialize");
2443 assert!(
2444 json.contains("0.25") && !json.contains("\"0.25\""),
2445 "finite values stay JSON numbers, got {json}"
2446 );
2447 let cmd = HostCommand::SetParameter {
2448 id: 3,
2449 value: f64::NAN,
2450 };
2451 let json = serde_json::to_string(&cmd).expect("serialize SetParameter");
2452 match serde_json::from_str::<HostCommand>(&json).expect("deserialize") {
2453 HostCommand::SetParameter { id, value } => {
2454 assert_eq!(id, 3);
2455 assert!(value.is_nan());
2456 }
2457 other => panic!("SetParameter round-trip changed the variant: {other:?}"),
2458 }
2459 }
2460
2461 #[test]
2462 fn audio_codec_round_trips_and_shrinks_the_payload() {
2463 for len in 0..9usize {
2465 let samples: Vec<f32> = (0..len).map(|i| i as f32 * -0.3125).collect();
2466 let encoded = audio_codec::encode_channel(&samples);
2467 let decoded = audio_codec::decode_channel(&encoded).expect("decode");
2468 assert_eq!(decoded, samples, "round-trip failed at len {len}");
2469 }
2470 assert!(audio_codec::decode_channel("!!!!").is_none());
2471 assert!(audio_codec::decode_channel("AAA").is_none(), "bad length");
2472 assert!(
2473 audio_codec::decode_channel("AAAA").is_none(),
2474 "3 bytes is not a whole f32"
2475 );
2476
2477 let block: Vec<Vec<f32>> = (0..2)
2479 .map(|c| {
2480 (0..512)
2481 .map(|i| ((i * 7 + c) as f32 / 512.0).sin())
2482 .collect()
2483 })
2484 .collect();
2485 let plain = serde_json::to_string(&block).expect("plain json").len();
2486 let encoded = serde_json::to_string(&HostResponse::AudioOutput {
2487 outputs: block,
2488 output_events: Vec::new(),
2489 })
2490 .expect("encoded json")
2491 .len();
2492 assert!(
2493 encoded < plain,
2494 "base64 payload ({encoded}) should be smaller than the number array ({plain})"
2495 );
2496 }
2497
2498 #[test]
2499 fn wire_provided_counts_are_clamped_on_receipt() {
2500 let json = r#"{"PluginInfo":{"vendor":"v","name":"n","version":"1","category":"",
2503 "uid":"u","has_gui":false,"audio_inputs":-4,"audio_outputs":999999,
2504 "output_channels":2000000,"has_midi_input":true,"has_midi_output":false}}"#;
2505 match serde_json::from_str::<HostResponse>(json).expect("deserialize PluginInfo") {
2506 HostResponse::PluginInfo {
2507 audio_inputs,
2508 audio_outputs,
2509 output_channels,
2510 ..
2511 } => {
2512 assert_eq!(audio_inputs, 0);
2513 assert_eq!(audio_outputs, MAX_WIRE_BUSES);
2514 assert_eq!(output_channels, MAX_WIRE_CHANNELS as i32);
2515 }
2516 other => panic!("PluginInfo round-trip changed the variant: {other:?}"),
2517 }
2518
2519 let channels: Vec<String> = (0..MAX_WIRE_CHANNELS + 5)
2521 .map(|_| audio_codec::encode_channel(&[0.0]))
2522 .collect();
2523 let json = serde_json::to_string(&serde_json::json!({
2524 "AudioOutput": { "outputs": channels, "output_events": [] }
2525 }))
2526 .expect("serialize");
2527 match serde_json::from_str::<HostResponse>(&json).expect("deserialize") {
2528 HostResponse::AudioOutput { outputs, .. } => {
2529 assert_eq!(outputs.len(), MAX_WIRE_CHANNELS)
2530 }
2531 other => panic!("AudioOutput round-trip changed the variant: {other:?}"),
2532 }
2533 }
2534
2535 #[test]
2536 fn oversized_lines_are_discarded_rather_than_buffered() {
2537 let mut input: Vec<u8> = Vec::new();
2539 input.extend_from_slice(b"short\n");
2540 input.extend_from_slice(&[b'x'; 64]);
2541 input.push(b'\n');
2542 input.extend_from_slice(b"ok\n");
2543 let mut reader = std::io::BufReader::new(std::io::Cursor::new(input));
2544
2545 assert!(matches!(read_bounded_line(&mut reader, 8), ReadLine::Line(l) if l == b"short\n"));
2546 assert!(matches!(
2547 read_bounded_line(&mut reader, 8),
2548 ReadLine::Oversized
2549 ));
2550 assert!(matches!(read_bounded_line(&mut reader, 8), ReadLine::Line(l) if l == b"ok\n"));
2551 assert!(matches!(read_bounded_line(&mut reader, 8), ReadLine::Eof));
2552 }
2553
2554 #[test]
2555 fn slow_commands_are_classified_apart_from_the_per_block_ones() {
2556 assert!(is_slow_command(&HostCommand::SaveState));
2557 assert!(is_slow_command(&HostCommand::LoadState {
2558 data: vec![],
2559 context: crate::plugin::StateContext::Project,
2560 }));
2561 assert!(is_slow_command(&HostCommand::LoadPlugin {
2562 path: "x".into(),
2563 sample_rate: 44100.0,
2564 block_size: 512,
2565 tempo: 120.0,
2566 time_sig_numerator: 4,
2567 time_sig_denominator: 4,
2568 class_id: None,
2569 }));
2570 assert!(!is_slow_command(&HostCommand::Process {
2571 inputs: vec![],
2572 frames: 64
2573 }));
2574 assert!(!is_slow_command(&HostCommand::GetAllParameters));
2575 }
2576
2577 #[cfg(unix)]
2580 #[test]
2581 fn hung_helper_times_out_and_is_killed_not_blocking() {
2582 use std::io::Write;
2583 use std::os::unix::fs::PermissionsExt;
2584 use std::time::{Duration, Instant};
2585
2586 let dir = std::env::temp_dir().join(format!("vst3_hang_{}", std::process::id()));
2588 std::fs::create_dir_all(&dir).unwrap();
2589 let fake = dir.join("hung-helper");
2590 let mut f = std::fs::File::create(&fake).unwrap();
2591 writeln!(f, "#!/bin/sh\nexec sleep 30").unwrap();
2594 drop(f);
2595 std::fs::set_permissions(&fake, std::fs::Permissions::from_mode(0o755)).unwrap();
2596
2597 let mut proc =
2598 PluginHostProcess::spawn(fake.clone(), Duration::from_millis(200)).expect("spawn");
2599 let started = Instant::now();
2600 let res = proc.send_command(HostCommand::Shutdown);
2601 let elapsed = started.elapsed();
2602
2603 assert!(
2604 res.is_err(),
2605 "a hung helper must yield an error, got {res:?}"
2606 );
2607 assert!(
2608 elapsed < Duration::from_secs(3),
2609 "send_command must return promptly on timeout, took {elapsed:?}"
2610 );
2611 assert!(proc.send_command(HostCommand::Shutdown).is_err());
2613
2614 let _ = std::fs::remove_dir_all(&dir);
2615 }
2616
2617 #[cfg(unix)]
2621 struct FlakyLoadHelper {
2622 dir: std::path::PathBuf,
2623 script: std::path::PathBuf,
2624 attempts: std::path::PathBuf,
2625 }
2626
2627 #[cfg(unix)]
2628 impl FlakyLoadHelper {
2629 fn new(name: &str, crashing_loads: u32) -> Self {
2630 use std::io::Write;
2631 use std::os::unix::fs::PermissionsExt;
2632
2633 let dir = std::env::temp_dir().join(format!(
2634 "vst3_flaky_{name}_{}_{:?}",
2635 std::process::id(),
2636 std::thread::current().id()
2637 ));
2638 let _ = std::fs::remove_dir_all(&dir);
2639 std::fs::create_dir_all(&dir).expect("temp dir");
2640 let script = dir.join("flaky-helper");
2641 let attempts = dir.join("attempts");
2642
2643 let mut f = std::fs::File::create(&script).expect("create script");
2644 write!(
2648 f,
2649 "#!/bin/sh\n\
2650 while IFS= read -r line; do\n\
2651 \x20 case \"$line\" in\n\
2652 \x20 *LoadPlugin*)\n\
2653 \x20 n=$(cat '{attempts}' 2>/dev/null || echo 0)\n\
2654 \x20 n=$((n+1))\n\
2655 \x20 printf '%s' \"$n\" > '{attempts}'\n\
2656 \x20 if [ \"$n\" -le {crashing_loads} ]; then exit 3; fi\n\
2657 \x20 printf '%s\\n' '{{\"PluginInfo\":{{\"vendor\":\"v\",\"name\":\"n\",\"version\":\"1\",\"category\":\"\",\"uid\":\"u\",\"has_gui\":false,\"audio_inputs\":0,\"audio_outputs\":1,\"output_channels\":2,\"has_midi_input\":true,\"has_midi_output\":false}}}}' ;;\n\
2658 \x20 *) exit 3 ;;\n\
2659 \x20 esac\n\
2660 done\n",
2661 attempts = attempts.display(),
2662 crashing_loads = crashing_loads,
2663 )
2664 .expect("write script");
2665 drop(f);
2666 std::fs::set_permissions(&script, std::fs::Permissions::from_mode(0o755))
2667 .expect("chmod");
2668 Self {
2669 dir,
2670 script,
2671 attempts,
2672 }
2673 }
2674
2675 fn load_attempts(&self) -> u32 {
2676 std::fs::read_to_string(&self.attempts)
2677 .ok()
2678 .and_then(|s| s.trim().parse().ok())
2679 .unwrap_or(0)
2680 }
2681
2682 fn load_command() -> HostCommand {
2683 HostCommand::LoadPlugin {
2684 path: "/tmp/flaky.vst3".to_string(),
2685 sample_rate: 44100.0,
2686 block_size: 512,
2687 tempo: 120.0,
2688 time_sig_numerator: 4,
2689 time_sig_denominator: 4,
2690 class_id: None,
2691 }
2692 }
2693 }
2694
2695 #[cfg(unix)]
2696 impl Drop for FlakyLoadHelper {
2697 fn drop(&mut self) {
2698 let _ = std::fs::remove_dir_all(&self.dir);
2699 }
2700 }
2701
2702 #[cfg(unix)]
2706 #[test]
2707 fn a_load_that_kills_the_helper_is_replayed_against_a_fresh_one() {
2708 let fake = FlakyLoadHelper::new("recovers", 1);
2709 let mut proc = PluginHostProcess::spawn(fake.script.clone(), Duration::from_secs(5))
2710 .expect("spawn flaky helper");
2711 let first_pid = proc.helper_pid().expect("helper pid");
2712
2713 let response = proc
2714 .send_command(FlakyLoadHelper::load_command())
2715 .expect("a crashed load must be retried, not reported");
2716 assert!(matches!(response, HostResponse::PluginInfo { .. }));
2717 assert_eq!(fake.load_attempts(), 2, "the load should be tried twice");
2718 assert_ne!(
2719 proc.helper_pid().expect("helper pid after retry"),
2720 first_pid,
2721 "the retry must run against a freshly spawned helper"
2722 );
2723 assert!(proc.is_alive(), "the handle must be usable after the retry");
2724 }
2725
2726 #[cfg(unix)]
2729 #[test]
2730 fn a_load_that_always_crashes_gives_up_after_one_retry() {
2731 let fake = FlakyLoadHelper::new("always", 99);
2732 let mut proc = PluginHostProcess::spawn(fake.script.clone(), Duration::from_secs(5))
2733 .expect("spawn flaky helper");
2734
2735 let error = proc
2736 .send_command(FlakyLoadHelper::load_command())
2737 .expect_err("a load that always crashes must still fail");
2738 assert!(
2739 error.to_lowercase().contains("crash") || error.to_lowercase().contains("exited"),
2740 "the reported failure must still read as a crash, got {error}"
2741 );
2742 assert_eq!(
2743 fake.load_attempts(),
2744 1 + LOAD_CRASH_RETRIES,
2745 "exactly one retry, no more"
2746 );
2747 }
2748
2749 #[cfg(unix)]
2753 #[test]
2754 fn a_crash_on_any_other_command_is_reported_not_retried() {
2755 let fake = FlakyLoadHelper::new("other", 0);
2756 let mut proc = PluginHostProcess::spawn(fake.script.clone(), Duration::from_secs(5))
2757 .expect("spawn flaky helper");
2758 let pid = proc.helper_pid().expect("helper pid");
2759
2760 assert!(
2761 proc.send_command(HostCommand::GetAllParameters).is_err(),
2762 "a helper that dies mid-command must surface as an error"
2763 );
2764 assert_eq!(
2765 proc.helper_pid(),
2766 Some(pid),
2767 "no other command may respawn the helper"
2768 );
2769 assert!(!proc.is_alive());
2770 }
2771}
2772
2773pub mod crash_protection {
2775 use std::panic::catch_unwind;
2776 use std::panic::UnwindSafe;
2777 use std::time::Duration;
2778
2779 #[derive(Debug, Clone, PartialEq)]
2781 pub enum PluginStatus {
2782 Ok,
2784 Crashed(String),
2786 Timeout(Duration),
2788 }
2789
2790 pub fn protected_call<F, R>(f: F) -> Result<R, String>
2792 where
2793 F: FnOnce() -> R + UnwindSafe,
2794 {
2795 catch_unwind(f).map_err(|e| {
2796 if let Some(s) = e.downcast_ref::<&str>() {
2797 format!("Plugin panicked: {}", s)
2798 } else if let Some(s) = e.downcast_ref::<String>() {
2799 format!("Plugin panicked: {}", s)
2800 } else {
2801 "Plugin panicked with unknown error".to_string()
2802 }
2803 })
2804 }
2805}