use std::sync::{Arc, Mutex};
use microsandbox_protocol::{
exec::{ExecOutputAck, ExecStdin},
message::MessageType,
};
use tokio::sync::{Semaphore, SemaphorePermit, mpsc, watch};
use crate::{MicrosandboxError, MicrosandboxResult, agent::AgentClient};
use super::ExecEvent;
pub(super) struct InputWindow {
available: Semaphore,
chunk_size: usize,
started: watch::Sender<bool>,
offsets: Mutex<Offsets>,
closed: watch::Sender<bool>,
}
pub(super) struct InputReservation<'a> {
window: &'a InputWindow,
permit: Option<SemaphorePermit<'a>>,
bytes: u32,
}
#[derive(Default)]
struct Offsets {
sent: u64,
consumed: u64,
}
pub(crate) struct EventReceiver {
events: mpsc::UnboundedReceiver<ExecEvent>,
consumed: Option<watch::Sender<ExecOutputAck>>,
}
impl InputWindow {
pub(super) fn new(bytes: u32, chunk_size: usize) -> Arc<Self> {
Arc::new(Self {
available: Semaphore::new(bytes as usize),
chunk_size,
started: watch::channel(false).0,
offsets: Mutex::new(Offsets::default()),
closed: watch::channel(false).0,
})
}
pub(super) async fn send(
&self,
client: &AgentClient,
id: u32,
data: impl AsRef<[u8]>,
) -> MicrosandboxResult<()> {
let data = data.as_ref();
for chunk in data.chunks(self.chunk_size) {
let reservation = self.reserve(chunk.len() as u32).await?;
client
.send_and_commit(
id,
MessageType::ExecStdin,
&ExecStdin {
data: chunk.to_vec(),
},
|| reservation.commit(),
)
.await?;
}
Ok(())
}
pub(super) async fn reserve(&self, bytes: u32) -> MicrosandboxResult<InputReservation<'_>> {
let permit = self
.available
.acquire_many(bytes)
.await
.map_err(|_| MicrosandboxError::Runtime("execution stdin closed".into()))?;
let mut offsets = self.offsets.lock().unwrap();
offsets.sent = offsets
.sent
.checked_add(u64::from(bytes))
.ok_or_else(|| MicrosandboxError::Runtime("execution stdin offset overflow".into()))?;
Ok(InputReservation {
window: self,
permit: Some(permit),
bytes,
})
}
pub(super) fn acknowledge(&self, consumed: u64) -> bool {
let mut offsets = self.offsets.lock().unwrap();
if consumed < offsets.consumed || consumed > offsets.sent {
return false;
}
self.available
.add_permits((consumed - offsets.consumed) as usize);
offsets.consumed = consumed;
true
}
pub(super) fn mark_started(&self) {
self.started.send_replace(true);
}
pub(super) async fn wait_started(&self) -> MicrosandboxResult<()> {
let mut started = self.started.subscribe();
let mut closed = self.closed.subscribe();
tokio::select! {
result = started.wait_for(|started| *started) => result.map(|_| ()).map_err(|_| stdin_closed()),
_ = closed.wait_for(|closed| *closed) => Err(stdin_closed()),
}
}
pub(super) fn close(&self) {
self.available.close();
self.closed.send_replace(true);
}
}
impl InputReservation<'_> {
pub(super) fn commit(mut self) {
self.permit.take().unwrap().forget();
}
}
impl EventReceiver {
pub(super) fn new(
events: mpsc::UnboundedReceiver<ExecEvent>,
controlled: bool,
) -> (EventReceiver, Option<OutputAcknowledgements>) {
let (consumed, acknowledgements) = if controlled {
let acknowledgements = OutputAcknowledgements::default();
(
Some(acknowledgements.consumed.clone()),
Some(acknowledgements),
)
} else {
(None, None)
};
(EventReceiver { events, consumed }, acknowledgements)
}
pub(crate) async fn recv(&mut self) -> Option<ExecEvent> {
let event = self.events.recv().await?;
if let Some(consumed) = &self.consumed {
consume(consumed, &event);
}
Some(event)
}
}
impl Drop for InputReservation<'_> {
fn drop(&mut self) {
if self.permit.is_some() {
let mut offsets = self.window.offsets.lock().unwrap();
offsets.sent -= u64::from(self.bytes);
}
}
}
impl Drop for EventReceiver {
fn drop(&mut self) {
self.events.close();
if let Some(consumed) = &self.consumed {
while let Ok(event) = self.events.try_recv() {
consume(consumed, &event);
}
}
}
}
fn stdin_closed() -> MicrosandboxError {
MicrosandboxError::Runtime("execution stdin closed".into())
}
pub(super) fn consume(consumed: &watch::Sender<ExecOutputAck>, event: &ExecEvent) {
if let ExecEvent::Stdout(data) | ExecEvent::Stderr(data) = event {
consumed.send_modify(|acknowledgement| {
acknowledgement
.consume(data.len())
.expect("execution output counter overflow");
});
}
}
#[cfg(test)]
mod tests {
use bytes::Bytes;
use microsandbox_protocol::exec::EXEC_OUTPUT_EVENT_OVERHEAD;
use super::*;
#[tokio::test]
async fn unread_output_sends_no_acknowledgement_and_reads_coalesce() {
let (tx, rx) = mpsc::unbounded_channel();
let (mut events, acknowledgements) = EventReceiver::new(rx, true);
let mut acknowledgements = acknowledgements.unwrap().consumed.subscribe();
tx.send(ExecEvent::Started { pid: 1 }).unwrap();
tx.send(ExecEvent::Stdout(Bytes::from_static(b"abc")))
.unwrap();
tx.send(ExecEvent::Stderr(Bytes::from_static(b"defg")))
.unwrap();
assert!(!acknowledgements.has_changed().unwrap());
events.recv().await.unwrap();
assert!(!acknowledgements.has_changed().unwrap());
events.recv().await.unwrap();
events.recv().await.unwrap();
assert_eq!(
acknowledgements.borrow_and_update().consumed,
7 + 2 * u64::from(EXEC_OUTPUT_EVENT_OVERHEAD)
);
tx.send(ExecEvent::Stdout(Bytes::from_static(b"unread")))
.unwrap();
drop(events);
assert!(tx.is_closed());
assert_eq!(
acknowledgements.borrow().consumed,
13 + 3 * u64::from(EXEC_OUTPUT_EVENT_OVERHEAD)
);
}
}
pub(super) use microsandbox_agent_client::OutputAcknowledgements;