Skip to content
Binary file added examples/external_kernel
Binary file not shown.
72 changes: 72 additions & 0 deletions include/libkrun.h
Original file line number Diff line number Diff line change
Expand Up @@ -985,6 +985,78 @@ int32_t krun_add_serial_console_default(uint32_t ctx_id,
int input_fd,
int output_fd);

/*
* Adds a multi-port virtio-console device to the guest with explicitly configured ports.
*
* This function creates a new virtio-console device that can have multiple ports added to it
* via krun_add_console_port_tty() and krun_add_console_port_inout(). Unlike krun_add_virtio_console_default(),
* this does not do any automatic detections to configure ports based on the file descriptors.
*
* The function can be called multiple times for adding multiple virtio-console devices.
* Each device appears in the guest with port 0 accessible as /dev/hvcN (hvc0, hvc1, etc.) in the order
* devices are added. If the implicit console is not disabled via `krun_disable_implicit_console`,
* the first explicitly added device will occupy the "hvc1" ID. Additional ports within each device
* (port 1, 2, ...) appear as /dev/vportNpM character devices.
*
* Arguments:
* "ctx_id" - the configuration context ID.
*
* Returns:
* The console_id (>= 0) on success or a negative error number on failure.
*/
int32_t krun_add_virtio_console_multiport(uint32_t ctx_id);

/*
* Adds a TTY port to a multi-port virtio-console device.
*
* The TTY file descriptor is used for both input and output. This port will be marked with the
* VIRTIO_CONSOLE_CONSOLE_PORT flag, enabling console-specific features like window resize signals.
* In the guest, port 0 of each device appears as /dev/hvcN, while subsequent ports appear as
* /dev/vportNpM character devices (regardless of whether they are TTY or generic I/O ports).
*
* This port type supports terminal features including window size detection and resize signals,
* making it suitable for interactive terminal sessions.
*
* Arguments:
* "ctx_id" - the configuration context ID
* "console_id" - the console ID returned by krun_add_virtio_console_multiport()
* "name" - the name of the port for identifying the port in the guest, can be empty ("")
* "tty_fd" - file descriptor for the TTY to use for both input, output, and determining terminal size
*
* Returns:
* Zero on success or a negative error number on failure.
*/
int32_t krun_add_console_port_tty(uint32_t ctx_id,
uint32_t console_id,
const char *name,
int tty_fd);

/*
* Adds a generic I/O port to a multi-port virtio-console device, suitable for arbitrary bidirectional
* data streams that don't require terminal functionality.
*
* This port will NOT be marked with the VIRTIO_CONSOLE_CONSOLE_PORT flag, meaning it won't support
* console-specific features like window resize signals. Like all ports, if this is port 0 of
* the device, it will appear as /dev/hvcN in the guest; otherwise it only appears as /dev/vportNpM
* (also accessible via /sys/class/virtio-ports/).
*
*
* Arguments:
* "ctx_id" - the configuration context ID
* "console_id" - the console ID returned by krun_add_virtio_console_multiport()
* "name" - the name of the port for identifying the port in the guest, can be empty ("")
* "input_fd" - file descriptor to use for input (host writes, guest reads)
* "output_fd" - file descriptor to use for output (guest writes, host reads)
*
* Returns:
* Zero on success or a negative error number on failure.
*/
int32_t krun_add_console_port_inout(uint32_t ctx_id,
uint32_t console_id,
const char *name,
int input_fd,
int output_fd);

