From e50a641126350477b90bfc4b2de80e0b2ab15570 Mon Sep 17 00:00:00 2001 From: Philipp Schuster Date: Wed, 17 Jan 2024 22:12:43 +0100 Subject: [PATCH] devices: add debug-console device This commit adds the debug-console (or debugcon) device to CHV. It is a very simple device on I/O port 0xe9 supported by QEMU and BOCHS. It is meant for printing information as easy as possible, without any necessary configuration from the guest at all. It is primarily interesting to OS/kernel and firmware developers as they can produce output as soon as the guest starts without any configuration of a serial device or similar. Furthermore, a kernel hacker might use this device for information of type B whereas information of type A are printed to the serial device. This device is not used by default by Linux, Windows, or any other "real" OS, but only by toy kernels and during firmware development. In the CLI, it can be configured similar to --console or --serial with the --debug-console parameter. Signed-off-by: Philipp Schuster --- devices/src/debug_console.rs | 64 +++++++++++ devices/src/lib.rs | 2 + docs/debug-port.md | 43 +++++++- fuzz/fuzz_targets/http_api.rs | 2 + src/main.rs | 37 +++++++ vmm/src/api/openapi/cloud-hypervisor.yaml | 15 +++ vmm/src/config.rs | 104 +++++++++++++++++- vmm/src/device_manager.rs | 125 +++++++++++++++++++++- vmm/src/lib.rs | 19 +++- vmm/src/vm.rs | 8 ++ vmm/src/vm_config.rs | 24 +++++ 11 files changed, 429 insertions(+), 14 deletions(-) create mode 100644 devices/src/debug_console.rs diff --git a/devices/src/debug_console.rs b/devices/src/debug_console.rs new file mode 100644 index 000000000..799c8a67e --- /dev/null +++ b/devices/src/debug_console.rs @@ -0,0 +1,64 @@ +// Copyright © 2023 Cyberus Technology +// +// SPDX-License-Identifier: Apache-2.0 +// + +//! Module for [`DebugconState`]. + +use std::io; +use std::io::Write; +use std::sync::{Arc, Barrier}; +use vm_device::BusDevice; +use vm_migration::{Migratable, MigratableError, Pausable, Snapshot, Snapshottable, Transportable}; + +/// I/O-port. +pub const DEFAULT_PORT: u64 = 0xe9; + +#[derive(Default)] +pub struct DebugconState {} + +/// Emulates a debug console similar to the QEMU debugcon device. This device +/// is stateless and only prints the bytes (usually text) that are written to +/// it. +/// +/// This device is only available on x86. +/// +/// Reference: +/// - https://github.com/qemu/qemu/blob/master/hw/char/debugcon.c +/// - https://phip1611.de/blog/how-to-use-qemus-debugcon-feature-and-write-to-a-file/ +pub struct DebugConsole { + id: String, + out: Box, +} + +impl DebugConsole { + pub fn new(id: String, out: Box) -> Self { + Self { id, out } + } +} + +impl BusDevice for DebugConsole { + fn read(&mut self, _base: u64, _offset: u64, _data: &mut [u8]) {} + + fn write(&mut self, _base: u64, _offset: u64, data: &[u8]) -> Option> { + if let Err(e) = self.out.write_all(data) { + // unlikely + error!("debug-console: failed writing data: {e:?}"); + } + None + } +} + +impl Snapshottable for DebugConsole { + fn id(&self) -> String { + self.id.clone() + } + + fn snapshot(&mut self) -> Result { + Snapshot::new_from_state(&()) + } +} + +impl Pausable for DebugConsole {} +impl Transportable for DebugConsole {} +impl Migratable for DebugConsole {} diff --git a/devices/src/lib.rs b/devices/src/lib.rs index 79e262020..260b1ecae 100644 --- a/devices/src/lib.rs +++ b/devices/src/lib.rs @@ -15,6 +15,8 @@ extern crate event_monitor; extern crate log; pub mod acpi; +#[cfg(target_arch = "x86_64")] +pub mod debug_console; #[cfg(target_arch = "aarch64")] pub mod gic; pub mod interrupt_controller; diff --git a/docs/debug-port.md b/docs/debug-port.md index 6bafd1c37..5cec79311 100644 --- a/docs/debug-port.md +++ b/docs/debug-port.md @@ -1,7 +1,16 @@ -# `cloud-hypervisor` debug IO port +# `cloud-hypervisor` debug IO ports -`cloud-hypervisor` uses the [`0x80`](https://www.intel.com/content/www/us/en/support/articles/000005500/boards-and-kits.html) -I/O port to trace user defined guest events. +When running x86 guests, `cloud-hypervisor` provides different kinds of debug ports: +- [`0x80` debug port](https://www.intel.com/content/www/us/en/support/articles/000005500/boards-and-kits.html) +- Debug console (by default at `0xe9`). +- Firmware debug port at `0x402`. + +All of them can be used to trace user-defined guest events and all of them can +be used simultaneously. + +## Debug Ports Overview + +### `0x80` I/O port Whenever the guest write one byte between `0x0` and `0xF` on this particular I/O port, `cloud-hypervisor` will log and timestamp that event at the `debug` @@ -30,7 +39,7 @@ guest will have `cloud-hypervisor` generate timestamped logs of all those steps. That provides a basic but convenient way of measuring not only the overall guest boot time but all intermediate steps as well. -## Logging +#### Logging Assuming parts of the guest software stack have been instrumented to use the `cloud-hypervisor` debug I/O port, we may want to gather the related logs. @@ -59,3 +68,29 @@ $ grep "Debug I/O port" /tmp/ch-fw.log cloud-hypervisor: 19.762449ms: DEBUG:vmm/src/vm.rs:510 -- [Debug I/O port: Firmware code 0x0] 0.019004 seconds cloud-hypervisor: 403.499628ms: DEBUG:vmm/src/vm.rs:510 -- [Debug I/O port: Firmware code 0x1] 0.402744 seconds ``` + +### Debug console port + +The debug console is inspired by QEMU and Bochs, which have a similar feature. +By default, the I/O port `0xe9` is used. This port can be configured like a +console. Thus, it can print to a tty, a file, or a pty, for example. + +### Firmware debug port + +The firmware debug port is also a simple port that prints all bytes written to +it. The firmware debug port only prints to stdout. + +## When do I need these ports? + +The ports are on the one hand interesting for firmware or kernel developers, as +they provide an easy way to print debug information from within a guest. +Furthermore, you can patch "normal" software to measure certain events, such as +the boot time of a guest. + +## Which port should I choose? + +The `0x80` debug port and the port of the firmware debug device are always +available. The debug console must be activated via the command line, but +provides more configuration options. + +You can use different ports for different aspect of your logging messages. \ No newline at end of file diff --git a/fuzz/fuzz_targets/http_api.rs b/fuzz/fuzz_targets/http_api.rs index 00b818d4a..1f3c92c47 100644 --- a/fuzz/fuzz_targets/http_api.rs +++ b/fuzz/fuzz_targets/http_api.rs @@ -173,6 +173,8 @@ impl RequestHandler for StubApiRequestHandler { iommu: false, socket: None, }, + #[cfg(target_arch = "x86_64")] + debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, diff --git a/src/main.rs b/src/main.rs index 5320d02f4..36869215f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -418,6 +418,15 @@ fn create_app(default_vcpus: String, default_memory: String, default_rng: String .group("vm-config"), ); + #[cfg(target_arch = "x86_64")] + let app = app.arg( + Arg::new("debug-console") + .long("debug-console") + .help("Debug console: off|pty|tty|file=,iobase=") + .default_value("off,iobase=0xe9") + .group("vm-config"), + ); + #[cfg(feature = "guest_debug")] let app = app.arg( Arg::new("gdb") @@ -792,6 +801,8 @@ mod unit_tests { ConsoleConfig, ConsoleOutputMode, CpuFeatures, CpusConfig, MemoryConfig, PayloadConfig, RngConfig, VmConfig, VmParams, }; + #[cfg(target_arch = "x86_64")] + use vmm::vm_config::DebugConsoleConfig; fn get_vm_config_from_vec(args: &[&str]) -> VmConfig { let (default_vcpus, default_memory, default_rng) = prepare_default_values(); @@ -881,6 +892,8 @@ mod unit_tests { iommu: false, socket: None, }, + #[cfg(target_arch = "x86_64")] + debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, @@ -1512,6 +1525,30 @@ mod unit_tests { }); } + #[cfg(target_arch = "x86_64")] + #[test] + fn test_valid_vm_config_debug_console() { + [( + vec![ + "cloud-hypervisor", + "--kernel", + "/path/to/kernel", + "--debug-console", + "tty,iobase=0xe9", + ], + // 233 == 0xe9 + r#"{ + "payload": {"kernel": "/path/to/kernel" }, + "debug_console": {"mode": "Tty", "iobase": 233 } + }"#, + true, + )] + .iter() + .for_each(|(cli, openapi, equal)| { + compare_vm_config_cli_vs_json(cli, openapi, *equal); + }); + } + #[test] fn test_valid_vm_config_serial_console() { [ diff --git a/vmm/src/api/openapi/cloud-hypervisor.yaml b/vmm/src/api/openapi/cloud-hypervisor.yaml index cfc4723fe..59db48a57 100644 --- a/vmm/src/api/openapi/cloud-hypervisor.yaml +++ b/vmm/src/api/openapi/cloud-hypervisor.yaml @@ -582,6 +582,8 @@ components: $ref: "#/components/schemas/ConsoleConfig" console: $ref: "#/components/schemas/ConsoleConfig" + debug_console: + $ref: "#/components/schemas/DebugConsoleConfig" devices: type: array items: @@ -999,6 +1001,19 @@ components: type: boolean default: false + DebugConsoleConfig: + required: + - mode + type: object + properties: + file: + type: string + mode: + type: string + enum: ["Off", "Pty", "Tty", "File", "Null"] + iobase: + type: integer + DeviceConfig: required: - path diff --git a/vmm/src/config.rs b/vmm/src/config.rs index 5d82c7ae3..c3c6b4c33 100644 --- a/vmm/src/config.rs +++ b/vmm/src/config.rs @@ -61,6 +61,9 @@ pub enum Error { ParsePersistentMemory(OptionParserError), /// Failed parsing console ParseConsole(OptionParserError), + #[cfg(target_arch = "x86_64")] + /// Failed parsing debug-console + ParseDebugConsole(OptionParserError), /// No mode given for console ParseConsoleInvalidModeGiven, /// Failed parsing device parameters @@ -116,6 +119,9 @@ pub enum ValidationError { ConsoleSocketPathMissing, /// Max is less than boot CpusMaxLowerThanBoot, + /// Missing file value for debug-console + #[cfg(target_arch = "x86_64")] + DebugconFileMissing, /// Both socket and path specified DiskSocketAndPath, /// Using vhost user requires shared memory @@ -185,6 +191,9 @@ pub enum ValidationError { DefaultPciSegmentInvalidNode(u32), /// Invalid rate-limiter group InvalidRateLimiterGroup, + /// The specified I/O port was invalid. It should be provided in hex, such as `0xe9`. + #[cfg(target_arch = "x86_64")] + InvalidIoPortHex(String), } type ValidationResult = std::result::Result; @@ -197,6 +206,8 @@ impl fmt::Display for ValidationError { ConsoleFileMissing => write!(f, "Path missing when using file console mode"), ConsoleSocketPathMissing => write!(f, "Path missing when using socket console mode"), CpusMaxLowerThanBoot => write!(f, "Max CPUs lower than boot CPUs"), + #[cfg(target_arch = "x86_64")] + DebugconFileMissing => write!(f, "Path missing when using file mode for debug console"), DiskSocketAndPath => write!(f, "Disk path and vhost socket both provided"), VhostUserRequiresSharedMemory => { write!( @@ -311,6 +322,13 @@ impl fmt::Display for ValidationError { InvalidRateLimiterGroup => { write!(f, "Invalid rate-limiter group") } + #[cfg(target_arch = "x86_64")] + InvalidIoPortHex(s) => { + write!( + f, + "The IO port was not properly provided in hex or a `0x` prefix is missing: {s}" + ) + } } } } @@ -320,6 +338,8 @@ impl fmt::Display for Error { use self::Error::*; match self { ParseConsole(o) => write!(f, "Error parsing --console: {o}"), + #[cfg(target_arch = "x86_64")] + ParseDebugConsole(o) => write!(f, "Error parsing --debug-console: {o}"), ParseConsoleInvalidModeGiven => { write!(f, "Error parsing --console: invalid console mode given") } @@ -399,6 +419,8 @@ pub struct VmParams<'a> { pub pmem: Option>, pub serial: &'a str, pub console: &'a str, + #[cfg(target_arch = "x86_64")] + pub debug_console: &'a str, pub devices: Option>, pub user_devices: Option>, pub vdpa: Option>, @@ -440,6 +462,8 @@ impl<'a> VmParams<'a> { .get_many::("net") .map(|x| x.map(|y| y as &str).collect()); let console = args.get_one::("console").unwrap(); + #[cfg(target_arch = "x86_64")] + let debug_console = args.get_one::("debug-console").unwrap().as_str(); let balloon = args.get_one::("balloon").map(|x| x as &str); let fs: Option> = args .get_many::("fs") @@ -489,6 +513,8 @@ impl<'a> VmParams<'a> { pmem, serial, console, + #[cfg(target_arch = "x86_64")] + debug_console, devices, user_devices, vdpa, @@ -1636,6 +1662,59 @@ impl ConsoleConfig { } } +#[cfg(target_arch = "x86_64")] +impl DebugConsoleConfig { + pub fn parse(debug_console_ops: &str) -> Result { + let mut parser = OptionParser::new(); + parser + .add_valueless("off") + .add_valueless("pty") + .add_valueless("tty") + .add_valueless("null") + .add("file") + .add("iobase"); + parser + .parse(debug_console_ops) + .map_err(Error::ParseConsole)?; + + let mut file: Option = default_consoleconfig_file(); + let mut iobase: Option = None; + let mut mode: ConsoleOutputMode = ConsoleOutputMode::Off; + + if parser.is_set("off") { + } else if parser.is_set("pty") { + mode = ConsoleOutputMode::Pty + } else if parser.is_set("tty") { + mode = ConsoleOutputMode::Tty + } else if parser.is_set("null") { + mode = ConsoleOutputMode::Null + } else if parser.is_set("file") { + mode = ConsoleOutputMode::File; + file = + Some(PathBuf::from(parser.get("file").ok_or( + Error::Validation(ValidationError::ConsoleFileMissing), + )?)); + } else { + return Err(Error::ParseConsoleInvalidModeGiven); + } + + if parser.is_set("iobase") { + if let Some(iobase_opt) = parser.get("iobase") { + if !iobase_opt.starts_with("0x") { + return Err(Error::Validation(ValidationError::InvalidIoPortHex( + iobase_opt, + ))); + } + iobase = Some(u16::from_str_radix(&iobase_opt[2..], 16).map_err(|_| { + Error::Validation(ValidationError::InvalidIoPortHex(iobase_opt)) + })?); + } + } + + Ok(Self { file, mode, iobase }) + } +} + impl DeviceConfig { pub const SYNTAX: &'static str = "Direct device assignment parameters \"path=,iommu=on|off,id=,pci_segment=\""; @@ -2054,9 +2133,20 @@ impl VmConfig { // Using such double tty mode, you need to configure the kernel // properly, such as: // "console=hvc0 earlyprintk=ttyS0" - if self.console.mode == ConsoleOutputMode::Tty && self.serial.mode == ConsoleOutputMode::Tty - { - warn!("Using TTY output for both virtio-console and serial port"); + + let mut tty_consoles = Vec::new(); + if self.console.mode == ConsoleOutputMode::Tty { + tty_consoles.push("virtio-console"); + }; + if self.serial.mode == ConsoleOutputMode::Tty { + tty_consoles.push("serial-console"); + }; + #[cfg(target_arch = "x86_64")] + if self.debug_console.mode == ConsoleOutputMode::Tty { + tty_consoles.push("debug-console"); + }; + if tty_consoles.len() > 1 { + warn!("Using TTY output for multiple consoles: {:?}", tty_consoles); } if self.console.mode == ConsoleOutputMode::File && self.console.file.is_none() { @@ -2375,6 +2465,8 @@ impl VmConfig { let console = ConsoleConfig::parse(vm_params.console)?; let serial = ConsoleConfig::parse(vm_params.serial)?; + #[cfg(target_arch = "x86_64")] + let debug_console = DebugConsoleConfig::parse(vm_params.debug_console)?; let mut devices: Option> = None; if let Some(device_list) = &vm_params.devices { @@ -2482,6 +2574,8 @@ impl VmConfig { pmem, serial, console, + #[cfg(target_arch = "x86_64")] + debug_console, devices, user_devices, vdpa, @@ -2606,6 +2700,8 @@ impl Clone for VmConfig { pmem: self.pmem.clone(), serial: self.serial.clone(), console: self.console.clone(), + #[cfg(target_arch = "x86_64")] + debug_console: self.debug_console.clone(), devices: self.devices.clone(), user_devices: self.user_devices.clone(), vdpa: self.vdpa.clone(), @@ -3341,6 +3437,8 @@ mod tests { iommu: false, socket: None, }, + #[cfg(target_arch = "x86_64")] + debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, diff --git a/vmm/src/device_manager.rs b/vmm/src/device_manager.rs index c7bdf86a7..f0a3bbea0 100644 --- a/vmm/src/device_manager.rs +++ b/vmm/src/device_manager.rs @@ -41,14 +41,14 @@ use block::{ }; #[cfg(feature = "io_uring")] use block::{fixed_vhd_async::FixedVhdDiskAsync, raw_async::RawFileDisk}; +#[cfg(target_arch = "x86_64")] +use devices::debug_console::DebugConsole; #[cfg(target_arch = "aarch64")] use devices::gic; #[cfg(target_arch = "x86_64")] use devices::ioapic; #[cfg(target_arch = "aarch64")] use devices::legacy::Pl011; -#[cfg(target_arch = "x86_64")] -use devices::legacy::Serial; use devices::{ interrupt_controller, interrupt_controller::InterruptController, AcpiNotificationFlags, }; @@ -103,6 +103,8 @@ use vm_migration::{ use vm_virtio::AccessPlatform; use vm_virtio::VirtioDeviceType; use vmm_sys_util::eventfd::EventFd; +#[cfg(target_arch = "x86_64")] +use {devices::debug_console, devices::legacy::Serial}; #[cfg(target_arch = "aarch64")] const MMIO_LEN: u64 = 0x1000; @@ -111,6 +113,8 @@ const MMIO_LEN: u64 = 0x1000; #[cfg(target_arch = "x86_64")] const IOAPIC_DEVICE_NAME: &str = "__ioapic"; const SERIAL_DEVICE_NAME: &str = "__serial"; +#[cfg(target_arch = "x86_64")] +const DEBUGCON_DEVICE_NAME: &str = "__debug_console"; #[cfg(target_arch = "aarch64")] const GPIO_DEVICE_NAME: &str = "__gpio"; const RNG_DEVICE_NAME: &str = "__rng"; @@ -249,6 +253,10 @@ pub enum DeviceManagerError { /// Error creating serial output file SerialOutputFileOpen(io::Error), + #[cfg(target_arch = "x86_64")] + /// Error creating debug-console output file + DebugconOutputFileOpen(io::Error), + /// Error creating console output file ConsoleOutputFileOpen(io::Error), @@ -258,6 +266,9 @@ pub enum DeviceManagerError { /// Error creating console pty ConsolePtyOpen(io::Error), + /// Error creating console pty + DebugconPtyOpen(io::Error), + /// Error setting pty raw mode SetPtyRaw(vmm_sys_util::errno::Error), @@ -819,6 +830,9 @@ pub struct DeviceManager { // serial PTY serial_pty: Option>>, + // debug-console PTY + debug_console_pty: Option>>, + // Serial Manager serial_manager: Option>, @@ -1166,6 +1180,7 @@ impl DeviceManager { serial_pty: None, serial_manager: None, console_pty: None, + debug_console_pty: None, console_resize_pipe: None, original_termios_opt: Arc::new(Mutex::new(None)), virtio_mem_devices: Vec::new(), @@ -1209,6 +1224,12 @@ impl DeviceManager { .map(|pty| pty.lock().unwrap().clone()) } + pub fn debug_console_pty(&self) -> Option { + self.debug_console_pty + .as_ref() + .map(|pty| pty.lock().unwrap().clone()) + } + pub fn console_resize_pipe(&self) -> Option> { self.console_resize_pipe.as_ref().map(Arc::clone) } @@ -1217,6 +1238,7 @@ impl DeviceManager { &mut self, serial_pty: Option, console_pty: Option, + debug_console_pty: Option, console_resize_pipe: Option, original_termios_opt: Arc>>, ) -> DeviceManagerResult<()> { @@ -1276,11 +1298,12 @@ impl DeviceManager { self.original_termios_opt = original_termios_opt; - self.console = self.add_console_device( + self.console = self.add_console_devices( &legacy_interrupt_manager, &mut virtio_devices, serial_pty, console_pty, + debug_console_pty, console_resize_pipe, )?; @@ -1799,6 +1822,53 @@ impl DeviceManager { Ok(()) } + #[cfg(target_arch = "x86_64")] + fn add_debug_console_device( + &mut self, + debug_console_writer: Box, + ) -> DeviceManagerResult>> { + let id = String::from(DEBUGCON_DEVICE_NAME); + let debug_console = Arc::new(Mutex::new(DebugConsole::new( + id.clone(), + debug_console_writer, + ))); + + let port = self + .config + .lock() + .unwrap() + .debug_console + .clone() + .iobase + .map(|port| port as u64) + .unwrap_or(debug_console::DEFAULT_PORT); + + self.bus_devices + .push(Arc::clone(&debug_console) as Arc>); + + self.address_manager + .allocator + .lock() + .unwrap() + .allocate_io_addresses(Some(GuestAddress(port)), 0x1, None) + .ok_or(DeviceManagerError::AllocateIoPort)?; + + self.address_manager + .io_bus + .insert(debug_console.clone(), port, 0x1) + .map_err(DeviceManagerError::BusError)?; + + // Fill the device tree with a new node. In case of restore, we + // know there is nothing to do, so we can simply override the + // existing entry. + self.device_tree + .lock() + .unwrap() + .insert(id.clone(), device_node!(id, debug_console)); + + Ok(debug_console) + } + #[cfg(target_arch = "x86_64")] fn add_serial_device( &mut self, @@ -2086,12 +2156,19 @@ impl DeviceManager { }) } - fn add_console_device( + /// Adds all devices that behave like a console with respect to the VM + /// configuration. This includes: + /// - debug-console + /// - serial-console + /// - virtio-console + fn add_console_devices( &mut self, interrupt_manager: &Arc>, virtio_devices: &mut Vec, serial_pty: Option, console_pty: Option, + #[cfg(target_arch = "x86_64")] debug_console_pty: Option, + #[cfg(not(target_arch = "x86_64"))] _: Option, console_resize_pipe: Option, ) -> DeviceManagerResult> { let serial_config = self.config.lock().unwrap().serial.clone(); @@ -2101,7 +2178,7 @@ impl DeviceManager { .map_err(DeviceManagerError::SerialOutputFileOpen)?, )), ConsoleOutputMode::Pty => { - if let Some(pty) = serial_pty { + if let Some(pty) = serial_pty.clone() { self.config.lock().unwrap().serial.file = Some(pty.path.clone()); self.serial_pty = Some(Arc::new(Mutex::new(pty))); } else { @@ -2149,6 +2226,44 @@ impl DeviceManager { }; } + #[cfg(target_arch = "x86_64")] + { + let debug_console_config = self.config.lock().unwrap().debug_console.clone(); + let debug_console_writer: Option> = match debug_console_config + .mode + { + ConsoleOutputMode::File => Some(Box::new( + File::create(debug_console_config.file.as_ref().unwrap()) + .map_err(DeviceManagerError::DebugconOutputFileOpen)?, + )), + ConsoleOutputMode::Pty => { + if let Some(pty) = debug_console_pty { + self.config.lock().unwrap().debug_console.file = Some(pty.path.clone()); + self.debug_console_pty = Some(Arc::new(Mutex::new(pty))); + } else { + let (main, sub, path) = + create_pty().map_err(DeviceManagerError::DebugconPtyOpen)?; + self.set_raw_mode(&sub) + .map_err(DeviceManagerError::SetPtyRaw)?; + self.config.lock().unwrap().debug_console.file = Some(path.clone()); + self.debug_console_pty = Some(Arc::new(Mutex::new(PtyPair { main, path }))); + } + None + } + ConsoleOutputMode::Tty => { + let out = stdout(); + let _ = self.set_raw_mode(&out); + Some(Box::new(out)) + } + ConsoleOutputMode::Off | ConsoleOutputMode::Null | ConsoleOutputMode::Socket => { + None + } + }; + if let Some(writer) = debug_console_writer { + let _ = self.add_debug_console_device(writer)?; + } + } + let console_resizer = self.add_virtio_console_device(virtio_devices, console_pty, console_resize_pipe)?; diff --git a/vmm/src/lib.rs b/vmm/src/lib.rs index 2fa07ffc8..121f14a52 100644 --- a/vmm/src/lib.rs +++ b/vmm/src/lib.rs @@ -801,6 +801,7 @@ impl Vmm { None, None, None, + None, Arc::clone(&self.original_termios_opt), Some(snapshot), ) @@ -1244,6 +1245,7 @@ impl RequestHandler for Vmm { None, None, None, + None, Arc::clone(&self.original_termios_opt), None, None, @@ -1343,6 +1345,7 @@ impl RequestHandler for Vmm { None, None, None, + None, Arc::clone(&self.original_termios_opt), Some(snapshot), Some(source_url), @@ -1377,17 +1380,24 @@ impl RequestHandler for Vmm { fn vm_reboot(&mut self) -> result::Result<(), VmError> { // First we stop the current VM - let (config, serial_pty, console_pty, console_resize_pipe) = + let (config, serial_pty, console_pty, debug_console_pty, console_resize_pipe) = if let Some(mut vm) = self.vm.take() { let config = vm.get_config(); let serial_pty = vm.serial_pty(); let console_pty = vm.console_pty(); + let debug_console_pty = vm.debug_console_pty(); let console_resize_pipe = vm .console_resize_pipe() .as_ref() .map(|pipe| pipe.try_clone().unwrap()); vm.shutdown()?; - (config, serial_pty, console_pty, console_resize_pipe) + ( + config, + serial_pty, + console_pty, + debug_console_pty, + console_resize_pipe, + ) } else { return Err(VmError::VmNotCreated); }; @@ -1423,6 +1433,7 @@ impl RequestHandler for Vmm { activate_evt, serial_pty, console_pty, + debug_console_pty, console_resize_pipe, Arc::clone(&self.original_termios_opt), None, @@ -2020,6 +2031,8 @@ const DEVICE_MANAGER_SNAPSHOT_ID: &str = "device-manager"; #[cfg(test)] mod unit_tests { use super::*; + #[cfg(target_arch = "x86_64")] + use crate::config::DebugConsoleConfig; use config::{ ConsoleConfig, ConsoleOutputMode, CpusConfig, HotplugMethod, MemoryConfig, PayloadConfig, RngConfig, VmConfig, @@ -2094,6 +2107,8 @@ mod unit_tests { iommu: false, socket: None, }, + #[cfg(target_arch = "x86_64")] + debug_console: DebugConsoleConfig::default(), devices: None, user_devices: None, vdpa: None, diff --git a/vmm/src/vm.rs b/vmm/src/vm.rs index ab0f41774..c3346b1ff 100644 --- a/vmm/src/vm.rs +++ b/vmm/src/vm.rs @@ -479,6 +479,7 @@ impl Vm { timestamp: Instant, serial_pty: Option, console_pty: Option, + debug_console_pty: Option, console_resize_pipe: Option, original_termios: Arc>>, snapshot: Option, @@ -631,6 +632,7 @@ impl Vm { .create_devices( serial_pty, console_pty, + debug_console_pty, console_resize_pipe, original_termios, ) @@ -786,6 +788,7 @@ impl Vm { activate_evt: EventFd, serial_pty: Option, console_pty: Option, + debug_console_pty: Option, console_resize_pipe: Option, original_termios: Arc>>, snapshot: Option, @@ -865,6 +868,7 @@ impl Vm { timestamp, serial_pty, console_pty, + debug_console_pty, console_resize_pipe, original_termios, snapshot, @@ -1299,6 +1303,10 @@ impl Vm { self.device_manager.lock().unwrap().console_pty() } + pub fn debug_console_pty(&self) -> Option { + self.device_manager.lock().unwrap().debug_console_pty() + } + pub fn console_resize_pipe(&self) -> Option> { self.device_manager.lock().unwrap().console_resize_pipe() } diff --git a/vmm/src/vm_config.rs b/vmm/src/vm_config.rs index 677bf61c0..6e5ec9ad7 100644 --- a/vmm/src/vm_config.rs +++ b/vmm/src/vm_config.rs @@ -396,6 +396,27 @@ pub fn default_consoleconfig_file() -> Option { None } +#[cfg(target_arch = "x86_64")] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] +pub struct DebugConsoleConfig { + #[serde(default)] + pub file: Option, + pub mode: ConsoleOutputMode, + /// Optionally dedicated I/O-port, if the default port should not be used. + pub iobase: Option, +} + +#[cfg(target_arch = "x86_64")] +impl Default for DebugConsoleConfig { + fn default() -> Self { + Self { + file: None, + mode: ConsoleOutputMode::Off, + iobase: Some(devices::debug_console::DEFAULT_PORT as u16), + } + } +} + #[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] pub struct DeviceConfig { pub path: PathBuf, @@ -537,6 +558,9 @@ pub struct VmConfig { pub serial: ConsoleConfig, #[serde(default = "default_console")] pub console: ConsoleConfig, + #[cfg(target_arch = "x86_64")] + #[serde(default)] + pub debug_console: DebugConsoleConfig, pub devices: Option>, pub user_devices: Option>, pub vdpa: Option>,