use std::collections::VecDeque;
use std::sync::{Arc, Mutex};
use tokio::sync::mpsc;
use arcbox_virtio_core::error::{Result, VirtioError};
use arcbox_virtio_core::queue::VirtQueue;
use arcbox_virtio_core::{QueueConfig, VirtioDevice, VirtioDeviceId, virtio_bindings};
use crate::{ConsoleIo, StdioConsole};
#[derive(Debug, Clone)]
pub struct ConsoleConfig {
pub cols: u16,
pub rows: u16,
pub max_ports: u32,
pub multiport: bool,
}
impl Default for ConsoleConfig {
fn default() -> Self {
Self {
cols: 80,
rows: 25,
max_ports: 1,
multiport: false,
}
}
}
#[derive(Debug)]
#[allow(dead_code)]
struct ConsolePort {
id: u32,
open: bool,
input_buffer: VecDeque<u8>,
output_buffer: VecDeque<u8>,
}
impl ConsolePort {
fn new(id: u32) -> Self {
Self {
id,
open: false,
input_buffer: VecDeque::with_capacity(4096),
output_buffer: VecDeque::with_capacity(4096),
}
}
fn take_complete_lines(&mut self) -> Vec<String> {
let mut lines = Vec::new();
while let Some(pos) = self.output_buffer.iter().position(|&b| b == b'\n') {
let line: Vec<u8> = self.output_buffer.drain(..=pos).collect();
if let Ok(s) = std::str::from_utf8(&line) {
lines.push(s.trim_end().to_string());
}
}
lines
}
fn take_all_lines(&mut self) -> Vec<String> {
let mut lines = self.take_complete_lines();
if !self.output_buffer.is_empty() {
let rest: Vec<u8> = self.output_buffer.drain(..).collect();
if let Ok(s) = std::str::from_utf8(&rest) {
let trimmed = s.trim_end();
if !trimmed.is_empty() {
lines.push(trimmed.to_string());
}
}
}
lines
}
}
fn emit_console_lines(lines: Vec<String>) {
for line in lines {
tracing::info!(target: "guest_console", "{line}");
}
}
#[allow(dead_code)]
pub struct VirtioConsole {
config: ConsoleConfig,
features: u64,
acked_features: u64,
ports: Vec<ConsolePort>,
rx_queue: Option<VirtQueue>,
tx_queue: Option<VirtQueue>,
io: Option<Arc<Mutex<dyn ConsoleIo>>>,
input_tx: Option<mpsc::UnboundedSender<Vec<u8>>>,
rx_last_avail: u16,
}
impl VirtioConsole {
pub const FEATURE_SIZE: u64 = 1 << 0;
pub const FEATURE_MULTIPORT: u64 = 1 << 1;
pub const FEATURE_EMERG_WRITE: u64 = 1 << 2;
pub const FEATURE_VERSION_1: u64 = 1 << virtio_bindings::virtio_config::VIRTIO_F_VERSION_1;
#[must_use]
pub fn new(config: ConsoleConfig) -> Self {
let mut features = Self::FEATURE_SIZE | Self::FEATURE_EMERG_WRITE | Self::FEATURE_VERSION_1;
if config.multiport {
features |= Self::FEATURE_MULTIPORT;
}
let mut ports = Vec::with_capacity(config.max_ports as usize);
ports.push(ConsolePort::new(0));
Self {
config,
features,
acked_features: 0,
ports,
rx_queue: None,
tx_queue: None,
io: None,
input_tx: None,
rx_last_avail: 0,
}
}
#[must_use]
pub fn with_stdio() -> Self {
let mut console = Self::new(ConsoleConfig::default());
console.io = Some(Arc::new(Mutex::new(StdioConsole)));
console
}
pub fn set_io(&mut self, io: Arc<Mutex<dyn ConsoleIo>>) {
self.io = Some(io);
}
pub fn queue_input(&mut self, data: &[u8]) -> Result<()> {
if let Some(port) = self.ports.first_mut() {
port.input_buffer.extend(data);
Ok(())
} else {
Err(VirtioError::NotReady("No console port".into()))
}
}
#[must_use]
pub fn read_output(&mut self) -> Vec<u8> {
if let Some(port) = self.ports.first_mut() {
port.output_buffer.drain(..).collect()
} else {
Vec::new()
}
}
fn handle_tx(&mut self, data: &[u8]) -> Result<()> {
if let Some(port) = self.ports.first_mut() {
port.output_buffer.extend(data);
}
if let Some(io) = &self.io {
let mut io = io
.lock()
.map_err(|e| VirtioError::Io(format!("Failed to lock I/O: {e}")))?;
io.write(data)
.map_err(|e| VirtioError::Io(format!("Write failed: {e}")))?;
io.flush()
.map_err(|e| VirtioError::Io(format!("Flush failed: {e}")))?;
}
tracing::trace!("Console TX: {} bytes", data.len());
Ok(())
}
#[allow(dead_code)]
fn handle_rx(&mut self, buf: &mut [u8]) -> Result<usize> {
if let Some(port) = self.ports.first_mut() {
if !port.input_buffer.is_empty() {
let len = buf.len().min(port.input_buffer.len());
for (i, byte) in port.input_buffer.drain(..len).enumerate() {
buf[i] = byte;
}
return Ok(len);
}
}
if let Some(io) = &self.io {
let mut io = io
.lock()
.map_err(|e| VirtioError::Io(format!("Failed to lock I/O: {e}")))?;
let n = io
.read(buf)
.map_err(|e| VirtioError::Io(format!("Read failed: {e}")))?;
tracing::trace!("Console RX: {} bytes", n);
return Ok(n);
}
Ok(0)
}
pub fn process_tx_queue(&mut self, memory: &[u8]) -> Result<Vec<(u16, u32)>> {
let mut tx_data: Vec<(u16, Vec<u8>)> = Vec::new();
{
let queue = self
.tx_queue
.as_mut()
.ok_or_else(|| VirtioError::NotReady("TX queue not ready".into()))?;
while let Some((head_idx, chain)) = queue.pop_avail() {
let mut data = Vec::new();
for desc in chain {
if !desc.is_write_only() {
let start = desc.addr as usize;
let end = start + desc.len as usize;
if end <= memory.len() {
data.extend_from_slice(&memory[start..end]);
}
}
}
tx_data.push((head_idx, data));
}
}
let mut completed = Vec::new();
for (head_idx, data) in tx_data {
let len = data.len() as u32;
self.handle_tx(&data)?;
completed.push((head_idx, len));
}
Ok(completed)
}
#[must_use]
pub fn rx_available(&self) -> usize {
self.ports
.first()
.map(|p| p.input_buffer.len())
.unwrap_or(0)
}
pub fn process_rx_queue(
&mut self,
memory: &mut [u8],
queue_config: &QueueConfig,
) -> Result<bool> {
if !queue_config.ready || queue_config.size == 0 {
return Ok(false);
}
if self.rx_available() == 0 {
return Ok(false);
}
let mem = std::sync::Arc::new(unsafe {
arcbox_virtio_core::GuestMemWriter::new(
memory.as_mut_ptr(),
memory.len(),
queue_config.gpa_base as usize,
)
});
let mut queue = arcbox_virtio_core::SplitQueue::new(mem, 0, queue_config, false);
queue.set_last_avail_idx(self.rx_last_avail);
let mut completions: Vec<(u16, u32)> = Vec::new();
while self.rx_available() > 0 {
let Some(chain) = queue.pop_avail() else {
break;
};
let mut written = 0u32;
let mut input_done = false;
for desc in &chain.descriptors {
if desc.is_write() && desc.len > 0 {
if let Some(port) = self.ports.first_mut() {
let n = (desc.len as usize).min(port.input_buffer.len());
if let Some(buf) = unsafe { queue.mem().slice_mut(desc.addr as usize, n) } {
for slot in buf.iter_mut() {
if let Some(b) = port.input_buffer.pop_front() {
*slot = b;
}
}
written += n as u32;
input_done = port.input_buffer.is_empty();
}
}
}
if input_done {
break; }
}
completions.push((chain.head_idx, written));
}
let injected = !completions.is_empty();
queue.push_used_batch(&completions);
self.rx_last_avail = queue.last_avail_idx();
Ok(injected)
}
}
impl VirtioDevice for VirtioConsole {
fn device_id(&self) -> VirtioDeviceId {
VirtioDeviceId::Console
}
fn features(&self) -> u64 {
self.features
}
fn ack_features(&mut self, features: u64) {
self.acked_features = self.features & features;
}
fn read_config(&self, offset: u64, data: &mut [u8]) {
let config_data = [
self.config.cols.to_le_bytes().as_slice(),
&self.config.rows.to_le_bytes(),
&self.config.max_ports.to_le_bytes(),
&0u32.to_le_bytes(), ]
.concat();
let offset = offset as usize;
let len = data.len().min(config_data.len().saturating_sub(offset));
if len > 0 {
data[..len].copy_from_slice(&config_data[offset..offset + len]);
}
}
fn write_config(&mut self, offset: u64, data: &[u8]) {
if offset == 8 && data.len() >= 4 {
let ch = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
if ch != 0 {
if let Some(c) = char::from_u32(ch) {
eprint!("{c}");
}
}
}
}
fn activate(&mut self) -> Result<()> {
self.rx_queue = Some(VirtQueue::new(256)?);
self.tx_queue = Some(VirtQueue::new(256)?);
if let Some(port) = self.ports.first_mut() {
port.open = true;
}
tracing::info!(
"VirtIO console activated: {}x{}, {} ports",
self.config.cols,
self.config.rows,
self.config.max_ports
);
Ok(())
}
fn reset(&mut self) {
self.acked_features = 0;
self.rx_queue = None;
self.tx_queue = None;
self.rx_last_avail = 0;
for port in &mut self.ports {
port.open = false;
port.input_buffer.clear();
emit_console_lines(port.take_all_lines());
}
}
fn process_queue(
&mut self,
queue_idx: u16,
memory: &mut [u8],
queue_config: &QueueConfig,
) -> Result<Vec<(u16, u32)>> {
if queue_idx != 1 {
return Ok(Vec::new());
}
if !queue_config.ready || queue_config.size == 0 {
return Ok(Vec::new());
}
let mem = std::sync::Arc::new(unsafe {
arcbox_virtio_core::GuestMemWriter::new(
memory.as_mut_ptr(),
memory.len(),
queue_config.gpa_base as usize,
)
});
let mut queue = arcbox_virtio_core::SplitQueue::new(mem, queue_idx, queue_config, false);
let start = queue.mem().read_u16(queue_config.used_addr as usize + 2);
queue.set_last_avail_idx(start);
let io = self.io.clone();
let mut completions = Vec::new();
while let Some(chain) = queue.pop_avail() {
let mut total_len = 0u32;
for desc in &chain.descriptors {
if desc.is_write() {
continue;
}
let Some(data) = queue.mem().slice(desc.addr as usize, desc.len as usize) else {
continue;
};
if let Some(io) = &io {
if let Ok(mut g) = io.lock() {
let _ = g.write(data);
let _ = g.flush();
}
}
if let Some(port) = self.ports.first_mut() {
port.output_buffer.extend(data.iter().copied());
emit_console_lines(port.take_complete_lines());
}
total_len += desc.len;
}
completions.push((chain.head_idx, total_len));
}
queue.push_used_batch(&completions);
Ok(completions)
}
}
impl Drop for VirtioConsole {
fn drop(&mut self) {
for port in &mut self.ports {
emit_console_lines(port.take_all_lines());
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::BufferConsole;
#[test]
fn test_console_creation() {
let console = VirtioConsole::new(ConsoleConfig::default());
assert_eq!(console.device_id(), VirtioDeviceId::Console);
assert!(console.features() & VirtioConsole::FEATURE_SIZE != 0);
}
#[test]
fn test_console_config_read() {
let config = ConsoleConfig {
cols: 120,
rows: 40,
max_ports: 4,
multiport: false,
};
let console = VirtioConsole::new(config);
let mut data = [0u8; 8];
console.read_config(0, &mut data);
assert_eq!(u16::from_le_bytes([data[0], data[1]]), 120); assert_eq!(u16::from_le_bytes([data[2], data[3]]), 40); assert_eq!(u32::from_le_bytes([data[4], data[5], data[6], data[7]]), 4);
}
#[test]
fn test_console_input_queue() {
let mut console = VirtioConsole::new(ConsoleConfig::default());
console.activate().unwrap();
console.queue_input(b"test input").unwrap();
assert_eq!(console.rx_available(), 10);
}
#[test]
fn test_console_output() {
let buffer = Arc::new(Mutex::new(BufferConsole::new()));
let mut console = VirtioConsole::new(ConsoleConfig::default());
console.set_io(buffer.clone());
console.activate().unwrap();
console.handle_tx(b"Hello, World!").unwrap();
let output = buffer.lock().unwrap().take_output();
assert_eq!(&output, b"Hello, World!");
}
#[test]
fn test_console_multiport_feature() {
let config = ConsoleConfig {
multiport: true,
..Default::default()
};
let console = VirtioConsole::new(config);
assert!(console.features() & VirtioConsole::FEATURE_MULTIPORT != 0);
}
#[test]
fn test_console_activate_and_reset() {
let mut console = VirtioConsole::new(ConsoleConfig::default());
console.activate().unwrap();
assert!(console.rx_queue.is_some());
assert!(console.tx_queue.is_some());
console.reset();
assert!(console.rx_queue.is_none());
assert!(console.tx_queue.is_none());
assert_eq!(console.acked_features, 0);
}
#[test]
fn test_console_read_output() {
let mut console = VirtioConsole::new(ConsoleConfig::default());
console.activate().unwrap();
let output = console.read_output();
assert!(output.is_empty());
console.handle_tx(b"test output").unwrap();
let output = console.read_output();
assert_eq!(&output, b"test output");
let output2 = console.read_output();
assert!(output2.is_empty());
}
#[test]
fn test_console_queue_input_not_ready() {
let mut console = VirtioConsole::new(ConsoleConfig::default());
console.ports.clear();
let result = console.queue_input(b"test");
assert!(result.is_err());
}
#[test]
fn test_console_config_write() {
let mut console = VirtioConsole::new(ConsoleConfig::default());
let emergency_char = 'X' as u32;
console.write_config(8, &emergency_char.to_le_bytes());
}
#[test]
fn test_console_feature_negotiation() {
let mut console = VirtioConsole::new(ConsoleConfig::default());
let offered = console.features();
assert!(offered & VirtioConsole::FEATURE_VERSION_1 != 0);
console.ack_features(VirtioConsole::FEATURE_SIZE | VirtioConsole::FEATURE_VERSION_1);
assert!(console.acked_features & VirtioConsole::FEATURE_SIZE != 0);
}
#[test]
fn test_console_with_stdio() {
let console = VirtioConsole::with_stdio();
assert!(console.io.is_some());
}
#[test]
fn test_console_config_partial_read() {
let console = VirtioConsole::new(ConsoleConfig {
cols: 80,
rows: 25,
max_ports: 1,
multiport: false,
});
let mut data = [0u8; 2];
console.read_config(0, &mut data);
assert_eq!(u16::from_le_bytes(data), 80);
let mut data2 = [0u8; 2];
console.read_config(2, &mut data2);
assert_eq!(u16::from_le_bytes(data2), 25);
}
#[test]
fn test_console_rx_available_empty() {
let console = VirtioConsole::new(ConsoleConfig::default());
assert_eq!(console.rx_available(), 0);
}
#[test]
fn test_console_process_rx_queue_injects_input() {
const SIZE: u16 = 4;
const DESC: usize = 0x100;
const AVAIL: usize = 0x200;
const USED: usize = 0x300;
const BUF: usize = 0x400;
let mut mem = vec![0u8; 0x600];
mem[DESC..DESC + 8].copy_from_slice(&(BUF as u64).to_le_bytes());
mem[DESC + 8..DESC + 12].copy_from_slice(&16u32.to_le_bytes());
mem[DESC + 12..DESC + 14].copy_from_slice(&2u16.to_le_bytes()); mem[DESC + 14..DESC + 16].copy_from_slice(&0u16.to_le_bytes());
mem[AVAIL + 4..AVAIL + 6].copy_from_slice(&0u16.to_le_bytes());
mem[AVAIL + 2..AVAIL + 4].copy_from_slice(&1u16.to_le_bytes());
let qc = QueueConfig {
desc_addr: DESC as u64,
avail_addr: AVAIL as u64,
used_addr: USED as u64,
size: SIZE,
ready: true,
gpa_base: 0,
};
let mut console = VirtioConsole::new(ConsoleConfig::default());
assert!(!console.process_rx_queue(&mut mem, &qc).unwrap());
console.queue_input(b"hello").unwrap();
let injected = console.process_rx_queue(&mut mem, &qc).unwrap();
assert!(injected);
assert_eq!(&mem[BUF..BUF + 5], b"hello");
assert_eq!(u16::from_le_bytes([mem[USED + 2], mem[USED + 3]]), 1);
assert_eq!(
u32::from_le_bytes([mem[USED + 4], mem[USED + 5], mem[USED + 6], mem[USED + 7]]),
0
);
assert_eq!(
u32::from_le_bytes([mem[USED + 8], mem[USED + 9], mem[USED + 10], mem[USED + 11]]),
5
);
assert_eq!(console.rx_available(), 0);
assert!(!console.process_rx_queue(&mut mem, &qc).unwrap());
}
#[test]
fn take_complete_lines_leaves_trailing_partial_buffered() {
let mut port = ConsolePort::new(0);
port.output_buffer
.extend(b"first\nsecond\npartial no newline");
assert_eq!(port.take_complete_lines(), vec!["first", "second"]);
assert!(!port.output_buffer.is_empty());
}
#[test]
fn take_all_lines_flushes_the_dying_partial_line() {
let mut port = ConsolePort::new(0);
port.output_buffer
.extend(b"Kernel panic - not syncing: Attempted to kill init");
let lines = port.take_all_lines();
assert_eq!(
lines,
vec!["Kernel panic - not syncing: Attempted to kill init"]
);
assert!(port.output_buffer.is_empty());
}
}