xref: /aosp_15_r20/external/crosvm/devices/src/virtio/vhost/user/device/console.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
1 // Copyright 2021 The ChromiumOS Authors
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4 
5 use std::path::PathBuf;
6 
7 use anyhow::anyhow;
8 use anyhow::bail;
9 use anyhow::Context;
10 use argh::FromArgs;
11 use base::error;
12 use base::Event;
13 use base::RawDescriptor;
14 use base::Terminal;
15 use cros_async::Executor;
16 use hypervisor::ProtectionType;
17 use vm_memory::GuestMemory;
18 use vmm_vhost::message::VhostUserProtocolFeatures;
19 use vmm_vhost::VHOST_USER_F_PROTOCOL_FEATURES;
20 
21 use crate::virtio::console::device::ConsoleDevice;
22 use crate::virtio::console::device::ConsoleSnapshot;
23 use crate::virtio::console::port::ConsolePort;
24 use crate::virtio::vhost::user::device::handler::DeviceRequestHandler;
25 use crate::virtio::vhost::user::device::handler::VhostUserDevice;
26 use crate::virtio::vhost::user::device::BackendConnection;
27 use crate::virtio::vhost::user::device::VhostUserDeviceBuilder;
28 use crate::virtio::Queue;
29 use crate::SerialHardware;
30 use crate::SerialParameters;
31 use crate::SerialType;
32 
33 /// Console device for use with vhost-user. Will set stdin back to canon mode if we are getting
34 /// input from it.
35 pub struct VhostUserConsoleDevice {
36     console: ConsoleDevice,
37     /// Whether we should set stdin to raw mode because we are getting user input from there.
38     raw_stdin: bool,
39 }
40 
41 impl Drop for VhostUserConsoleDevice {
drop(&mut self)42     fn drop(&mut self) {
43         if self.raw_stdin {
44             // Restore terminal capabilities back to what they were before
45             match std::io::stdin().set_canon_mode() {
46                 Ok(()) => (),
47                 Err(e) => error!("failed to restore canonical mode for terminal: {:#}", e),
48             }
49         }
50     }
51 }
52 
53 impl VhostUserDeviceBuilder for VhostUserConsoleDevice {
build(mut self: Box<Self>, _ex: &Executor) -> anyhow::Result<Box<dyn vmm_vhost::Backend>>54     fn build(mut self: Box<Self>, _ex: &Executor) -> anyhow::Result<Box<dyn vmm_vhost::Backend>> {
55         if self.raw_stdin {
56             // Set stdin() to raw mode so we can send over individual keystrokes unbuffered
57             std::io::stdin()
58                 .set_raw_mode()
59                 .context("failed to set terminal in raw mode")?;
60         }
61 
62         self.console.start_input_threads();
63 
64         let backend = ConsoleBackend { device: *self };
65 
66         let handler = DeviceRequestHandler::new(backend);
67         Ok(Box::new(handler))
68     }
69 }
70 
71 struct ConsoleBackend {
72     device: VhostUserConsoleDevice,
73 }
74 
75 impl VhostUserDevice for ConsoleBackend {
max_queue_num(&self) -> usize76     fn max_queue_num(&self) -> usize {
77         self.device.console.max_queues()
78     }
79 
features(&self) -> u6480     fn features(&self) -> u64 {
81         self.device.console.features() | 1 << VHOST_USER_F_PROTOCOL_FEATURES
82     }
83 
protocol_features(&self) -> VhostUserProtocolFeatures84     fn protocol_features(&self) -> VhostUserProtocolFeatures {
85         VhostUserProtocolFeatures::CONFIG
86             | VhostUserProtocolFeatures::MQ
87             | VhostUserProtocolFeatures::DEVICE_STATE
88     }
89 
read_config(&self, offset: u64, data: &mut [u8])90     fn read_config(&self, offset: u64, data: &mut [u8]) {
91         self.device.console.read_config(offset, data);
92     }
93 
reset(&mut self)94     fn reset(&mut self) {
95         if let Err(e) = self.device.console.reset() {
96             error!("console reset failed: {:#}", e);
97         }
98     }
99 
start_queue(&mut self, idx: usize, queue: Queue, _mem: GuestMemory) -> anyhow::Result<()>100     fn start_queue(&mut self, idx: usize, queue: Queue, _mem: GuestMemory) -> anyhow::Result<()> {
101         self.device.console.start_queue(idx, queue)
102     }
103 
stop_queue(&mut self, idx: usize) -> anyhow::Result<Queue>104     fn stop_queue(&mut self, idx: usize) -> anyhow::Result<Queue> {
105         match self.device.console.stop_queue(idx) {
106             Ok(Some(queue)) => Ok(queue),
107             Ok(None) => Err(anyhow!("queue {idx} not started")),
108             Err(e) => Err(e).with_context(|| format!("failed to stop queue {idx}")),
109         }
110     }
111 
enter_suspended_state(&mut self) -> anyhow::Result<()>112     fn enter_suspended_state(&mut self) -> anyhow::Result<()> {
113         Ok(())
114     }
115 
snapshot(&mut self) -> anyhow::Result<serde_json::Value>116     fn snapshot(&mut self) -> anyhow::Result<serde_json::Value> {
117         let snap = self.device.console.snapshot()?;
118         serde_json::to_value(snap).context("failed to snapshot vhost-user console")
119     }
120 
restore(&mut self, data: serde_json::Value) -> anyhow::Result<()>121     fn restore(&mut self, data: serde_json::Value) -> anyhow::Result<()> {
122         let snap: ConsoleSnapshot =
123             serde_json::from_value(data).context("failed to deserialize vhost-user console")?;
124         self.device.console.restore(&snap)
125     }
126 }
127 
128 #[derive(FromArgs)]
129 #[argh(subcommand, name = "console")]
130 /// Console device
131 pub struct Options {
132     #[argh(option, arg_name = "PATH", hidden_help)]
133     /// deprecated - please use --socket-path instead
134     socket: Option<String>,
135     #[argh(option, arg_name = "PATH")]
136     /// path to the vhost-user socket to bind to.
137     /// If this flag is set, --fd cannot be specified.
138     socket_path: Option<String>,
139     #[argh(option, arg_name = "FD")]
140     /// file descriptor of a connected vhost-user socket.
141     /// If this flag is set, --socket-path cannot be specified.
142     fd: Option<RawDescriptor>,
143 
144     #[argh(option, arg_name = "OUTFILE")]
145     /// path to a file
146     output_file: Option<PathBuf>,
147     #[argh(option, arg_name = "INFILE")]
148     /// path to a file
149     input_file: Option<PathBuf>,
150     /// whether we are logging to syslog or not
151     #[argh(switch)]
152     syslog: bool,
153     #[argh(option, arg_name = "type=TYPE,[path=PATH,input=PATH,console]")]
154     /// multiport parameters
155     port: Vec<SerialParameters>,
156 }
157 
create_vu_multi_port_device( params: &[SerialParameters], keep_rds: &mut Vec<RawDescriptor>, ) -> anyhow::Result<VhostUserConsoleDevice>158 fn create_vu_multi_port_device(
159     params: &[SerialParameters],
160     keep_rds: &mut Vec<RawDescriptor>,
161 ) -> anyhow::Result<VhostUserConsoleDevice> {
162     let ports = params
163         .iter()
164         .map(|x| {
165             let port = x
166                 .create_serial_device::<ConsolePort>(
167                     ProtectionType::Unprotected,
168                     // We need to pass an event as per Serial Device API but we don't really use it
169                     // anyway.
170                     &Event::new()?,
171                     keep_rds,
172                 )
173                 .expect("failed to create multiport console");
174 
175             Ok(port)
176         })
177         .collect::<anyhow::Result<Vec<_>>>()?;
178 
179     let device = ConsoleDevice::new_multi_port(ProtectionType::Unprotected, ports);
180 
181     Ok(VhostUserConsoleDevice {
182         console: device,
183         raw_stdin: false, // currently we are not support stdin raw mode
184     })
185 }
186 
187 /// Starts a multiport enabled vhost-user console device.
188 /// Returns an error if the given `args` is invalid or the device fails to run.
run_multi_port_device(opts: Options) -> anyhow::Result<()>189 fn run_multi_port_device(opts: Options) -> anyhow::Result<()> {
190     if opts.port.is_empty() {
191         bail!("console: must have at least one `--port`");
192     }
193 
194     // We won't jail the device and can simply ignore `keep_rds`.
195     let device = Box::new(create_vu_multi_port_device(&opts.port, &mut Vec::new())?);
196     let ex = Executor::new().context("Failed to create executor")?;
197 
198     let conn =
199         BackendConnection::from_opts(opts.socket.as_deref(), opts.socket_path.as_deref(), opts.fd)?;
200     conn.run_device(ex, device)
201 }
202 
203 /// Return a new vhost-user console device. `params` are the device's configuration, and `keep_rds`
204 /// is a vector into which `RawDescriptors` that need to survive a fork are added, in case the
205 /// device is meant to run within a child process.
create_vu_console_device( params: &SerialParameters, keep_rds: &mut Vec<RawDescriptor>, ) -> anyhow::Result<VhostUserConsoleDevice>206 pub fn create_vu_console_device(
207     params: &SerialParameters,
208     keep_rds: &mut Vec<RawDescriptor>,
209 ) -> anyhow::Result<VhostUserConsoleDevice> {
210     let device = params.create_serial_device::<ConsoleDevice>(
211         ProtectionType::Unprotected,
212         // We need to pass an event as per Serial Device API but we don't really use it anyway.
213         &Event::new()?,
214         keep_rds,
215     )?;
216 
217     Ok(VhostUserConsoleDevice {
218         console: device,
219         raw_stdin: params.stdin,
220     })
221 }
222 
223 /// Starts a vhost-user console device.
224 /// Returns an error if the given `args` is invalid or the device fails to run.
run_console_device(opts: Options) -> anyhow::Result<()>225 pub fn run_console_device(opts: Options) -> anyhow::Result<()> {
226     // try to start a multiport console first
227     if !opts.port.is_empty() {
228         return run_multi_port_device(opts);
229     }
230 
231     // fall back to a multiport disabled console
232     let type_ = match opts.output_file {
233         Some(_) => {
234             if opts.syslog {
235                 bail!("--output-file and --syslog options cannot be used together.");
236             }
237             SerialType::File
238         }
239         None => {
240             if opts.syslog {
241                 SerialType::Syslog
242             } else {
243                 SerialType::Stdout
244             }
245         }
246     };
247 
248     let params = SerialParameters {
249         type_,
250         hardware: SerialHardware::VirtioConsole,
251         // Required only if type_ is SerialType::File or SerialType::UnixSocket
252         path: opts.output_file,
253         input: opts.input_file,
254         num: 1,
255         console: true,
256         earlycon: false,
257         // We don't use stdin if syslog mode is enabled
258         stdin: !opts.syslog,
259         out_timestamp: false,
260         ..Default::default()
261     };
262 
263     // We won't jail the device and can simply ignore `keep_rds`.
264     let device = Box::new(create_vu_console_device(&params, &mut Vec::new())?);
265     let ex = Executor::new().context("Failed to create executor")?;
266 
267     let conn =
268         BackendConnection::from_opts(opts.socket.as_deref(), opts.socket_path.as_deref(), opts.fd)?;
269 
270     conn.run_device(ex, device)
271 }
272