/**
* Configure block device to be used as root filesystem.
*
Expand Down
55 changes: 12 additions & 43 deletions src/devices/src/virtio/console/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,6 @@ use std::mem::{size_of, size_of_val};
use std::os::unix::io::{AsRawFd, RawFd};
use std::sync::Arc;

use libc::TIOCGWINSZ;
use nix::ioctl_read_bad;
use utils::eventfd::EventFd;
use vm_memory::{ByteValued, Bytes, GuestMemoryMmap};

Expand All @@ -31,34 +29,6 @@ pub(crate) const AVAIL_FEATURES: u64 = (1 << uapi::VIRTIO_CONSOLE_F_SIZE as u64)
| (1 << uapi::VIRTIO_CONSOLE_F_MULTIPORT as u64)
| (1 << uapi::VIRTIO_F_VERSION_1 as u64);

#[repr(C)]
#[derive(Default)]
struct WS {
rows: u16,
cols: u16,
xpixel: u16,
ypixel: u16,
}
ioctl_read_bad!(tiocgwinsz, TIOCGWINSZ, WS);

pub(crate) fn get_win_size() -> (u16, u16) {
let mut ws: WS = WS::default();

let ret = unsafe { tiocgwinsz(0, &mut ws) };

if let Err(err) = ret {
match err {
// If the port isn't a TTY, this is expected to fail. Avoid logging
// an error in that case.
nix::errno::Errno::ENOTTY => {}
_ => error!("Couldn't get terminal dimensions: {err}"),
}
(0, 0)
} else {
(ws.cols, ws.rows)
}
}

#[derive(Copy, Clone, Debug, Default)]
#[repr(C, packed)]
pub struct VirtioConsoleConfig {
Expand Down Expand Up @@ -102,10 +72,6 @@ pub struct Console {
impl Console {
pub fn new(ports: Vec<PortDescription>) -> super::Result<Console> {
assert!(!ports.is_empty(), "Expected at least 1 port");
assert!(
matches!(ports[0], PortDescription::Console { .. }),
"First port must be a console"
);

let num_queues = num_queues(ports.len());
let queues = vec![VirtQueue::new(QUEUE_SIZE); num_queues];
Expand All @@ -116,12 +82,16 @@ impl Console {
.push(EventFd::new(utils::eventfd::EFD_NONBLOCK).map_err(ConsoleError::EventFd)?);
}

let (cols, rows) = get_win_size();
let config = VirtioConsoleConfig::new(cols, rows, ports.len() as u32);
let ports = zip(0u32.., ports)
let ports: Vec<Port> = zip(0u32.., ports)
.map(|(port_id, description)| Port::new(port_id, description))
.collect();

let (cols, rows) = ports[0]
.terminal()
.map(|t| t.get_win_size())
.unwrap_or((0, 0));
let config = VirtioConsoleConfig::new(cols, rows, ports.len() as u32);

Ok(Console {
control: ConsoleControl::new(),
ports,
Expand All @@ -146,11 +116,10 @@ impl Console {
self.sigwinch_evt.as_raw_fd()
}

pub fn update_console_size(&mut self, cols: u16, rows: u16) {
log::debug!("update_console_size: {cols} {rows}");
// Note that we currently only support resizing on the first/main console
pub fn update_console_size(&mut self, port_id: u32, cols: u16, rows: u16) {
log::debug!("update_console_size {port_id}: {cols} {rows}");
self.control
.console_resize(0, VirtioConsoleResize { rows, cols });
.console_resize(port_id, VirtioConsoleResize { rows, cols });
}

pub(crate) fn process_control_rx(&mut self) -> bool {
Expand Down Expand Up @@ -233,10 +202,10 @@ impl Console {
continue;
}

if self.ports[cmd.id as usize].is_console() {
if let Some(term) = self.ports[cmd.id as usize].terminal() {
self.control.mark_console_port(mem, cmd.id);
self.control.port_open(cmd.id, true);
let (cols, rows) = get_win_size();
let (cols, rows) = term.get_win_size();
self.control
.console_resize(cmd.id, VirtioConsoleResize { cols, rows });
} else {
Expand Down
10 changes: 7 additions & 3 deletions src/devices/src/virtio/console/event_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::os::unix::io::AsRawFd;
use polly::event_manager::{EventManager, Subscriber};
use utils::epoll::{EpollEvent, EventSet};

use super::device::{get_win_size, Console};
use super::device::Console;
use crate::virtio::console::device::{CONTROL_RXQ_INDEX, CONTROL_TXQ_INDEX};
use crate::virtio::console::port_queue_mapping::{queue_idx_to_port_id, QueueDirection};
use crate::virtio::device::VirtioDevice;
Expand Down Expand Up @@ -88,8 +88,12 @@ impl Console {
error!("Failed to read the sigwinch event: {e:?}");
}

let (cols, rows) = get_win_size();
self.update_console_size(cols, rows);
for i in 0..self.ports.len() {
if let Some(term) = self.ports[i].terminal() {
let (cols, rows) = term.get_win_size();
self.update_console_size(i as u32, cols, rows);
}
}
}

fn read_control_queue_event(&mut self, event: &EpollEvent) {
Expand Down
92 changes: 54 additions & 38 deletions src/devices/src/virtio/console/port.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,53 @@ use crate::virtio::console::console_control::ConsoleControl;
use crate::virtio::console::port_io::{PortInput, PortOutput};
use crate::virtio::console::process_rx::process_rx;
use crate::virtio::console::process_tx::process_tx;
use crate::virtio::port_io::PortTerminalProperties;
use crate::virtio::{InterruptTransport, Queue};

pub enum PortDescription {
Console {
pub struct PortDescription {
pub name: Cow<'static, str>,
pub input: Option<Box<dyn PortInput + Send>>,
pub output: Option<Box<dyn PortOutput + Send>>,
pub terminal: Option<Box<dyn PortTerminalProperties>>,
}

impl PortDescription {
pub fn console(
input: Option<Box<dyn PortInput + Send>>,
output: Option<Box<dyn PortOutput + Send>>,
},
InputPipe {
name: Cow<'static, str>,
input: Box<dyn PortInput + Send>,
},
OutputPipe {
name: Cow<'static, str>,
terminal: Box<dyn PortTerminalProperties>,
) -> Self {
Self {
name: "".into(),
input,
output,
terminal: Some(terminal),
}
}

pub fn output_pipe(
name: impl Into<Cow<'static, str>>,
output: Box<dyn PortOutput + Send>,
},
) -> Self {
Self {
name: name.into(),
input: None,
output: Some(output),
terminal: None,
}
}

pub fn input_pipe(
name: impl Into<Cow<'static, str>>,
input: Box<dyn PortInput + Send>,
) -> Self {
Self {
name: name.into(),
input: Some(input),
output: None,
terminal: None,
}
}
}

enum PortState {
Expand All @@ -41,48 +73,32 @@ pub(crate) struct Port {
port_id: u32,
/// Empty if no name given
name: Cow<'static, str>,
represents_console: bool,
state: PortState,
input: Option<Arc<Mutex<Box<dyn PortInput + Send>>>>,
output: Option<Arc<Mutex<Box<dyn PortOutput + Send>>>>,
terminal: Option<Box<dyn PortTerminalProperties>>,
}

impl Port {
pub(crate) fn new(port_id: u32, description: PortDescription) -> Self {
match description {
PortDescription::Console { input, output } => Self {
port_id,
name: "".into(),
represents_console: true,
state: PortState::Inactive,
input: input.map(|input| Arc::new(Mutex::new(input))),
output: output.map(|output| Arc::new(Mutex::new(output))),
},
PortDescription::InputPipe { name, input } => Self {
port_id,
name,
represents_console: false,
state: PortState::Inactive,
input: Some(Arc::new(Mutex::new(input))),
output: None,
},
PortDescription::OutputPipe { name, output } => Self {
port_id,
name,
represents_console: false,
state: PortState::Inactive,
input: None,
output: Some(Arc::new(Mutex::new(output))),
},
Self {
port_id,
name: description.name,
state: PortState::Inactive,
input: description.input.map(|input| Arc::new(Mutex::new(input))),
output: description
.output
.map(|output| Arc::new(Mutex::new(output))),
terminal: description.terminal,
}
}

pub fn name(&self) -> &str {
&self.name
}

pub fn is_console(&self) -> bool {
self.represents_console
pub fn terminal(&self) -> Option<&dyn PortTerminalProperties> {
self.terminal.as_deref()
}

pub fn notify_rx(&self) {
Expand Down
Loading
Loading