use std::sync::Arc;
use mineral_protocol::{
CloseReason, MessageBatch, RejectReason, Request, ServerHello, SessionMessage, SessionRequest,
SessionResult, SocketWire, Wire, WireSink, WireSource,
};
use tokio::net::{UnixListener, UnixStream};
use tokio::sync::{Notify, mpsc};
use super::dispatch;
use super::registry::{ConnGuard, ConnRegistry};
use super::subscriptions::Subscriptions;
use crate::client::ClientHandle;
use crate::publisher::DomainPublishers;
#[derive(Debug, thiserror::Error)]
enum ConnectionError {
#[error("IPC handshake send failed")]
Handshake(#[from] mineral_protocol::WireError),
}
const OUTBOUND_CAPACITY: usize = 512;
const OUTBOUND_BATCH: usize = 64;
pub(crate) struct SessionServices {
pub(crate) client: ClientHandle,
pub(crate) registry: Arc<ConnRegistry>,
pub(crate) publishers: DomainPublishers,
pub(crate) shutdown: Arc<Notify>,
pub(crate) on_connect: Arc<dyn Fn() + Send + Sync>,
}
pub(crate) async fn run(
listener: UnixListener,
services: Arc<SessionServices>,
) -> Result<(), crate::Error> {
loop {
let (stream, _addr) = listener.accept().await?;
let conn_id = services.registry.register();
(services.on_connect)();
mineral_log::info!(target: "ipc", conn_id, "client connected");
let client = services.client.for_connection(conn_id);
let guard = ConnGuard {
registry: Arc::clone(&services.registry),
id: conn_id,
client: client.clone(),
};
let services = Arc::clone(&services);
tokio::spawn(async move {
let _guard = guard;
if let Err(e) = handle_connection(stream, conn_id, &services).await {
mineral_log::warn!(target: "ipc", conn_id, error = mineral_log::chain(&e), "connection ended with error");
}
});
}
}
async fn handle_connection(
stream: UnixStream,
conn_id: u64,
services: &SessionServices,
) -> Result<(), ConnectionError> {
let mut wire = Box::new(SocketWire::from_stream(stream));
let Some(info) = handshake(&mut *wire, &services.client).await? else {
return Ok(());
};
services.registry.set_identity(conn_id, info.clone());
let (sink, source) = wire.split();
let (out_tx, out_rx) = mpsc::channel(OUTBOUND_CAPACITY);
let writer = tokio::spawn(writer_loop(sink, out_rx));
read_loop(source, &services.client, services, &out_tx).await;
services.client.connection_closed();
drop(out_tx);
writer.abort();
Ok(())
}
async fn handshake(
wire: &mut dyn Wire,
client: &ClientHandle,
) -> Result<Option<mineral_protocol::ClientInfo>, ConnectionError> {
let batch = match wire.recv().await {
Ok(Some(batch)) => batch,
Ok(None) => return Ok(None),
Err(error) => {
mineral_log::warn!(target: "ipc", error = mineral_log::chain(&error), "握手读失败");
return Ok(None);
}
};
let info = batch.messages.into_iter().find_map(|message| {
if let SessionMessage::Hello(info) = message {
Some(info)
} else {
None
}
});
let Some(info) = info else {
mineral_log::warn!(target: "ipc", "首帧不是 Hello,断开");
return Ok(None);
};
if !info.version_matches() {
mineral_log::warn!(
target: "ipc",
client_version = %info.version,
"client 版本不匹配,拒绝连接"
);
client.record_connection_reject(mineral_stats::RejectReason::VersionMismatch);
let _ = wire
.send(MessageBatch {
messages: vec![
SessionMessage::Welcome(ServerHello::reject(RejectReason::VersionMismatch)),
SessionMessage::Goodbye(CloseReason::Protocol {
detail: "版本不匹配".to_owned(),
}),
],
})
.await;
return Ok(None);
}
wire.send(MessageBatch::one(SessionMessage::Welcome(
ServerHello::accept(),
)))
.await?;
mineral_log::debug!(target: "ipc", client = %info.name, "handshake accepted");
Ok(Some(info))
}
async fn writer_loop(mut sink: Box<dyn WireSink>, mut rx: mpsc::Receiver<SessionMessage>) {
let mut buf = Vec::with_capacity(OUTBOUND_BATCH);
loop {
let count = rx.recv_many(&mut buf, OUTBOUND_BATCH).await;
if count == 0 {
break;
}
let batch = MessageBatch {
messages: std::mem::take(&mut buf),
};
if let Err(error) = sink.send(batch).await {
mineral_log::warn!(target: "ipc", error = mineral_log::chain(&error), "会话写失败,writer 退出");
break;
}
}
let _ = sink.close().await;
}
async fn read_loop(
mut source: Box<dyn WireSource>,
client: &ClientHandle,
services: &SessionServices,
out: &mpsc::Sender<SessionMessage>,
) {
let mut subscriptions = Subscriptions::default();
loop {
let batch = match source.recv().await {
Ok(Some(batch)) => batch,
Ok(None) => break,
Err(error) => {
mineral_log::warn!(target: "ipc", error = mineral_log::chain(&error), "会话读失败");
break;
}
};
for message in batch.messages {
match message {
SessionMessage::Requests(requests) => {
for request in requests {
handle_request(request, client, out, &services.shutdown).await;
}
}
SessionMessage::Subscribe(request) => {
subscriptions.subscribe(&request, client, services, out);
}
SessionMessage::Unsubscribe(id) => {
subscriptions.unsubscribe(id, services.publishers.pcm());
}
SessionMessage::Resync(id) => subscriptions.resync(id),
SessionMessage::Close(_reason) => {
subscriptions.shutdown(services.publishers.pcm());
return;
}
other => {
mineral_log::warn!(target: "ipc", message = ?other, "忽略意外会话消息");
}
}
}
}
subscriptions.shutdown(services.publishers.pcm());
}
async fn handle_request(
request: SessionRequest,
client: &ClientHandle,
out: &mpsc::Sender<SessionMessage>,
shutdown: &Arc<Notify>,
) {
let id = request.id;
if let Some(slow) = dispatch::async_request(&request.request) {
let client = client.clone();
let out = out.clone();
tokio::spawn(async move {
let result = dispatch::execute_async(&client, slow).await;
let _ = out
.send(SessionMessage::Results(vec![SessionResult { id, result }]))
.await;
});
return;
}
let result = match request.request {
Request::SetAudioOutput(target) => match client.set_audio_output(target).await {
Ok(()) => mineral_protocol::OperationResult::Applied,
Err(error) => dispatch::failure(&error, dispatch::audio_failure_kind(&error)),
},
Request::Playlist { op } => match client.playlist_operation(op).await {
Ok(Some(id)) => dispatch::query(mineral_protocol::Response::PlaylistSaved { id }),
Ok(None) => mineral_protocol::OperationResult::Applied,
Err(error) => dispatch::failure(&error, error.kind()),
},
Request::QueueEdit { op } => dispatch::execute_queue_edit(client, op).await,
other => {
let is_shutdown = matches!(other, Request::Shutdown);
let result = dispatch::execute_sync(client, other);
if is_shutdown {
let _ = out
.send(SessionMessage::Results(vec![SessionResult {
id,
result: result.clone(),
}]))
.await;
shutdown.notify_one();
return;
}
result
}
};
let _ = out
.send(SessionMessage::Results(vec![SessionResult { id, result }]))
.await;
}