diff --git a/examples/external_kernel b/examples/external_kernel new file mode 100755 index 000000000..620436a41 Binary files /dev/null and b/examples/external_kernel differ diff --git a/examples/multiport.c b/examples/multiport.c new file mode 100644 index 000000000..4fce75f82 --- /dev/null +++ b/examples/multiport.c @@ -0,0 +1,226 @@ +/* Multiport console example: multiple TTY ports via tmux, plus one non-TTY FIFO port. + * VM: 4 vCPUs, 4 GiB RAM. Runs /bin/bash in the guest. + */ +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +static int cmd_output(char *output, size_t output_size, const char *prog, ...) +{ + va_list args; + const char *argv[32]; + int argc = 0; + int pipe_fds[2] = { -1, -1 }; + + argv[argc++] = prog; + va_start(args, prog); + while (argc < 31) { + const char *arg = va_arg(args, const char *); + argv[argc++] = arg; + if (arg == NULL) break; + } + va_end(args); + argv[argc] = NULL; + + if (output && output_size > 0) { + if (pipe(pipe_fds) < 0) return -1; + } + + pid_t pid = fork(); + if (pid < 0) return -1; + if (pid == 0) { + if (pipe_fds[0] >= 0) { + close(pipe_fds[0]); + dup2(pipe_fds[1], STDOUT_FILENO); + close(pipe_fds[1]); + } + execvp(prog, (char *const *)argv); + _exit(127); + } + + if (pipe_fds[0] >= 0) { + close(pipe_fds[1]); + ssize_t n = read(pipe_fds[0], output, output_size - 1); + close(pipe_fds[0]); + if (n < 0) n = 0; + output[n] = '\0'; + } + + int status; + if (waitpid(pid, &status, 0) < 0) return -1; + if (!WIFEXITED(status)) return -1; + return WEXITSTATUS(status); +} + +#define cmd(...) ({ char _d[1]; cmd_output(_d, 0, __VA_ARGS__); }) + +static int create_tmux_tty(const char *session_name) +{ + char tty_path[256]; + char wait_cmd[128]; + + snprintf(wait_cmd, sizeof(wait_cmd), "tail --pid=%d -f /dev/null", (int)getpid()); + if (cmd("tmux", "new-session", "-d", "-s", session_name, "sh", "-c", wait_cmd, NULL) != 0) + return -1; + + { + char sig_cmd[64]; + char hook_cmd[128]; + snprintf(sig_cmd, sizeof(sig_cmd), "kill -WINCH %d", (int)getpid()); + snprintf(hook_cmd, sizeof(hook_cmd), "run-shell \"%s\"", sig_cmd); + cmd("tmux", "set-hook", "-g", "client-resized", hook_cmd, NULL); + cmd("tmux", "set-hook", "-g", "window-layout-changed", hook_cmd, NULL); + } + + if (cmd_output(tty_path, sizeof(tty_path), "tmux", "display-message", "-p", "-t", session_name, "#{pane_tty}", NULL) != 0) + return -1; + tty_path[strcspn(tty_path, "\n")] = '\0'; + + int fd = open(tty_path, O_RDWR); + if (fd < 0) return -1; + return fd; +} + +static int mkfifo_if_needed(const char *path) +{ + if (mkfifo(path, 0666) < 0) { + if (errno != EEXIST) return -1; + } + return 0; +} + + +static int create_fifo_inout(const char *fifo_in, const char *fifo_out, int *input_fd, int *output_fd) +{ + if (mkfifo_if_needed(fifo_in) < 0) return -1; + if (mkfifo_if_needed(fifo_out) < 0) return -1; + + int in_fd = open(fifo_in, O_RDONLY | O_NONBLOCK); + if (in_fd < 0) return -1; + + int out_fd = open(fifo_out, O_RDWR | O_NONBLOCK); + if (out_fd < 0) { close(in_fd); return -1; } + + *input_fd = in_fd; + *output_fd = out_fd; + return 0; +} + +int main(int argc, char *const argv[]) +{ + if (argc < 3) { + fprintf(stderr, "Usage: %s ROOT_DIR COMMAND [ARGS...]\n", argv[0]); + return 1; + } + + const char *root_dir = argv[1]; + const char *command = argv[2]; + const char *const *command_args = (argc > 3) ? (const char *const *)&argv[3] : NULL; + const char *const envp[] = { 0 }; + + int err; + int ctx_id = krun_create_ctx(); + if (ctx_id < 0) { errno = -ctx_id; perror("krun_create_ctx"); return 1; } + + if ((err = krun_disable_implicit_console(ctx_id))) { + errno = -err; + perror("krun_disable_implicit_console"); + return 1; + } + + int console_id = krun_add_virtio_console_multiport(ctx_id); + if (console_id < 0) { + errno = -console_id; + perror("krun_add_virtio_console_multiport"); + return 1; + } + + /* Configure console ports - edit this section to add/remove ports */ + { + + // You could also use the controlling terminal of this process in the guest: + /* + int tty_fd = open("/dev/tty", O_RDWR); + if (tty_fd >= 0) { + if ((err = krun_add_console_port_tty(ctx_id, console_id, "host_tty", tty_fd))) { + errno = -err; + perror("port host_tty"); + return 1; + } + } + */ + + int num_consoles = 2; + for (int i = 0; i < num_consoles; i++) { + char session_name[64]; + char port_name[64]; + snprintf(session_name, sizeof(session_name), "krun-console-%d", i + 1); + snprintf(port_name, sizeof(port_name), "console-%d", i + 1); + + int tmux_fd = create_tmux_tty(session_name); + if (tmux_fd < 0) { + perror("create_tmux_tty"); + return 1; + } + if ((err = krun_add_console_port_tty(ctx_id, console_id, port_name, tmux_fd))) { + errno = -err; + perror("krun_add_console_port_tty"); + return 1; + } + } + + int in_fd, out_fd; + if (create_fifo_inout("/tmp/multiport_example_in", "/tmp/multiport_example_out", &in_fd, &out_fd) < 0) { + perror("create_fifo_inout"); + return 1; + } + if ((err = krun_add_console_port_inout(ctx_id, console_id, "fifo_inout", in_fd, out_fd))) { + errno = -err; + perror("krun_add_console_port_inout"); + return 1; + } + + fprintf(stderr, "\n=== Console ports configured ===\n"); + for (int i = 0; i < num_consoles; i++) { + fprintf(stderr, " console-%d: tmux attach -t krun-console-%d\n", i + 1, i + 1); + } + fprintf(stderr, " fifo_inout: /tmp/multiport_example_in (host->guest)\n"); + fprintf(stderr, " fifo_inout: /tmp/multiport_example_out (guest->host)\n"); + fprintf(stderr, "================================\n\n"); + } + + if ((err = krun_set_vm_config(ctx_id, 4, 4096))) { + errno = -err; + perror("krun_set_vm_config"); + return 1; + } + + if ((err = krun_set_root(ctx_id, root_dir))) { + errno = -err; + perror("krun_set_root"); + return 1; + } + + if ((err = krun_set_exec(ctx_id, command, command_args, envp))) { + errno = -err; + perror("krun_set_exec"); + return 1; + } + + if ((err = krun_start_enter(ctx_id))) { + errno = -err; + perror("krun_start_enter"); + return 1; + } + return 0; +} + + diff --git a/include/libkrun.h b/include/libkrun.h index 5a55b3c64..0c0deaddf 100644 --- a/include/libkrun.h +++ b/include/libkrun.h @@ -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. * diff --git a/src/devices/src/virtio/console/device.rs b/src/devices/src/virtio/console/device.rs index 4753d41b2..2e4896e9c 100644 --- a/src/devices/src/virtio/console/device.rs +++ b/src/devices/src/virtio/console/device.rs @@ -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}; @@ -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 { @@ -102,10 +72,6 @@ pub struct Console { impl Console { pub fn new(ports: Vec) -> super::Result { 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]; @@ -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 = 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, @@ -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 { @@ -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 { diff --git a/src/devices/src/virtio/console/event_handler.rs b/src/devices/src/virtio/console/event_handler.rs index ba0d731fd..cb89b99d5 100644 --- a/src/devices/src/virtio/console/event_handler.rs +++ b/src/devices/src/virtio/console/event_handler.rs @@ -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; @@ -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) { diff --git a/src/devices/src/virtio/console/port.rs b/src/devices/src/virtio/console/port.rs index c096f3f7e..caae5bc7f 100644 --- a/src/devices/src/virtio/console/port.rs +++ b/src/devices/src/virtio/console/port.rs @@ -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>, + pub output: Option>, + pub terminal: Option>, +} + +impl PortDescription { + pub fn console( input: Option>, output: Option>, - }, - InputPipe { - name: Cow<'static, str>, - input: Box, - }, - OutputPipe { - name: Cow<'static, str>, + terminal: Box, + ) -> Self { + Self { + name: "".into(), + input, + output, + terminal: Some(terminal), + } + } + + pub fn output_pipe( + name: impl Into>, output: Box, - }, + ) -> Self { + Self { + name: name.into(), + input: None, + output: Some(output), + terminal: None, + } + } + + pub fn input_pipe( + name: impl Into>, + input: Box, + ) -> Self { + Self { + name: name.into(), + input: Some(input), + output: None, + terminal: None, + } + } } enum PortState { @@ -41,39 +73,23 @@ pub(crate) struct Port { port_id: u32, /// Empty if no name given name: Cow<'static, str>, - represents_console: bool, state: PortState, input: Option>>>, output: Option>>>, + terminal: Option>, } 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, } } @@ -81,8 +97,8 @@ impl Port { &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) { diff --git a/src/devices/src/virtio/console/port_io.rs b/src/devices/src/virtio/console/port_io.rs index 59579ef34..be515ab81 100644 --- a/src/devices/src/virtio/console/port_io.rs +++ b/src/devices/src/virtio/console/port_io.rs @@ -1,12 +1,14 @@ -use std::fs::File; -use std::io::{self, ErrorKind}; -use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; - -use libc::{fcntl, F_GETFL, F_SETFL, O_NONBLOCK, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; +use libc::{ + fcntl, F_GETFL, F_SETFL, O_NONBLOCK, STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO, TIOCGWINSZ, +}; use log::Level; use nix::errno::Errno; +use nix::ioctl_read_bad; use nix::poll::{poll, PollFd, PollFlags, PollTimeout}; -use nix::unistd::dup; +use nix::unistd::{dup, isatty}; +use std::fs::File; +use std::io::{self, ErrorKind}; +use std::os::fd::{AsFd, AsRawFd, BorrowedFd, OwnedFd, RawFd}; use utils::eventfd::EventFd; use utils::eventfd::EFD_NONBLOCK; use vm_memory::bitmap::Bitmap; @@ -24,6 +26,11 @@ pub trait PortOutput { fn wait_until_writable(&self); } +/// Terminal properties associated with this port +pub trait PortTerminalProperties: Send + Sync { + fn get_win_size(&self) -> (u16, u16); +} + pub fn stdin() -> Result, nix::Error> { let fd = dup_raw_fd_into_owned(STDIN_FILENO)?; make_non_blocking(&fd)?; @@ -44,6 +51,21 @@ pub fn stderr() -> Result, nix::Error> { output_to_raw_fd_dup(STDERR_FILENO) } +pub fn term_fd( + term_fd: RawFd, +) -> Result, nix::Error> { + let fd = dup_raw_fd_into_owned(term_fd)?; + assert!( + isatty(&fd).is_ok_and(|v| v), + "Expected fd {fd:?}, to be a tty, to query the window size!" + ); + Ok(Box::new(PortTerminalPropertiesFd(fd))) +} + +pub fn term_fixed_size(width: u16, height: u16) -> Box { + Box::new(PortTerminalPropertiesFixed((width, height))) +} + pub fn input_empty() -> Result, nix::Error> { Ok(Box::new(PortInputEmpty {})) } @@ -278,3 +300,35 @@ impl PortInput for PortInputEmpty { } } } + +struct PortTerminalPropertiesFixed((u16, u16)); + +impl PortTerminalProperties for PortTerminalPropertiesFixed { + fn get_win_size(&self) -> (u16, u16) { + self.0 + } +} + +struct PortTerminalPropertiesFd(OwnedFd); + +impl PortTerminalProperties for PortTerminalPropertiesFd { + fn get_win_size(&self) -> (u16, u16) { + let mut ws: WS = WS::default(); + + if let Err(err) = unsafe { tiocgwinsz(self.0.as_raw_fd(), &mut ws) } { + error!("Couldn't get terminal dimensions: {err}"); + return (0, 0); + } + (ws.cols, ws.rows) + } +} + +#[repr(C)] +#[derive(Default)] +struct WS { + rows: u16, + cols: u16, + xpixel: u16, + ypixel: u16, +} +ioctl_read_bad!(tiocgwinsz, TIOCGWINSZ, WS); diff --git a/src/libkrun/src/lib.rs b/src/libkrun/src/lib.rs index c1740f13c..ba5168bab 100644 --- a/src/libkrun/src/lib.rs +++ b/src/libkrun/src/lib.rs @@ -13,10 +13,7 @@ use devices::virtio::CacheType; use env_logger::{Env, Target}; #[cfg(feature = "gpu")] use krun_display::DisplayBackend; -use libc::c_char; -#[cfg(feature = "net")] -use libc::c_int; -use libc::size_t; +use libc::{c_char, c_int, size_t}; use once_cell::sync::Lazy; use polly::event_manager::EventManager; #[cfg(all(feature = "blk", not(feature = "tee")))] @@ -29,16 +26,20 @@ use std::env; use std::ffi::CString; use std::ffi::{c_void, CStr}; use std::fs::File; +use std::io::IsTerminal; #[cfg(target_os = "linux")] use std::os::fd::AsRawFd; -use std::os::fd::{FromRawFd, RawFd}; +use std::os::fd::{BorrowedFd, FromRawFd, RawFd}; use std::path::PathBuf; use std::slice; use std::sync::atomic::{AtomicI32, Ordering}; use std::sync::LazyLock; use std::sync::Mutex; use utils::eventfd::EventFd; -use vmm::resources::{ConsoleConfig, ConsoleType, VmResources}; +use vmm::resources::{ + DefaultVirtioConsoleConfig, PortConfig, SerialConsoleConfig, VirtioConsoleConfigMode, + VmResources, +}; #[cfg(feature = "blk")] use vmm::vmm_config::block::{BlockDeviceConfig, BlockRootConfig}; #[cfg(not(feature = "tee"))] @@ -2117,16 +2118,16 @@ pub unsafe extern "C" fn krun_add_virtio_console_default( match CTX_MAP.lock().unwrap().entry(ctx_id) { Entry::Occupied(mut ctx_cfg) => { let cfg = ctx_cfg.get_mut(); - cfg.vmr.consoles.entry(ConsoleType::Virtio).or_default(); - // safe to unwrap since we inserted an empty Vec if the key didn't exist - let consoles = cfg.vmr.consoles.get_mut(&ConsoleType::Virtio).unwrap(); - consoles.push(ConsoleConfig { - output_path: None, - input_fd, - output_fd, - err_fd, - }); + cfg.vmr + .virtio_consoles + .push(VirtioConsoleConfigMode::Autoconfigure( + DefaultVirtioConsoleConfig { + input_fd, + output_fd, + err_fd, + }, + )); } Entry::Vacant(_) => return -libc::ENOENT, } @@ -2136,23 +2137,117 @@ pub unsafe extern "C" fn krun_add_virtio_console_default( #[allow(clippy::missing_safety_doc)] #[no_mangle] -pub unsafe extern "C" fn krun_add_serial_console_default( +pub unsafe extern "C" fn krun_add_virtio_console_multiport(ctx_id: u32) -> i32 { + match CTX_MAP.lock().unwrap().entry(ctx_id) { + Entry::Occupied(mut ctx_cfg) => { + let cfg = ctx_cfg.get_mut(); + let console_id = cfg.vmr.virtio_consoles.len() as i32; + + cfg.vmr + .virtio_consoles + .push(VirtioConsoleConfigMode::Explicit(Vec::new())); + + console_id + } + Entry::Vacant(_) => -libc::ENOENT, + } +} + +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn krun_add_console_port_tty( ctx_id: u32, - input_fd: libc::c_int, - output_fd: libc::c_int, + console_id: u32, + name: *const libc::c_char, + tty_fd: libc::c_int, +) -> i32 { + if tty_fd < 0 { + return -libc::EINVAL; + } + + let name_str = if name.is_null() { + String::new() + } else { + match CStr::from_ptr(name).to_str() { + Ok(s) => s.to_string(), + Err(_) => return -libc::EINVAL, + } + }; + + if !BorrowedFd::borrow_raw(tty_fd).is_terminal() { + return -libc::ENOTTY; + } + + match CTX_MAP.lock().unwrap().entry(ctx_id) { + Entry::Occupied(mut ctx_cfg) => { + let cfg = ctx_cfg.get_mut(); + + match cfg.vmr.virtio_consoles.get_mut(console_id as usize) { + Some(VirtioConsoleConfigMode::Explicit(ports)) => { + ports.push(PortConfig::Tty { + name: name_str, + tty_fd, + }); + KRUN_SUCCESS + } + _ => -libc::EINVAL, + } + } + Entry::Vacant(_) => -libc::ENOENT, + } +} + +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn krun_add_console_port_inout( + ctx_id: u32, + console_id: u32, + name: *const c_char, + input_fd: c_int, + output_fd: c_int, ) -> i32 { + let name_str = if name.is_null() { + String::new() + } else { + match CStr::from_ptr(name).to_str() { + Ok(s) => s.to_string(), + Err(_) => return -libc::EINVAL, + } + }; + match CTX_MAP.lock().unwrap().entry(ctx_id) { Entry::Occupied(mut ctx_cfg) => { let cfg = ctx_cfg.get_mut(); - cfg.vmr.consoles.entry(ConsoleType::Serial).or_default(); - // safe to unwrap since we inserted an empty Vec if the key didn't exist - let consoles = cfg.vmr.consoles.get_mut(&ConsoleType::Serial).unwrap(); - consoles.push(ConsoleConfig { - output_path: None, + match cfg.vmr.virtio_consoles.get_mut(console_id as usize) { + Some(VirtioConsoleConfigMode::Explicit(ports)) => { + ports.push(PortConfig::InOut { + name: name_str, + input_fd, + output_fd, + }); + KRUN_SUCCESS + } + _ => -libc::EINVAL, + } + } + Entry::Vacant(_) => -libc::ENOENT, + } +} + +#[allow(clippy::missing_safety_doc)] +#[no_mangle] +pub unsafe extern "C" fn krun_add_serial_console_default( + ctx_id: u32, + input_fd: c_int, + output_fd: c_int, +) -> i32 { + match CTX_MAP.lock().unwrap().entry(ctx_id) { + Entry::Occupied(mut ctx_cfg) => { + let cfg = ctx_cfg.get_mut(); + cfg.vmr.serial_consoles.push(SerialConsoleConfig { input_fd, output_fd, - err_fd: -1, }); } Entry::Vacant(_) => return -libc::ENOENT, diff --git a/src/vmm/src/builder.rs b/src/vmm/src/builder.rs index b6b92e6eb..9698a8053 100644 --- a/src/vmm/src/builder.rs +++ b/src/vmm/src/builder.rs @@ -12,7 +12,6 @@ use std::collections::HashMap; use std::fmt::{Display, Formatter}; use std::fs::File; use std::io::{self, Read}; -#[cfg(target_os = "linux")] use std::os::fd::AsRawFd; use std::os::fd::{BorrowedFd, FromRawFd}; use std::path::PathBuf; @@ -24,7 +23,9 @@ use super::{Error, Vmm}; #[cfg(target_arch = "x86_64")] use crate::device_manager::legacy::PortIODeviceManager; use crate::device_manager::mmio::MMIODeviceManager; -use crate::resources::{ConsoleType, VmResources}; +use crate::resources::{ + DefaultVirtioConsoleConfig, PortConfig, VirtioConsoleConfigMode, VmResources, +}; use crate::vmm_config::external_kernel::{ExternalKernel, KernelFormat}; #[cfg(feature = "net")] use crate::vmm_config::net::NetBuilder; @@ -52,7 +53,7 @@ use crate::device_manager; use crate::signal_handler::register_sigint_handler; #[cfg(target_os = "linux")] use crate::signal_handler::register_sigwinch_handler; -use crate::terminal::term_set_raw_mode; +use crate::terminal::{term_restore_mode, term_set_raw_mode}; #[cfg(feature = "blk")] use crate::vmm_config::block::BlockBuilder; #[cfg(not(any(feature = "tee", feature = "nitro")))] @@ -722,11 +723,7 @@ pub fn build_microvm( )?); }; - for s in vm_resources - .consoles - .get(&ConsoleType::Serial) - .unwrap_or(&Vec::new()) - { + for s in &vm_resources.serial_consoles { let input: Option> = if s.input_fd >= 0 { Some(Box::new(unsafe { File::from_raw_fd(s.input_fd) })) } else { @@ -941,18 +938,13 @@ pub fn build_microvm( console_id += 1; } - for console in vm_resources - .consoles - .get(&ConsoleType::Virtio) - .unwrap_or(&Vec::new()) - .iter() - { + for console_cfg in vm_resources.virtio_consoles.iter() { attach_console_devices( &mut vmm, event_manager, intc.clone(), vm_resources, - Some(console), + Some(console_cfg), console_id, )?; console_id += 1; @@ -1860,114 +1852,212 @@ fn attach_fs_devices( Ok(()) } -fn attach_console_devices( +fn autoconfigure_console_ports( vmm: &mut Vmm, - event_manager: &mut EventManager, - intc: IrqChip, vm_resources: &VmResources, - cfg: Option<&super::resources::ConsoleConfig>, - id_number: u32, -) -> std::result::Result<(), StartMicrovmError> { + cfg: Option<&DefaultVirtioConsoleConfig>, + creating_implicit_console: bool, +) -> std::result::Result, StartMicrovmError> { use self::StartMicrovmError::*; let mut console_output_path: Option = None; - - // we don't care about the console output that was set for the vm_resources - // if the implicit console is disabled if let Some(path) = vm_resources.console_output.clone() { - // only set the console output for the implicit console - if !vm_resources.disable_implicit_console && cfg.is_none() { + if !vm_resources.disable_implicit_console && creating_implicit_console { console_output_path = Some(path) } } - let ports = if console_output_path.is_some() { + if console_output_path.is_some() { let file = File::create(console_output_path.unwrap()).map_err(OpenConsoleFile)?; - vec![PortDescription::Console { - input: Some(port_io::input_empty().unwrap()), - output: Some(port_io::output_file(file).unwrap()), - }] + // Manually emulate our Legacy behavior: In the case of output_path we have always used the + // stdin to determine the console size + let stdin_fd = unsafe { BorrowedFd::borrow_raw(STDIN_FILENO) }; + let term_fd = if isatty(stdin_fd).is_ok_and(|v| v) { + port_io::term_fd(stdin_fd.as_raw_fd()).unwrap() + } else { + port_io::term_fixed_size(0, 0) + }; + Ok(vec![PortDescription::console( + Some(port_io::input_empty().unwrap()), + Some(port_io::output_file(file).unwrap()), + term_fd, + )]) } else { let (input_fd, output_fd, err_fd) = match cfg { Some(c) => (c.input_fd, c.output_fd, c.err_fd), None => (STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO), }; - let stdin_is_terminal = - isatty(unsafe { BorrowedFd::borrow_raw(input_fd) }).unwrap_or(false); - let stdout_is_terminal = - isatty(unsafe { BorrowedFd::borrow_raw(output_fd) }).unwrap_or(false); - let stderr_is_terminal = isatty(unsafe { BorrowedFd::borrow_raw(err_fd) }).unwrap_or(false); - - if let Err(e) = term_set_raw_mode(!stdin_is_terminal) { - log::error!("Failed to set terminal to raw mode: {e}") - } + let input_is_terminal = + input_fd >= 0 && isatty(unsafe { BorrowedFd::borrow_raw(input_fd) }).unwrap_or(false); + let output_is_terminal = + output_fd >= 0 && isatty(unsafe { BorrowedFd::borrow_raw(output_fd) }).unwrap_or(false); + let error_is_terminal = + err_fd >= 0 && isatty(unsafe { BorrowedFd::borrow_raw(err_fd) }).unwrap_or(false); + + let term_fd = if input_is_terminal { + Some(unsafe { BorrowedFd::borrow_raw(input_fd) }) + } else if output_is_terminal { + Some(unsafe { BorrowedFd::borrow_raw(output_fd) }) + } else if error_is_terminal { + Some(unsafe { BorrowedFd::borrow_raw(err_fd) }) + } else { + None + }; - let console_input = if stdin_is_terminal { - Some(port_io::stdin().unwrap()) - } else if input_fd < 0 { - Some(port_io::input_empty().unwrap()) - } else if input_fd != STDIN_FILENO { + let forwarding_sigint; + let console_input = if input_is_terminal && input_fd >= 0 { + forwarding_sigint = false; Some(port_io::input_to_raw_fd_dup(input_fd).unwrap()) } else { #[cfg(target_os = "linux")] { + forwarding_sigint = true; let sigint_input = port_io::PortInputSigInt::new(); let sigint_input_fd = sigint_input.sigint_evt().as_raw_fd(); register_sigint_handler(sigint_input_fd).map_err(RegisterFsSigwinch)?; Some(Box::new(sigint_input) as _) } #[cfg(not(target_os = "linux"))] - Some(port_io::input_empty().unwrap()) + { + forwarding_sigint = false; + Some(port_io::input_empty().unwrap()) + } }; - let console_output = if stdout_is_terminal { - Some(port_io::stdout().unwrap()) - } else if output_fd != STDOUT_FILENO && output_fd > 0 { + let console_output = if output_is_terminal && output_fd >= 0 { Some(port_io::output_to_raw_fd_dup(output_fd).unwrap()) } else { Some(port_io::output_to_log_as_err()) }; - let mut ports = vec![PortDescription::Console { - input: console_input, - output: console_output, - }]; + let terminal_properties = term_fd + .map(|fd| port_io::term_fd(fd.as_raw_fd()).unwrap()) + .unwrap_or_else(|| port_io::term_fixed_size(0, 0)); - let console_err = if stderr_is_terminal { - Some(port_io::stderr().unwrap()) - } else if err_fd != STDERR_FILENO && err_fd > 0 { - Some(port_io::output_to_raw_fd_dup(err_fd).unwrap()) - } else { - None - }; + setup_terminal_raw_mode(vmm, term_fd, forwarding_sigint); - ports.push(PortDescription::Console { - input: None, - output: console_err, - }); + let mut ports = vec![PortDescription::console( + console_input, + console_output, + terminal_properties, + )]; - if !stdin_is_terminal && input_fd == STDIN_FILENO { - ports.push(PortDescription::InputPipe { - name: "krun-stdin".into(), - input: port_io::stdin().unwrap(), - }) + if input_fd >= 0 && !input_is_terminal { + ports.push(PortDescription::input_pipe( + "krun-stdin", + port_io::input_to_raw_fd_dup(input_fd).unwrap(), + )); } - if !stdout_is_terminal && output_fd == STDOUT_FILENO { - ports.push(PortDescription::OutputPipe { - name: "krun-stdout".into(), - output: port_io::stdout().unwrap(), - }) + if output_fd >= 0 && !output_is_terminal { + ports.push(PortDescription::output_pipe( + "krun-stdout", + port_io::output_to_raw_fd_dup(output_fd).unwrap(), + )); }; - if !stderr_is_terminal && err_fd == STDERR_FILENO { - ports.push(PortDescription::OutputPipe { - name: "krun-stderr".into(), - output: port_io::stderr().unwrap(), - }); + if err_fd >= 0 && !error_is_terminal { + ports.push(PortDescription::output_pipe( + "krun-stderr", + port_io::output_to_raw_fd_dup(err_fd).unwrap(), + )); } - ports + Ok(ports) + } +} + +fn setup_terminal_raw_mode( + vmm: &mut Vmm, + term_fd: Option>, + handle_signals_by_terminal: bool, +) { + if let Some(term_fd) = term_fd { + match term_set_raw_mode(term_fd, handle_signals_by_terminal) { + Ok(old_mode) => { + let raw_fd = term_fd.as_raw_fd(); + vmm.exit_observers.push(Arc::new(Mutex::new(move || { + if let Err(e) = + term_restore_mode(unsafe { BorrowedFd::borrow_raw(raw_fd) }, &old_mode) + { + log::error!("Failed to restore terminal mode: {e}") + } + }))); + } + Err(e) => { + log::error!("Failed to set terminal to raw mode: {e}") + } + }; + } +} + +fn create_explicit_ports( + vmm: &mut Vmm, + port_configs: &[PortConfig], +) -> std::result::Result, StartMicrovmError> { + let mut ports = Vec::with_capacity(port_configs.len()); + + for port_cfg in port_configs { + let port_desc = match port_cfg { + PortConfig::Tty { name, tty_fd } => { + assert!(*tty_fd > 0, "PortConfig::Tty must have a valid tty_fd"); + let term_fd = unsafe { BorrowedFd::borrow_raw(*tty_fd) }; + setup_terminal_raw_mode(vmm, Some(term_fd), false); + + PortDescription { + name: name.clone().into(), + input: Some(port_io::input_to_raw_fd_dup(*tty_fd).unwrap()), + output: Some(port_io::output_to_raw_fd_dup(*tty_fd).unwrap()), + terminal: Some(port_io::term_fd(*tty_fd).unwrap()), + } + } + PortConfig::InOut { + name, + input_fd, + output_fd, + } => PortDescription { + name: name.clone().into(), + input: if *input_fd < 0 { + None + } else { + Some(port_io::input_to_raw_fd_dup(*input_fd).unwrap()) + }, + output: if *output_fd < 0 { + None + } else { + Some(port_io::output_to_raw_fd_dup(*output_fd).unwrap()) + }, + terminal: None, + }, + }; + + ports.push(port_desc); + } + + Ok(ports) +} + +fn attach_console_devices( + vmm: &mut Vmm, + event_manager: &mut EventManager, + intc: IrqChip, + vm_resources: &VmResources, + cfg: Option<&VirtioConsoleConfigMode>, + id_number: u32, +) -> std::result::Result<(), StartMicrovmError> { + use self::StartMicrovmError::*; + + let creating_implicit_console = cfg.is_none(); + + let ports = match cfg { + None => autoconfigure_console_ports(vmm, vm_resources, None, creating_implicit_console)?, + Some(VirtioConsoleConfigMode::Autoconfigure(autocfg)) => autoconfigure_console_ports( + vmm, + vm_resources, + Some(autocfg), + creating_implicit_console, + )?, + Some(VirtioConsoleConfigMode::Explicit(ports)) => create_explicit_ports(vmm, ports)?, }; let console = Arc::new(Mutex::new(devices::virtio::Console::new(ports).unwrap())); diff --git a/src/vmm/src/lib.rs b/src/vmm/src/lib.rs index 2a2d1424b..94cf481c9 100644 --- a/src/vmm/src/lib.rs +++ b/src/vmm/src/lib.rs @@ -47,7 +47,6 @@ use std::time::Duration; #[cfg(target_arch = "x86_64")] use crate::device_manager::legacy::PortIODeviceManager; use crate::device_manager::mmio::MMIODeviceManager; -use crate::terminal::term_set_canonical_mode; #[cfg(target_os = "linux")] use crate::vstate::VcpuEvent; use crate::vstate::{Vcpu, VcpuHandle, VcpuResponse, Vm}; @@ -360,10 +359,6 @@ impl Vmm { pub fn stop(&mut self, exit_code: i32) { info!("Vmm is stopping."); - if let Err(e) = term_set_canonical_mode() { - log::error!("Failed to restore terminal to canonical mode: {e}") - } - for observer in &self.exit_observers { observer .lock() diff --git a/src/vmm/src/resources.rs b/src/vmm/src/resources.rs index 7f6652155..5259eb8d7 100644 --- a/src/vmm/src/resources.rs +++ b/src/vmm/src/resources.rs @@ -3,7 +3,6 @@ //#![deny(warnings)] -use std::collections::HashMap; #[cfg(feature = "tee")] use std::fs::File; #[cfg(feature = "tee")] @@ -29,7 +28,6 @@ use crate::vmm_config::machine_config::{VmConfig, VmConfigError}; use crate::vmm_config::net::{NetBuilder, NetworkInterfaceConfig, NetworkInterfaceError}; use crate::vmm_config::vsock::*; use crate::vstate::VcpuConfig; - #[cfg(feature = "gpu")] use devices::virtio::display::DisplayInfo; #[cfg(feature = "tee")] @@ -83,20 +81,34 @@ impl Default for TeeConfig { } } -#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)] -pub enum ConsoleType { - Serial, - Virtio, +pub struct SerialConsoleConfig { + pub input_fd: RawFd, + pub output_fd: RawFd, } -#[derive(Debug, Default)] -pub struct ConsoleConfig { - pub output_path: Option, +pub struct DefaultVirtioConsoleConfig { pub input_fd: RawFd, pub output_fd: RawFd, pub err_fd: RawFd, } +pub enum VirtioConsoleConfigMode { + Autoconfigure(DefaultVirtioConsoleConfig), + Explicit(Vec), +} + +pub enum PortConfig { + Tty { + name: String, + tty_fd: RawFd, + }, + InOut { + name: String, + input_fd: RawFd, + output_fd: RawFd, + }, +} + /// A data structure that encapsulates the device configurations /// held in the Vmm. #[derive(Default)] @@ -153,8 +165,10 @@ pub struct VmResources { pub disable_implicit_console: bool, /// The console id to use for console= in the kernel cmdline pub kernel_console: Option, - /// Consoles to attach to the guest - pub consoles: HashMap>, + /// Serial consoles to attach to the guest + pub serial_consoles: Vec, + /// Virtio consoles to attach to the guest + pub virtio_consoles: Vec, } impl VmResources { @@ -353,7 +367,7 @@ impl VmResources { mod tests { #[cfg(feature = "gpu")] use crate::resources::DisplayBackendConfig; - use crate::resources::VmResources; + use crate::resources::{DefaultVirtioConsoleConfig, VmResources}; use crate::vmm_config::kernel_cmdline::KernelCmdlineConfig; use crate::vmm_config::machine_config::{CpuFeaturesTemplate, VmConfig, VmConfigError}; use crate::vmm_config::vsock::tests::{default_config, TempSockFile}; @@ -393,7 +407,8 @@ mod tests { nested_enabled: false, split_irqchip: false, disable_implicit_console: false, - consoles: HashMap::new(), + serial_consoles: Vec::new(), + virtio_consoles: Vec::new(), kernel_console: None, } } diff --git a/src/vmm/src/terminal.rs b/src/vmm/src/terminal.rs index 7067df19b..8fd43cd21 100644 --- a/src/vmm/src/terminal.rs +++ b/src/vmm/src/terminal.rs @@ -1,29 +1,16 @@ -use libc::{STDERR_FILENO, STDIN_FILENO, STDOUT_FILENO}; -use nix::sys::termios::{tcgetattr, tcsetattr, LocalFlags, SetArg}; -use nix::unistd::isatty; +use nix::sys::termios::{tcgetattr, tcsetattr, LocalFlags, SetArg, Termios}; use std::os::fd::BorrowedFd; -pub fn term_set_raw_mode(handle_signals_by_terminal: bool) -> Result<(), nix::Error> { - if let Some(fd) = get_connected_term_fd() { - term_fd_set_raw_mode(fd, handle_signals_by_terminal) - } else { - Ok(()) - } -} - -pub fn term_set_canonical_mode() -> Result<(), nix::Error> { - if let Some(fd) = get_connected_term_fd() { - term_fd_set_canonical_mode(fd) - } else { - Ok(()) - } -} +#[must_use] +pub struct TerminalMode(Termios); -pub fn term_fd_set_raw_mode( +// Enable raw mode for the terminal and return the old state to be restored +pub fn term_set_raw_mode( term: BorrowedFd, handle_signals_by_terminal: bool, -) -> Result<(), nix::Error> { +) -> Result { let mut termios = tcgetattr(term)?; + let old_state = termios.clone(); let mut mask = LocalFlags::ECHO | LocalFlags::ICANON; if !handle_signals_by_terminal { @@ -32,32 +19,9 @@ pub fn term_fd_set_raw_mode( termios.local_flags &= !mask; tcsetattr(term, SetArg::TCSANOW, &termios)?; - Ok(()) + Ok(TerminalMode(old_state)) } -pub fn term_fd_set_canonical_mode(term: BorrowedFd) -> Result<(), nix::Error> { - let mut termios = tcgetattr(term)?; - termios.local_flags |= LocalFlags::ECHO | LocalFlags::ICANON | LocalFlags::ISIG; - tcsetattr(term, SetArg::TCSANOW, &termios)?; - Ok(()) -} - -pub fn get_connected_term_fd() -> Option> { - let (stdin, stdout, stderr) = unsafe { - ( - BorrowedFd::borrow_raw(STDIN_FILENO), - BorrowedFd::borrow_raw(STDOUT_FILENO), - BorrowedFd::borrow_raw(STDERR_FILENO), - ) - }; - - if isatty(stdin).unwrap_or(false) { - Some(stdin) - } else if isatty(stdout).unwrap_or(false) { - Some(stdout) - } else if isatty(stderr).unwrap_or(false) { - Some(stderr) - } else { - None - } +pub fn term_restore_mode(term: BorrowedFd, restore: &TerminalMode) -> Result<(), nix::Error> { + tcsetattr(term, SetArg::TCSANOW, &restore.0) } diff --git a/tests/test_cases/src/lib.rs b/tests/test_cases/src/lib.rs index 68aa6433e..551a89b2d 100644 --- a/tests/test_cases/src/lib.rs +++ b/tests/test_cases/src/lib.rs @@ -10,6 +10,9 @@ use test_tsi_tcp_guest_connect::TestTsiTcpGuestConnect; mod test_tsi_tcp_guest_listen; use test_tsi_tcp_guest_listen::TestTsiTcpGuestListen; +mod test_multiport_console; +use test_multiport_console::TestMultiportConsole; + pub fn test_cases() -> Vec { // Register your test here: vec![ @@ -36,6 +39,7 @@ pub fn test_cases() -> Vec { "tsi-tcp-guest-listen", Box::new(TestTsiTcpGuestListen::new()), ), + TestCase::new("multiport-console", Box::new(TestMultiportConsole)), ] } diff --git a/tests/test_cases/src/test_multiport_console.rs b/tests/test_cases/src/test_multiport_console.rs new file mode 100644 index 000000000..0249586f8 --- /dev/null +++ b/tests/test_cases/src/test_multiport_console.rs @@ -0,0 +1,161 @@ +use macros::{guest, host}; + +pub struct TestMultiportConsole; + +#[host] +mod host { + use super::*; + + use crate::common::setup_fs_and_enter; + use crate::{krun_call, krun_call_u32}; + use crate::{Test, TestSetup}; + use krun_sys::*; + use std::ffi::CString; + use std::io::{BufRead, BufReader, Write}; + use std::os::fd::AsRawFd; + use std::os::unix::net::UnixStream; + use std::{mem, thread}; + + fn spawn_ping_pong_responder(stream: UnixStream) { + thread::spawn(move || { + let mut reader = BufReader::new(stream.try_clone().unwrap()); + let mut writer = stream; + let mut line = String::new(); + while reader.read_line(&mut line).is_ok() && !line.is_empty() { + let response = line.replace("PING", "PONG"); + writer.write_all(response.as_bytes()).unwrap(); + writer.flush().unwrap(); + line.clear(); + } + }); + } + + fn test_port( + ctx: u32, + console_id: u32, + name: &str, + ) -> anyhow::Result<()> { + let (guest, host) = UnixStream::pair()?; + let name_cstring = CString::new(name)?; + unsafe { + krun_call!(krun_add_console_port_inout( + ctx, + console_id, + name_cstring.as_ptr(), + guest.as_raw_fd(), + guest.as_raw_fd() + ))?; + } + mem::forget(guest); + spawn_ping_pong_responder(host); + Ok(()) + } + + impl Test for TestMultiportConsole { + fn start_vm(self: Box, test_setup: TestSetup) -> anyhow::Result<()> { + unsafe { + krun_call!(krun_set_log_level(KRUN_LOG_LEVEL_WARN))?; + let ctx = krun_call_u32!(krun_create_ctx())?; + + krun_call!(krun_disable_implicit_console(ctx))?; + + // Add a default console (as with other tests this uses stdout for writing "OK") + krun_call!(krun_add_virtio_console_default( + ctx, + -1, + std::io::stdout().as_raw_fd(), + -1, + ))?; + + let console_id = krun_call_u32!(krun_add_virtio_console_multiport(ctx))?; + + test_port(ctx, console_id, "test-port-alpha")?; + test_port(ctx, console_id, "test-port-beta")?; + test_port(ctx, console_id, "test-port-gamma")?; + + + krun_call!(krun_set_vm_config(ctx, 1, 1024))?; + setup_fs_and_enter(ctx, test_setup)?; + } + Ok(()) + } + } +} + +#[guest] +mod guest { + use super::*; + use crate::Test; + use std::fs; + use std::io::{BufRead, BufReader, Write}; + + fn test_port(port_map: &std::collections::HashMap, name: &str, message: &str) { + let device_path = format!("/dev/{}", port_map.get(name).unwrap()); + let mut port = fs::OpenOptions::new() + .read(true) + .write(true) + .open(&device_path) + .unwrap(); + + port.write_all(message.as_bytes()).unwrap(); + port.flush().unwrap(); + + let mut reader = BufReader::new(port); + let mut response = String::new(); + reader.read_line(&mut response).unwrap(); + + let expected = message.replace("PING", "PONG").to_string(); + assert_eq!(response, expected, "{}: wrong response", name); + } + + impl Test for TestMultiportConsole { + fn in_guest(self: Box) { + let ports_dir = "/sys/class/virtio-ports"; + + let mut port_map = std::collections::HashMap::new(); + + for entry in fs::read_dir(ports_dir).unwrap() { + let entry = entry.unwrap(); + let port_name_path = entry.path().join("name"); + + if port_name_path.exists() { + let port_name = fs::read_to_string(&port_name_path) + .unwrap() + .trim() + .to_string(); + + if !port_name.is_empty() { + let device_name = entry.file_name().to_string_lossy().to_string(); + port_map.insert(port_name, device_name); + } + } + } + + assert!( + port_map.contains_key("krun-stdout"), + "krun-stdout not found" + ); + assert!( + port_map.contains_key("test-port-alpha"), + "test-port-alpha not found" + ); + assert!( + port_map.contains_key("test-port-beta"), + "test-port-beta not found" + ); + assert!( + port_map.contains_key("test-port-gamma"), + "test-port-gamma not found" + ); + + // We shouldn't have any more than configured here + assert_eq!(port_map.len(), 4); + + test_port(&port_map, "test-port-alpha", "PING-ALPHA\n"); + test_port(&port_map, "test-port-beta", "PING-BETA\n"); + test_port(&port_map, "test-port-gamma", "PING-GAMMA\n"); + + println!("OK"); + } + } +}