140 lines
5.3 KiB
Rust
140 lines
5.3 KiB
Rust
//! A daemon that loses the platform's MIDI service makes way for a new one.
|
|
//!
|
|
//! When Apple's `MIDIServer` dies, nothing in the process that was using it can reach it again,
|
|
//! not even a new client (R-079). The daemon's answer is to stop serving and be replaced, and the
|
|
//! replacement says why it started.
|
|
|
|
#![allow(
|
|
clippy::expect_used,
|
|
clippy::indexing_slicing,
|
|
clippy::panic,
|
|
clippy::unwrap_used
|
|
)]
|
|
|
|
mod common;
|
|
|
|
use midi_harbor_core::endpoint::Direction;
|
|
use midi_harbor_core::fingerprint::DeviceFingerprint;
|
|
use midi_harbor_daemon::{Daemon, Stopped};
|
|
use midi_harbor_ipc::{HarborClient, pb, transport};
|
|
use midi_harbor_platform::fake::FakeMidiPlatform;
|
|
use midi_harbor_platform::midi::{DiscoveredDevice, MidiPlatform};
|
|
use std::path::PathBuf;
|
|
use std::sync::Arc;
|
|
use std::time::Duration;
|
|
|
|
/// Starts a daemon serving on its own short socket path, returning what `run` resolves to.
|
|
async fn serving(
|
|
label: &str,
|
|
) -> (
|
|
Arc<Daemon>,
|
|
Arc<FakeMidiPlatform>,
|
|
PathBuf,
|
|
tokio::task::JoinHandle<Result<Stopped, String>>,
|
|
) {
|
|
let root = common::scratch("midi-harbor-server-lost")
|
|
.join(format!("{label}-{}", uuid::Uuid::new_v4()));
|
|
let unique = uuid::Uuid::new_v4().simple().to_string();
|
|
let socket = common::scratch("mh-lost").join(format!("{}.sock", &unique[..8]));
|
|
let parent = socket
|
|
.parent()
|
|
.expect("the socket path sits in a scratch directory");
|
|
std::fs::create_dir_all(parent).expect("the socket's scratch directory is created");
|
|
|
|
let platform = Arc::new(FakeMidiPlatform::new());
|
|
let daemon = Daemon::start(
|
|
common::quiet(root).with_socket(Some(socket.clone())),
|
|
Arc::clone(&platform) as Arc<dyn MidiPlatform>,
|
|
)
|
|
.await
|
|
.expect("the daemon starts over a scratch directory");
|
|
let served = Arc::clone(&daemon);
|
|
let running = tokio::spawn(async move {
|
|
midi_harbor_daemon::run(served)
|
|
.await
|
|
.map_err(|error| error.to_string())
|
|
});
|
|
for _ in 0..100 {
|
|
if transport::probe(&socket).await {
|
|
break;
|
|
}
|
|
tokio::time::sleep(Duration::from_millis(20)).await;
|
|
}
|
|
(daemon, platform, socket, running)
|
|
}
|
|
|
|
/// Proves that losing the MIDI server stops the daemon with the reason `Replace` and removes its
|
|
/// socket, even with a window watching its streams, while an ordinary change to the MIDI
|
|
/// environment leaves it serving (R-079).
|
|
///
|
|
/// The socket goes with it, or the replacement would find it and think a daemon is running. An
|
|
/// open window keeps streams open that never finish on their own; stopping waited for them, so
|
|
/// a daemon asked to stop with the window open kept running until it was killed. A device
|
|
/// arriving is refreshed like any change; the wait of a second and a half gives the watcher time
|
|
/// to act on it.
|
|
#[tokio::test(flavor = "multi_thread")]
|
|
async fn losing_the_midi_server_stops_the_daemon_to_be_replaced() {
|
|
for (name, watching) in [("no client", false), ("a window watching", true)] {
|
|
let (daemon, platform, socket, running) = serving("lost").await;
|
|
assert!(
|
|
transport::probe(&socket).await,
|
|
"{name}: the daemon must be serving before the loss"
|
|
);
|
|
|
|
let mut streams = None;
|
|
if watching {
|
|
let mut client = HarborClient::new(
|
|
transport::connect(&socket)
|
|
.await
|
|
.expect("the window connects to the daemon's socket"),
|
|
);
|
|
let state = client
|
|
.watch_state(pb::WatchStateRequest {})
|
|
.await
|
|
.expect("the window opens a state stream");
|
|
let events = client
|
|
.watch_events(pb::WatchEventsRequest { after_id: None })
|
|
.await
|
|
.expect("the window opens an event stream");
|
|
streams = Some((client, state, events));
|
|
}
|
|
|
|
platform.attach(DiscoveredDevice {
|
|
fingerprint: DeviceFingerprint {
|
|
unique_id: Some(9001),
|
|
name: "Pad Controller Port A".to_owned(),
|
|
..DeviceFingerprint::default()
|
|
},
|
|
direction: Direction::Bidirectional,
|
|
claimed_by: None,
|
|
software: false,
|
|
});
|
|
tokio::time::sleep(Duration::from_millis(1_500)).await;
|
|
assert!(
|
|
!running.is_finished(),
|
|
"{name}: a device arriving stopped the daemon"
|
|
);
|
|
assert!(
|
|
transport::probe(&socket).await,
|
|
"{name}: the daemon must still serve after a device arrived"
|
|
);
|
|
|
|
platform.lose_server();
|
|
let stopped = tokio::time::timeout(Duration::from_secs(10), running)
|
|
.await
|
|
.expect("the daemon stops within ten seconds of the MIDI server being lost")
|
|
.expect("the serving task completes")
|
|
.expect("the daemon stops cleanly");
|
|
assert_eq!(
|
|
stopped,
|
|
Stopped::Replace,
|
|
"{name}: a daemon that lost the MIDI server must ask to be replaced"
|
|
);
|
|
assert!(
|
|
daemon.midi_server_lost_at().await.is_some(),
|
|
"{name}: a daemon that lost the MIDI server must say when, for its replacement's warning"
|
|
);
|
|
assert!(!socket.exists(), "{name}: the socket was left behind");
|
|
drop(streams);
|
|
}
|
|
}
|