#[cfg(windows)]
use std::ffi::OsString;
#[cfg(unix)]
use std::io;
#[cfg(unix)]
use std::os::fd::RawFd;
use std::sync::Arc;
#[cfg(unix)]
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, AtomicU64, AtomicUsize, Ordering};
use std::time::Duration;
#[cfg(unix)]
use bytes::Buf;
use bytes::Bytes;
use crossbeam_queue::ArrayQueue;
use microsandbox_utils::wake_pipe::WakePipe;
#[cfg(unix)]
use msb_krun::ConsolePortBackend;
#[cfg(windows)]
use tokio::io::{AsyncReadExt, AsyncWriteExt};
#[cfg(windows)]
use tokio::net::windows::named_pipe::{NamedPipeServer, PipeMode, ServerOptions};
const DEFAULT_QUEUE_BYTE_CAPACITY: usize = 32 * 1024 * 1024;
const MAX_QUEUE_ENTRIES: usize = 8192;
const QUEUE_ENTRY_GRANULE: usize = 4096;
#[cfg(windows)]
const NAMED_PIPE_BRIDGE_BUFFER_SIZE: usize = 8192;
#[cfg(windows)]
const NAMED_PIPE_BRIDGE_WAIT_TIMEOUT: Duration = Duration::from_secs(60);
pub struct ByteQueue {
entries: ArrayQueue<QueuedBytes>,
queued_bytes: Arc<AtomicUsize>,
high_water_bytes: AtomicUsize,
full_events: AtomicU64,
byte_capacity: usize,
}
pub struct QueuedBytes {
bytes: Bytes,
charge: ByteCharge,
}
struct ByteCharge {
queued_bytes: Arc<AtomicUsize>,
remaining: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ByteQueueSnapshot {
pub queued_bytes: usize,
pub high_water_bytes: usize,
pub full_events: u64,
pub capacity: usize,
}
pub struct ConsoleSharedState {
pub(crate) workload_control: Arc<super::workload_control::WorkloadControl>,
pub resident_paused: Arc<std::sync::atomic::AtomicBool>,
pub tx_ring: ByteQueue,
pub rx_ring: ByteQueue,
pub tx_wake: WakePipe,
pub rx_wake: WakePipe,
pub tx_capacity_wake: WakePipe,
pub rx_capacity_wake: WakePipe,
closed: AtomicBool,
}
pub struct AgentConsoleBackend {
#[cfg(unix)]
shared: Arc<ConsoleSharedState>,
#[cfg(unix)]
pending: Mutex<Option<QueuedBytes>>,
#[cfg(unix)]
blocked_write_len: AtomicUsize,
}
#[cfg(windows)]
pub(crate) struct AgentConsolePipeBridge {
task: tokio::task::JoinHandle<()>,
}
impl ConsoleSharedState {
pub fn new() -> Self {
Self::with_capacity(DEFAULT_QUEUE_BYTE_CAPACITY)
}
pub fn with_capacity(byte_capacity: usize) -> Self {
Self {
workload_control: super::workload_control::WorkloadControl::new(),
resident_paused: Arc::new(std::sync::atomic::AtomicBool::new(false)),
tx_ring: ByteQueue::new(byte_capacity),
rx_ring: ByteQueue::new(byte_capacity),
tx_wake: WakePipe::new(),
rx_wake: WakePipe::new(),
tx_capacity_wake: WakePipe::new(),
rx_capacity_wake: WakePipe::new(),
closed: AtomicBool::new(false),
}
}
pub fn close(&self) {
self.workload_control.close();
self.closed.store(true, Ordering::Release);
self.tx_capacity_wake.wake();
self.rx_capacity_wake.wake();
self.tx_wake.wake();
self.rx_wake.wake();
}
pub fn is_closed(&self) -> bool {
self.closed.load(Ordering::Acquire)
}
}
impl ByteQueue {
pub fn new(byte_capacity: usize) -> Self {
let entry_capacity = byte_capacity
.div_ceil(QUEUE_ENTRY_GRANULE)
.clamp(1, MAX_QUEUE_ENTRIES);
Self {
entries: ArrayQueue::new(entry_capacity),
queued_bytes: Arc::new(AtomicUsize::new(0)),
high_water_bytes: AtomicUsize::new(0),
full_events: AtomicU64::new(0),
byte_capacity,
}
}
pub fn push(&self, bytes: impl Into<Bytes>) -> Result<(), Bytes> {
let bytes = bytes.into();
let len = bytes.len();
let reserved_bytes = loop {
let queued = self.queued_bytes.load(Ordering::Acquire);
let Some(next) = queued.checked_add(len) else {
self.full_events.fetch_add(1, Ordering::Relaxed);
return Err(bytes);
};
if next > self.byte_capacity {
self.full_events.fetch_add(1, Ordering::Relaxed);
return Err(bytes);
}
if self
.queued_bytes
.compare_exchange_weak(queued, next, Ordering::AcqRel, Ordering::Acquire)
.is_ok()
{
break next;
}
};
self.high_water_bytes
.fetch_max(reserved_bytes, Ordering::Relaxed);
let queued = QueuedBytes {
bytes,
charge: ByteCharge {
queued_bytes: Arc::clone(&self.queued_bytes),
remaining: len,
},
};
if let Err(queued) = self.entries.push(queued) {
self.full_events.fetch_add(1, Ordering::Relaxed);
return Err(queued.into_unqueued());
}
Ok(())
}
pub fn pop(&self) -> Option<QueuedBytes> {
self.entries.pop()
}
pub fn can_fit(&self, len: usize) -> bool {
self.queued_bytes
.load(Ordering::Acquire)
.checked_add(len)
.is_some_and(|next| next <= self.byte_capacity)
&& !self.entries.is_full()
}
pub fn queued_bytes(&self) -> usize {
self.queued_bytes.load(Ordering::Acquire)
}
pub fn capacity(&self) -> usize {
self.byte_capacity
}
pub fn snapshot(&self) -> ByteQueueSnapshot {
ByteQueueSnapshot {
queued_bytes: self.queued_bytes(),
high_water_bytes: self.high_water_bytes.load(Ordering::Acquire),
full_events: self.full_events.load(Ordering::Relaxed),
capacity: self.capacity(),
}
}
}
impl QueuedBytes {
pub fn len(&self) -> usize {
self.bytes.len()
}
pub fn is_empty(&self) -> bool {
self.bytes.is_empty()
}
#[cfg(unix)]
fn copy_prefix_into(&mut self, out: &mut [u8]) -> usize {
let len = self.len().min(out.len());
out[..len].copy_from_slice(&self.bytes[..len]);
self.bytes.advance(len);
self.charge.release(len);
len
}
fn into_unqueued(mut self) -> Bytes {
let remaining = self.charge.remaining;
self.charge.release(remaining);
std::mem::take(&mut self.bytes)
}
}
impl AsRef<[u8]> for QueuedBytes {
fn as_ref(&self) -> &[u8] {
&self.bytes
}
}
impl std::ops::Deref for QueuedBytes {
type Target = [u8];
fn deref(&self) -> &Self::Target {
self.as_ref()
}
}
impl ByteCharge {
fn release(&mut self, bytes: usize) {
debug_assert!(bytes <= self.remaining);
if bytes == 0 {
return;
}
self.remaining -= bytes;
self.queued_bytes.fetch_sub(bytes, Ordering::AcqRel);
}
}
impl Drop for ByteCharge {
fn drop(&mut self) {
self.release(self.remaining);
}
}
impl AgentConsoleBackend {
pub fn new(shared: Arc<ConsoleSharedState>) -> Self {
#[cfg(unix)]
{
Self {
shared,
pending: Mutex::new(None),
blocked_write_len: AtomicUsize::new(0),
}
}
#[cfg(windows)]
{
let _ = shared;
Self {}
}
}
}
#[cfg(windows)]
impl AgentConsolePipeBridge {
pub(crate) fn spawn(
pipe_name: impl Into<OsString>,
shared: Arc<ConsoleSharedState>,
handle: &tokio::runtime::Handle,
) -> std::io::Result<Self> {
let pipe_name = pipe_name.into();
let server = {
let _guard = handle.enter();
ServerOptions::new()
.first_pipe_instance(true)
.pipe_mode(PipeMode::Byte)
.create(&pipe_name)?
};
let task = handle.spawn(async move {
if let Err(error) = run_agent_console_pipe_bridge(server, shared).await {
tracing::warn!(error = %error, "agent console named-pipe bridge stopped");
}
});
Ok(Self { task })
}
}
impl Default for ConsoleSharedState {
fn default() -> Self {
Self::new()
}
}
#[cfg(windows)]
impl Drop for AgentConsolePipeBridge {
fn drop(&mut self) {
self.task.abort();
}
}
#[cfg(unix)]
impl ConsolePortBackend for AgentConsoleBackend {
fn read(&self, buf: &mut [u8]) -> io::Result<usize> {
self.shared.rx_wake.drain();
if buf.is_empty() {
return Ok(0);
}
let mut pending = self.pending.lock().unwrap();
let mut written = 0;
while written < buf.len() {
if pending.is_none() {
*pending = self.shared.rx_ring.pop();
}
let Some(chunk) = pending.as_mut() else {
break;
};
written += chunk.copy_prefix_into(&mut buf[written..]);
if chunk.is_empty() {
pending.take();
}
}
if written == 0 {
Err(io::ErrorKind::WouldBlock.into())
} else {
self.shared.rx_capacity_wake.wake();
Ok(written)
}
}
fn write(&self, buf: &[u8]) -> io::Result<usize> {
if self.shared.is_closed() {
return Err(io::ErrorKind::BrokenPipe.into());
}
self.shared
.tx_ring
.push(Bytes::copy_from_slice(buf))
.map_err(|_| {
self.blocked_write_len.store(buf.len(), Ordering::Release);
io::Error::from(io::ErrorKind::WouldBlock)
})?;
self.blocked_write_len.store(0, Ordering::Release);
self.shared.tx_wake.wake();
Ok(buf.len())
}
fn read_wake_fd(&self) -> RawFd {
self.shared.rx_wake.as_raw_fd()
}
fn wait_until_writable(&self) {
loop {
let blocked_len = self.blocked_write_len.load(Ordering::Acquire).max(1);
if self.shared.is_closed() || self.shared.tx_ring.can_fit(blocked_len) {
return;
}
self.shared.tx_capacity_wake.drain();
if self.shared.is_closed() || self.shared.tx_ring.can_fit(blocked_len) {
return;
}
let _ = self
.shared
.tx_capacity_wake
.wait_timeout(Duration::from_secs(60));
}
}
}
#[cfg(windows)]
async fn run_agent_console_pipe_bridge(
server: NamedPipeServer,
shared: Arc<ConsoleSharedState>,
) -> std::io::Result<()> {
server.connect().await?;
tracing::debug!("agent console named-pipe bridge connected");
let (reader, writer) = tokio::io::split(server);
let reader_shared = Arc::clone(&shared);
let mut reader_task =
tokio::spawn(async move { bridge_guest_to_host(reader, reader_shared).await });
let mut writer_task = tokio::spawn(async move { bridge_host_to_guest(writer, shared).await });
tokio::select! {
result = &mut reader_task => {
writer_task.abort();
result.map_err(std::io::Error::other)?
}
result = &mut writer_task => {
reader_task.abort();
result.map_err(std::io::Error::other)?
}
}
}
#[cfg(windows)]
async fn bridge_guest_to_host(
mut reader: tokio::io::ReadHalf<NamedPipeServer>,
shared: Arc<ConsoleSharedState>,
) -> std::io::Result<()> {
let mut buf = vec![0u8; NAMED_PIPE_BRIDGE_BUFFER_SIZE];
loop {
if shared.is_closed() {
return Ok(());
}
let n = reader.read(&mut buf).await?;
if n == 0 {
return Ok(());
}
push_queue_lossless(Arc::clone(&shared), Bytes::copy_from_slice(&buf[..n])).await;
shared.tx_wake.wake();
}
}
#[cfg(windows)]
async fn bridge_host_to_guest(
mut writer: tokio::io::WriteHalf<NamedPipeServer>,
shared: Arc<ConsoleSharedState>,
) -> std::io::Result<()> {
loop {
if shared.is_closed() {
return Ok(());
}
let mut wrote = false;
while let Some(chunk) = shared.rx_ring.pop() {
writer.write_all(&chunk).await?;
drop(chunk);
shared.rx_capacity_wake.wake();
wrote = true;
}
if wrote {
writer.flush().await?;
continue;
}
shared.rx_wake.drain();
if shared.rx_ring.queued_bytes() != 0 {
continue;
}
let shared_for_wait = Arc::clone(&shared);
let _ = tokio::task::spawn_blocking(move || {
shared_for_wait
.rx_wake
.wait_timeout(NAMED_PIPE_BRIDGE_WAIT_TIMEOUT)
})
.await;
}
}
#[cfg(windows)]
async fn push_queue_lossless(shared: Arc<ConsoleSharedState>, mut chunk: Bytes) {
loop {
match shared.tx_ring.push(chunk) {
Ok(()) => return,
Err(returned) => {
chunk = returned;
if shared.is_closed() {
return;
}
shared.tx_capacity_wake.drain();
if shared.tx_ring.can_fit(chunk.len()) {
continue;
}
let shared_for_wait = Arc::clone(&shared);
let _ = tokio::task::spawn_blocking(move || {
shared_for_wait
.tx_capacity_wake
.wait_timeout(NAMED_PIPE_BRIDGE_WAIT_TIMEOUT)
})
.await;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[cfg(unix)]
#[test]
fn backend_write_and_read_roundtrip() {
let shared = Arc::new(ConsoleSharedState::new());
let backend = AgentConsoleBackend::new(Arc::clone(&shared));
assert_eq!(backend.write(b"hello").unwrap(), 5);
let chunk = shared.tx_ring.pop().unwrap();
assert_eq!(chunk.as_ref(), b"hello");
shared.rx_ring.push(b"world".to_vec()).unwrap();
shared.rx_wake.wake();
let mut buf = [0u8; 16];
let n = backend.read(&mut buf).unwrap();
assert_eq!(&buf[..n], b"world");
}
#[cfg(unix)]
#[test]
fn backend_read_empty_returns_would_block() {
let shared = Arc::new(ConsoleSharedState::new());
let backend = AgentConsoleBackend::new(shared);
let mut buf = [0u8; 16];
let err = backend.read(&mut buf).unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
}
#[cfg(unix)]
#[test]
fn backend_read_fills_one_guest_buffer_across_fragments() {
let shared = Arc::new(ConsoleSharedState::new());
let backend = AgentConsoleBackend::new(Arc::clone(&shared));
shared.rx_ring.push(Bytes::from_static(b"inc")).unwrap();
shared.rx_ring.push(Bytes::from_static(b"hdr")).unwrap();
shared.rx_ring.push(Bytes::from_static(b"payload")).unwrap();
let mut first = [0u8; 10];
let n = backend.read(&mut first).unwrap();
assert_eq!(n, first.len());
assert_eq!(&first, b"inchdrpayl");
let mut second = [0u8; 8];
let n = backend.read(&mut second).unwrap();
assert_eq!(&second[..n], b"oad");
assert_eq!(
backend.read(&mut second).unwrap_err().kind(),
io::ErrorKind::WouldBlock
);
}
#[cfg(unix)]
#[test]
fn backend_write_full_returns_would_block() {
let shared = Arc::new(ConsoleSharedState::with_capacity(1));
let backend = AgentConsoleBackend::new(shared);
assert!(backend.write(b"a").is_ok());
let err = backend.write(b"b").unwrap_err();
assert_eq!(err.kind(), io::ErrorKind::WouldBlock);
}
#[test]
fn byte_queue_releases_exact_capacity_on_pop() {
let queue = ByteQueue::new(8);
queue.push(Bytes::from_static(b"12345678")).unwrap();
assert_eq!(queue.queued_bytes(), 8);
assert!(queue.push(Bytes::from_static(b"x")).is_err());
assert_eq!(queue.pop().unwrap().as_ref(), b"12345678");
assert_eq!(queue.queued_bytes(), 0);
queue.push(Bytes::from_static(b"x")).unwrap();
assert_eq!(
queue.snapshot(),
ByteQueueSnapshot {
queued_bytes: 1,
high_water_bytes: 8,
full_events: 1,
capacity: 8,
}
);
}
#[cfg(unix)]
#[test]
fn backend_capacity_wait_sleeps_until_consumer_pops() {
let shared = Arc::new(ConsoleSharedState::with_capacity(1));
let backend = AgentConsoleBackend::new(Arc::clone(&shared));
backend.write(b"a").unwrap();
assert_eq!(
backend.write(b"b").unwrap_err().kind(),
io::ErrorKind::WouldBlock
);
let (done_tx, done_rx) = std::sync::mpsc::channel();
let waiter = std::thread::spawn(move || {
backend.wait_until_writable();
done_tx.send(()).unwrap();
});
assert!(
done_rx.recv_timeout(Duration::from_millis(25)).is_err(),
"waiter returned while the byte queue was still full"
);
shared.tx_ring.pop().unwrap();
shared.tx_capacity_wake.wake();
done_rx.recv_timeout(Duration::from_secs(1)).unwrap();
waiter.join().unwrap();
}
#[cfg(unix)]
#[test]
fn backend_read_drains_rx_wake_pipe() {
let shared = Arc::new(ConsoleSharedState::new());
let backend = AgentConsoleBackend::new(Arc::clone(&shared));
shared.rx_ring.push(b"ping".to_vec()).unwrap();
shared.rx_wake.wake();
let mut pollfd = libc::pollfd {
fd: backend.read_wake_fd(),
events: libc::POLLIN,
revents: 0,
};
let ret = unsafe { libc::poll(&mut pollfd, 1, 0) };
assert_eq!(ret, 1, "wake pipe should be readable before read()");
assert_ne!(pollfd.revents & libc::POLLIN, 0);
let mut buf = [0u8; 8];
let n = backend.read(&mut buf).unwrap();
assert_eq!(&buf[..n], b"ping");
pollfd.revents = 0;
let ret = unsafe { libc::poll(&mut pollfd, 1, 0) };
assert_eq!(ret, 0, "wake pipe should be drained by read()");
}
#[cfg(windows)]
#[tokio::test]
async fn named_pipe_bridge_exchanges_agent_bytes() {
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::windows::named_pipe::ClientOptions;
let pipe_name = unique_named_pipe("console-bridge");
let shared = Arc::new(ConsoleSharedState::new());
let _bridge = AgentConsolePipeBridge::spawn(
&pipe_name,
Arc::clone(&shared),
&tokio::runtime::Handle::current(),
)
.unwrap();
let mut client = ClientOptions::new().open(&pipe_name).unwrap();
client.write_all(b"guest-ready").await.unwrap();
tokio::time::timeout(Duration::from_secs(1), async {
loop {
if let Some(bytes) = shared.tx_ring.pop() {
assert_eq!(bytes.as_ref(), b"guest-ready");
return;
}
tokio::time::sleep(Duration::from_millis(1)).await;
}
})
.await
.unwrap();
shared.rx_ring.push(b"host-ack".to_vec()).unwrap();
shared.rx_wake.wake();
let mut buf = [0u8; 8];
tokio::time::timeout(Duration::from_secs(1), client.read_exact(&mut buf))
.await
.unwrap()
.unwrap();
assert_eq!(&buf, b"host-ack");
}
#[cfg(windows)]
fn unique_named_pipe(name: &str) -> String {
let id =
std::sync::atomic::AtomicU64::new(0).fetch_add(1, std::sync::atomic::Ordering::Relaxed);
format!(r"\\.\pipe\msb-runtime-{name}-{}-{id}", std::process::id())
}
}