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(¶ms, &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