blob: e6d7d52cc66b02e7d41f6b35b3d9b9f6ec0fb831 [file] [log] [blame]
// Copyright 2021 The ChromiumOS Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
use std::path::PathBuf;
use anyhow::anyhow;
use anyhow::bail;
use anyhow::Context;
use argh::FromArgs;
use base::error;
use base::Event;
use base::RawDescriptor;
use base::Terminal;
use cros_async::Executor;
use data_model::DataInit;
use hypervisor::ProtectionType;
use vm_memory::GuestMemory;
use vmm_vhost::message::VhostUserProtocolFeatures;
use vmm_vhost::message::VhostUserVirtioFeatures;
use crate::virtio;
use crate::virtio::console::asynchronous::ConsoleDevice;
use crate::virtio::console::virtio_console_config;
use crate::virtio::copy_config;
use crate::virtio::vhost::user::device::handler::sys::Doorbell;
use crate::virtio::vhost::user::device::handler::VhostUserBackend;
use crate::virtio::vhost::user::device::listener::sys::VhostUserListener;
use crate::virtio::vhost::user::device::listener::VhostUserListenerTrait;
use crate::virtio::vhost::user::device::VhostUserDevice;
use crate::SerialHardware;
use crate::SerialParameters;
use crate::SerialType;
const MAX_QUEUE_NUM: usize = 2 /* transmit and receive queues */;
/// Console device for use with vhost-user. Will set stdin back to canon mode if we are getting
/// input from it.
pub struct VhostUserConsoleDevice {
console: ConsoleDevice,
/// Whether we should set stdin to raw mode because we are getting user input from there.
raw_stdin: bool,
}
impl Drop for VhostUserConsoleDevice {
fn drop(&mut self) {
if self.raw_stdin {
// Restore terminal capabilities back to what they were before
match std::io::stdin().set_canon_mode() {
Ok(()) => (),
Err(e) => error!("failed to restore canonical mode for terminal: {:#}", e),
}
}
}
}
impl VhostUserDevice for VhostUserConsoleDevice {
fn max_queue_num(&self) -> usize {
MAX_QUEUE_NUM
}
fn into_backend(self: Box<Self>, ex: &Executor) -> anyhow::Result<Box<dyn VhostUserBackend>> {
if self.raw_stdin {
// Set stdin() to raw mode so we can send over individual keystrokes unbuffered
std::io::stdin()
.set_raw_mode()
.context("failed to set terminal in raw mode")?;
}
Ok(Box::new(ConsoleBackend {
device: *self,
acked_features: 0,
acked_protocol_features: VhostUserProtocolFeatures::empty(),
ex: ex.clone(),
}))
}
}
struct ConsoleBackend {
device: VhostUserConsoleDevice,
acked_features: u64,
acked_protocol_features: VhostUserProtocolFeatures,
ex: Executor,
}
impl VhostUserBackend for ConsoleBackend {
fn max_queue_num(&self) -> usize {
self.device.max_queue_num()
}
fn features(&self) -> u64 {
self.device.console.avail_features() | VhostUserVirtioFeatures::PROTOCOL_FEATURES.bits()
}
fn ack_features(&mut self, value: u64) -> anyhow::Result<()> {
let unrequested_features = value & !self.features();
if unrequested_features != 0 {
bail!("invalid features are given: {:#x}", unrequested_features);
}
self.acked_features |= value;
Ok(())
}
fn acked_features(&self) -> u64 {
self.acked_features
}
fn protocol_features(&self) -> VhostUserProtocolFeatures {
VhostUserProtocolFeatures::CONFIG
}
fn ack_protocol_features(&mut self, features: u64) -> anyhow::Result<()> {
let features = VhostUserProtocolFeatures::from_bits(features)
.ok_or_else(|| anyhow!("invalid protocol features are given: {:#x}", features))?;
let supported = self.protocol_features();
self.acked_protocol_features = features & supported;
Ok(())
}
fn acked_protocol_features(&self) -> u64 {
self.acked_protocol_features.bits()
}
fn read_config(&self, offset: u64, data: &mut [u8]) {
let config = virtio_console_config {
max_nr_ports: 1.into(),
..Default::default()
};
copy_config(data, 0, config.as_slice(), offset);
}
fn reset(&mut self) {
for queue_num in 0..self.max_queue_num() {
self.stop_queue(queue_num);
}
}
fn start_queue(
&mut self,
idx: usize,
queue: virtio::Queue,
mem: GuestMemory,
doorbell: Doorbell,
kick_evt: Event,
) -> anyhow::Result<()> {
match idx {
// ReceiveQueue
0 => self
.device
.console
.start_receive_queue(&self.ex, mem, queue, doorbell, kick_evt),
// TransmitQueue
1 => self
.device
.console
.start_transmit_queue(&self.ex, mem, queue, doorbell, kick_evt),
_ => bail!("attempted to start unknown queue: {}", idx),
}
}
fn stop_queue(&mut self, idx: usize) {
match idx {
0 => {
if let Err(e) = self.device.console.stop_receive_queue() {
error!("error while stopping rx queue: {}", e);
}
}
1 => {
if let Err(e) = self.device.console.stop_transmit_queue() {
error!("error while stopping tx queue: {}", e);
}
}
_ => error!("attempted to stop unknown queue: {}", idx),
};
}
}
#[derive(FromArgs)]
#[argh(subcommand, name = "console")]
/// Console device
pub struct Options {
#[argh(option, arg_name = "PATH")]
/// path to a vhost-user socket
socket: Option<String>,
#[argh(option, arg_name = "STRING")]
/// VFIO-PCI device name (e.g. '0000:00:07.0')
vfio: Option<String>,
#[argh(option, arg_name = "OUTFILE")]
/// path to a file
output_file: Option<PathBuf>,
#[argh(option, arg_name = "INFILE")]
/// path to a file
input_file: Option<PathBuf>,
/// whether we are logging to syslog or not
#[argh(switch)]
syslog: bool,
}
/// Return a new vhost-user console device. `params` are the device's configuration, and `keep_rds`
/// is a vector into which `RawDescriptors` that need to survive a fork are added, in case the
/// device is meant to run within a child process.
pub fn create_vu_console_device(
params: &SerialParameters,
keep_rds: &mut Vec<RawDescriptor>,
) -> anyhow::Result<VhostUserConsoleDevice> {
let device = params.create_serial_device::<ConsoleDevice>(
ProtectionType::Unprotected,
// We need to pass an event as per Serial Device API but we don't really use it anyway.
&Event::new()?,
keep_rds,
)?;
Ok(VhostUserConsoleDevice {
console: device,
raw_stdin: params.stdin,
})
}
/// Starts a vhost-user console device.
/// Returns an error if the given `args` is invalid or the device fails to run.
pub fn run_console_device(opts: Options) -> anyhow::Result<()> {
let type_ = match opts.output_file {
Some(_) => {
if opts.syslog {
bail!("--output-file and --syslog options cannot be used together.");
}
SerialType::File
}
None => {
if opts.syslog {
SerialType::Syslog
} else {
SerialType::Stdout
}
}
};
let params = SerialParameters {
type_,
hardware: SerialHardware::VirtioConsole,
// Required only if type_ is SerialType::File or SerialType::UnixSocket
path: opts.output_file,
input: opts.input_file,
num: 1,
console: true,
earlycon: false,
// We don't use stdin if syslog mode is enabled
stdin: !opts.syslog,
out_timestamp: false,
..Default::default()
};
// We won't jail the device and can simply ignore `keep_rds`.
let device = Box::new(create_vu_console_device(&params, &mut Vec::new())?);
let ex = Executor::new().context("Failed to create executor")?;
let listener = VhostUserListener::new_from_socket_or_vfio(
&opts.socket,
&opts.vfio,
device.max_queue_num(),
None,
)?;
listener.run_device(ex, device)
}