//! Virtual ports and devices through the real Windows backend. //! //! Ignored by default because they need Windows MIDI Services: the API Windows carries from its //! late-2026 update, or before then the App SDK runtime. A second backend plays the part of //! another application: it finds the first's ports through WinMM as any program would, and opens //! them. //! //! Run them from the desktop session, as a scheduled task with an interactive logon for example: //! the service does not answer a virtual device created from an SSH session (research R-093). //! The service Windows shipped before its late-2026 update stops answering once a virtual device //! is closed, so there each test runs alone and the Windows MIDI Service is restarted between //! them; the backend gives up on the service rather than wait, so the next test fails at once. //! //! Every test shares the same two backends. Dropping a backend closes its ports at once, and a //! port created within moments of another closing can take its slot and be listed under the old //! name, or not at all (research R-086), which the next test would trip over. Run: //! //! ```text //! cargo test -p midi-harbor-platform --test windows_midi -- --ignored --test-threads 1 //! ``` #![cfg(windows)] #![allow(clippy::expect_used, clippy::panic, clippy::unwrap_used)] use midi_harbor_core::endpoint::Direction; use midi_harbor_core::ids::EndpointId; use midi_harbor_core::midi::{Channel, MidiMessage}; use midi_harbor_core::rtchannel::{self, Drained, RtConsumer}; use midi_harbor_core::stream::SysExEnd; use midi_harbor_platform::MidiPlatform; use midi_harbor_platform::PlatformError; use midi_harbor_platform::midi::{MidiPlatformEvent, VirtualPortSpec}; use std::sync::{Arc, OnceLock}; use std::time::{Duration, Instant}; /// The backend under test, and a second one standing in for another application. fn backends() -> (Arc, Arc) { static BACKENDS: OnceLock<(Arc, Arc)> = OnceLock::new(); let (ours, theirs) = BACKENDS.get_or_init(|| { ( midi_harbor_platform::midi_backend().expect("the real backend"), midi_harbor_platform::midi_backend().expect("a second backend"), ) }); (Arc::clone(ours), Arc::clone(theirs)) } /// Waits for ports this test destroyed to close and for WinMM to catch up, so the next test does /// not meet a list still changing. /// /// Churning ports this fast is something only these tests do; the backend's own handling of a /// moving list is what the waits inside each test exercise. fn settle() { std::thread::sleep(Duration::from_millis(1500)); } /// A port name no other test or program is using. fn unique(label: &str) -> String { format!("Harbor {label} {}", std::process::id()) } fn note(number: u8) -> MidiMessage { MidiMessage::NoteOn { channel: Channel::new(0).unwrap(), note: number, velocity: 100, } } /// How long WinMM may take to list a port the driver created, or to drop one it closed. /// /// Usually well under a second, but a closed port was once still listed fifteen seconds later. const WINMM_SETTLE: Duration = Duration::from_secs(15); /// Waits for a device named `name` to be listed by `backend` in `direction`. /// /// WinMM adds a new port a moment after the driver creates it, and lists its input and output /// halves separately, so one half can be listed before the other. fn wait_for_device( backend: &Arc, name: &str, direction: Direction, ) -> Option { let started = Instant::now(); while started.elapsed() < WINMM_SETTLE { if let Some(device) = backend .list_devices() .expect("devices") .into_iter() .find(|device| device.fingerprint.name == name && device.direction == direction) { return Some(device); } std::thread::sleep(Duration::from_millis(50)); } let listed: Vec = backend .list_devices() .expect("devices") .into_iter() .map(|device| format!("{:?}", device.fingerprint)) .collect(); eprintln!("{name} never appeared; listed: {listed:#?}"); None } /// Sends with `send` until `wanted` arrives on `listening`, returning how many sends it took. /// /// Sent again rather than once, and the count reported, because a message sent in the first /// moments after another application opens a port can be lost in the driver (research R-086). fn sent_until_heard(send: &dyn Fn(), listening: &mut RtConsumer, wanted: &Drained) -> Option { for attempt in 1..=10 { send(); let started = Instant::now(); while started.elapsed() < Duration::from_millis(200) { if listening.drain(64).iter().any(|drained| drained == wanted) { if attempt > 1 { eprintln!("heard after {attempt} sends"); } return Some(attempt); } std::thread::sleep(Duration::from_millis(5)); } } None } /// Locks that a port created through Windows MIDI Services is listed by WinMM to another /// application as one software device in both directions, and carries MIDI each way once opened. /// /// WinMM lists the halves of a port separately; were they not paired, another application would /// see two devices for one port. #[test] #[ignore = "needs Windows MIDI Services"] fn a_created_port_carries_midi_both_ways_to_another_application() { let (ours, theirs) = backends(); let name = unique("Both Ways"); let (into_ours, mut ours_heard) = rtchannel::channel(EndpointId::new()); let (port, _) = ours .create_virtual_port(&VirtualPortSpec::simple(&name), vec![into_ours]) .expect("the port is created"); // The other application sees one device, both ways, belonging to software. let device = wait_for_device(&theirs, &name, Direction::Bidirectional) .expect("the port must be visible through WinMM"); assert_eq!( device.direction, Direction::Bidirectional, "the port's two halves must be one device" ); assert!(device.software, "an application's port is not hardware"); let (into_theirs, mut theirs_heard) = rtchannel::channel(EndpointId::new()); let opened = theirs .open_device_with_sink(&device.fingerprint, Some(into_theirs)) .expect("the other application opens it"); // What they send arrives on our MIDI In. let arrived = Drained::Message { timestamp: 0, message: note(60), }; let send = || theirs.send(opened, &[note(60)]).expect("they send"); assert!( sent_until_heard(&send, &mut ours_heard, &arrived).is_some(), "their note never reached us" ); // What we send out reaches them. let arrived = Drained::Message { timestamp: 0, message: note(62), }; let send = || ours.send(port, &[note(62)]).expect("we send"); assert!( sent_until_heard(&send, &mut theirs_heard, &arrived).is_some(), "our note never reached them" ); theirs.close_device(opened).expect("they close it"); ours.destroy_virtual_port(port) .expect("the port is destroyed"); settle(); } /// Locks that a system-exclusive dump longer than one WinMM input buffer crosses a port whole in /// each direction, carried as UMP type 3 packets and rejoined. /// /// 6,003 bytes: the framing, a manufacturer byte and 6,000 data bytes, so it arrives in pieces. #[test] #[ignore = "needs Windows MIDI Services"] fn system_exclusive_crosses_a_port_whole() { let (ours, theirs) = backends(); let name = unique("Dump"); let (into_ours, mut ours_heard) = rtchannel::channel(EndpointId::new()); let (port, _) = ours .create_virtual_port(&VirtualPortSpec::simple(&name), vec![into_ours]) .expect("the port is created"); let device = wait_for_device(&theirs, &name, Direction::Bidirectional).expect("the port is visible"); let (into_theirs, mut theirs_heard) = rtchannel::channel(EndpointId::new()); let opened = theirs .open_device_with_sink(&device.fingerprint, Some(into_theirs)) .unwrap(); // A note first each way, so the dump is not what meets a port just opened. let warm = Drained::Message { timestamp: 0, message: note(61), }; let send = || theirs.send(opened, &[note(61)]).expect("they send"); assert!( sent_until_heard(&send, &mut ours_heard, &warm).is_some(), "their warming note never reached us" ); let send = || ours.send(port, &[note(61)]).expect("we send"); assert!( sent_until_heard(&send, &mut theirs_heard, &warm).is_some(), "our warming note never reached them" ); // Longer than one WinMM input buffer, so it arrives in pieces and is joined. let mut dump = vec![0xF0, 0x7D]; dump.extend((0..6000u32).map(|i| (i % 128) as u8)); dump.push(0xF7); let collect = |listening: &mut RtConsumer| -> Vec { let started = Instant::now(); let mut bytes = Vec::new(); while started.elapsed() < Duration::from_secs(5) { for drained in listening.drain(64) { if let Drained::SysEx { bytes: piece, end, .. } = drained { bytes.extend(piece); if end != SysExEnd::Open { return bytes; } } } std::thread::sleep(Duration::from_millis(10)); } bytes }; theirs.send_sysex(opened, &dump).expect("they send a dump"); let arrived = collect(&mut ours_heard); assert!( arrived == dump, "their dump arrived as {} bytes", arrived.len() ); ours.send_sysex(port, &dump).expect("we send a dump"); let arrived = collect(&mut theirs_heard); assert!( arrived == dump, "our dump arrived as {} bytes", arrived.len() ); theirs.close_device(opened).unwrap(); ours.destroy_virtual_port(port).unwrap(); settle(); } /// Locks that a port this backend created is not offered back to it as a device. /// /// WinMM lists every port on the machine, ours among them, and routing to our own port through /// WinMM would loop. #[test] #[ignore = "needs Windows MIDI Services"] fn our_own_ports_are_not_offered_back_to_us_as_devices() { let (ours, theirs) = backends(); let name = unique("Own"); let (port, _) = ours .create_virtual_port(&VirtualPortSpec::simple(&name), Vec::new()) .expect("the port is created"); // Seen by another backend first, so its absence from ours is not just WinMM being slow. wait_for_device(&theirs, &name, Direction::Bidirectional) .expect("the port is visible to another application"); let listed = ours .list_devices() .unwrap() .into_iter() .any(|device| device.fingerprint.name == name); assert!(!listed, "our own port was offered as a device"); ours.destroy_virtual_port(port).unwrap(); settle(); } /// Locks that a port with several connectors is listed as one WinMM port per group, each in the /// direction its connectors give it, under either naming Windows MIDI Services uses. /// /// The current service names a port after its connector's function block; the one Windows /// shipped before its late-2026 update names it after its group, "Gr 2" for the second /// (research R-093). #[test] #[ignore = "needs Windows MIDI Services"] fn several_connectors_are_offered_as_numbered_ports_in_the_right_directions() { let (ours, theirs) = backends(); let name = unique("Split"); let spec = VirtualPortSpec { inputs: 2, outputs: 1, ..VirtualPortSpec::simple(&name) }; let (port, _) = ours .create_virtual_port(&spec, Vec::new()) .expect("the port is created"); // The first carries MIDI In 1 and the only MIDI Out; the second only takes MIDI in, which // another application sees as an output to send to. Both are asked of one listing. let by_connector = [ (format!("{name} 1"), Direction::Bidirectional), (format!("{name} 2"), Direction::Output), ]; let by_group = [ (name.clone(), Direction::Bidirectional), (format!("{name} Gr 2"), Direction::Output), ]; let started = Instant::now(); let mut listed = Vec::new(); while started.elapsed() < WINMM_SETTLE { listed = theirs .list_devices() .unwrap() .into_iter() .filter(|device| device.fingerprint.name.starts_with(&name)) .map(|device| (device.fingerprint.name, device.direction)) .collect(); listed.sort_by(|a, b| a.0.cmp(&b.0)); if listed == by_connector || listed == by_group { break; } std::thread::sleep(Duration::from_millis(50)); } assert!( listed == by_connector || listed == by_group, "the port must be listed under one naming or the other, got {listed:?}" ); assert!( ours.send_to(port, 1, &[note(64)]).is_err(), "there is no second MIDI Out" ); ours.destroy_virtual_port(port).unwrap(); settle(); } /// Locks that creating a port under a name another port has is refused as a name conflict. /// /// Windows MIDI Services makes a device's identifier from its name and refuses a second device /// of the same name without saying why, so the backend must refuse it first, as a conflict the /// interface can explain. #[test] #[ignore = "needs Windows MIDI Services"] fn a_name_another_port_has_is_refused_as_a_conflict() { let (ours, _) = backends(); let name = unique("Taken"); let (port, _) = ours .create_virtual_port(&VirtualPortSpec::simple(&name), Vec::new()) .expect("the port is created"); let second = ours.create_virtual_port(&VirtualPortSpec::simple(&name), Vec::new()); assert!( matches!(second, Err(PlatformError::NameConflict(ref taken)) if *taken == name), "a second port of the same name must be refused as a conflict, got {second:?}" ); ours.destroy_virtual_port(port).unwrap(); settle(); } /// Locks that a port another application creates is announced as a setup change, so the /// daemon looks again at once rather than on its next poll. #[test] #[ignore = "needs Windows MIDI Services"] fn another_application_creating_a_port_is_announced() { let (ours, theirs) = backends(); let _ = ours.drain_events(); let (port, _) = theirs .create_virtual_port(&VirtualPortSpec::simple(unique("Arrival")), Vec::new()) .expect("the port is created"); let started = Instant::now(); let mut announced = false; while started.elapsed() < WINMM_SETTLE { if ours .drain_events() .contains(&MidiPlatformEvent::SetupChanged) { announced = true; break; } std::thread::sleep(Duration::from_millis(50)); } theirs.destroy_virtual_port(port).unwrap(); settle(); assert!( announced, "a port another application created was never announced" ); } /// Locks that a port renamed by destroying and creating it is listed under its new name within /// three seconds, and that the old port is not offered back to us meanwhile. /// /// Closed first, the old port's slot went to the new one and WinMM went on showing the old name /// in every process, on some renames and not others (research R-086), so several are done in a /// row. #[test] #[ignore = "needs Windows MIDI Services"] fn a_port_renamed_by_destroying_and_creating_it_shows_its_new_name_at_once() { let (ours, theirs) = backends(); let mut name = unique("Rename 0"); let (mut port, _) = ours .create_virtual_port(&VirtualPortSpec::simple(&name), Vec::new()) .expect("the port is created"); wait_for_device(&theirs, &name, Direction::Bidirectional).expect("the port is visible"); for round in 1..=8 { let before = name; name = unique(&format!("Rename {round}")); ours.destroy_virtual_port(port) .expect("the port is destroyed"); port = ours .create_virtual_port(&VirtualPortSpec::simple(&name), Vec::new()) .expect("the renamed port is created") .0; let started = Instant::now(); let mut seen = false; while !seen && started.elapsed() < Duration::from_secs(3) { seen = theirs .list_devices() .unwrap() .iter() .any(|device| device.fingerprint.name == name); std::thread::sleep(Duration::from_millis(20)); } assert!(seen, "rename {round}: the new name did not appear"); // The old port is ours, so it is not offered back to us while WinMM lingers over it. let offered_back = ours .list_devices() .unwrap() .iter() .any(|device| device.fingerprint.name == before); assert!( !offered_back, "rename {round}: the port we destroyed was offered back as a device" ); } ours.destroy_virtual_port(port).unwrap(); settle(); }