1 // Copyright 2024 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 //! Testing virtio-console multiport feature.
6
7 #![cfg(any(target_os = "android", target_os = "linux"))]
8
9 use std::ffi::CString;
10 use std::fs::read_to_string;
11 use std::fs::OpenOptions;
12 use std::io::Read;
13 use std::io::Write;
14 use std::path::PathBuf;
15
16 use base::error;
17 use base::EventToken;
18 use base::WaitContext;
19 use base::WorkerThread;
20 use fixture::utils::create_vu_console_multiport_config;
21 use fixture::vhost_user::VhostUserBackend;
22 use fixture::vm::Config as VmConfig;
23 use fixture::vm::TestVm;
24 use tempfile::NamedTempFile;
25 use tempfile::TempDir;
26
run_vhost_user_console_multiport_test_portname(config: VmConfig) -> anyhow::Result<()>27 fn run_vhost_user_console_multiport_test_portname(config: VmConfig) -> anyhow::Result<()> {
28 let socket = NamedTempFile::new().unwrap();
29 let temp_dir = TempDir::new()?;
30
31 // Prepare 2 virtio-console with only output
32 let file_path = vec![
33 (temp_dir.path().join("vconsole0.out"), PathBuf::new()),
34 (temp_dir.path().join("vconsole1.out"), PathBuf::new()),
35 ];
36 let vu_config = create_vu_console_multiport_config(socket.path(), file_path.clone());
37 let _vu_device = VhostUserBackend::new(vu_config).unwrap();
38
39 let config = config.extra_args(vec![
40 "--mem".to_owned(),
41 "512".to_owned(),
42 "--vhost-user-console".to_string(),
43 socket.path().to_str().unwrap().to_string(),
44 ]);
45 let mut vm = TestVm::new(config).unwrap();
46
47 // mount sysfs to check details
48 vm.exec_in_guest("mount -t sysfs sysfs /sys")?;
49
50 // Get portlist
51 let result = vm
52 .exec_in_guest("ls /sys/class/virtio-ports/")
53 .expect("No virtio-ports dir");
54 let mut portlist: Vec<&str> = result.stdout.trim_end().split('\n').collect();
55 // Remove serial virtio-console created defaultly
56 portlist.remove(0);
57 for (i, port) in portlist.into_iter().enumerate() {
58 let portname = vm
59 .exec_in_guest(format!("cat /sys/class/virtio-ports/{}/name", port).as_str())
60 .expect("Failed to read portname")
61 .stdout;
62 assert_eq!(portname.trim_end(), format!("port{}", i).as_str());
63 }
64 Ok(())
65 }
66
67 /// Tests vhost-user console device with `crosvm device`.
68 #[test]
vhost_user_console_portname_check() -> anyhow::Result<()>69 fn vhost_user_console_portname_check() -> anyhow::Result<()> {
70 let config = VmConfig::new();
71 run_vhost_user_console_multiport_test_portname(config)?;
72 Ok(())
73 }
74
run_vhost_user_console_multiport_test_output(config: VmConfig) -> anyhow::Result<()>75 fn run_vhost_user_console_multiport_test_output(config: VmConfig) -> anyhow::Result<()> {
76 let socket = NamedTempFile::new().unwrap();
77 let temp_dir = TempDir::new()?;
78
79 // Prepare 2 virtio-console with only output
80 let file_path = vec![
81 (temp_dir.path().join("vconsole0.out"), PathBuf::new()),
82 (temp_dir.path().join("vconsole1.out"), PathBuf::new()),
83 ];
84 let vu_config = create_vu_console_multiport_config(socket.path(), file_path.clone());
85 let _vu_device = VhostUserBackend::new(vu_config).unwrap();
86
87 let config = config.extra_args(vec![
88 "--mem".to_owned(),
89 "512".to_owned(),
90 "--vhost-user-console".to_string(),
91 socket.path().to_str().unwrap().to_string(),
92 ]);
93 let mut vm = TestVm::new(config).unwrap();
94
95 // mount sysfs to check details
96 vm.exec_in_guest("mount -t sysfs sysfs /sys")?;
97
98 // Get portlist
99 let result = vm
100 .exec_in_guest("ls /sys/class/virtio-ports/")
101 .expect("No virtio-ports dir");
102 let mut portlist: Vec<&str> = result.stdout.trim_end().split('\n').collect();
103 // Remove serial virtio-console created defaultly
104 portlist.remove(0);
105
106 // Test output flow.
107 for (i, port) in portlist.into_iter().enumerate() {
108 vm.exec_in_guest(format!("echo \"hello {}\" > /dev/{}", port, port).as_str())
109 .expect("Failed to echo data to port");
110
111 let data = read_to_string(&file_path[i].0).expect("vu-console: read output failed");
112
113 assert_eq!(data.trim(), format!("hello {}", port).as_str());
114 }
115 Ok(())
116 }
117
118 #[test]
vhost_user_console_check_output() -> anyhow::Result<()>119 fn vhost_user_console_check_output() -> anyhow::Result<()> {
120 let config = VmConfig::new();
121 run_vhost_user_console_multiport_test_output(config)?;
122 Ok(())
123 }
124
125 /// Generate the workthread to monitor input and transmit data to output fifo
126 ///
127 /// Create fifo according to input and output name.
128 /// Then spawn a thread to monitor them, simultaneously watch a kill_event to stop thread.
generate_workthread_to_monitor_fifo( idx: usize, infile: PathBuf, outfile: PathBuf, ) -> WorkerThread<()>129 fn generate_workthread_to_monitor_fifo(
130 idx: usize,
131 infile: PathBuf,
132 outfile: PathBuf,
133 ) -> WorkerThread<()> {
134 #[derive(EventToken)]
135 enum Token {
136 InputDataAvailable,
137 Kill,
138 }
139 let cpath_in = CString::new(infile.to_str().unwrap()).unwrap();
140 let cpath_out = CString::new(outfile.to_str().unwrap()).unwrap();
141 // SAFETY: make two fifos here for monitor thread, path is guaranteed to be valid
142 unsafe {
143 libc::mkfifo(cpath_in.as_ptr(), 0o777);
144 libc::mkfifo(cpath_out.as_ptr(), 0o777);
145 }
146 WorkerThread::start(format!("monitor_vconsole{}", idx), move |kill_event| {
147 let mut tx = OpenOptions::new().write(true).open(outfile).unwrap();
148 let mut rx = OpenOptions::new().read(true).open(infile).unwrap();
149 let mut msg = vec![0; 256];
150 let wait_ctx: WaitContext<Token> = match WaitContext::build_with(&[
151 (&rx, Token::InputDataAvailable),
152 (&kill_event, Token::Kill),
153 ]) {
154 Ok(wait_ctx) => wait_ctx,
155 Err(e) => {
156 error!("failed creating WaitContext: {}", e);
157 return;
158 }
159 };
160 'monitor_loop: loop {
161 let wait_events = match wait_ctx.wait() {
162 Ok(wait_events) => wait_events,
163 Err(e) => {
164 error!("failed polling for events: {}", e);
165 break;
166 }
167 };
168 for wait_event in wait_events.iter().filter(|e| e.is_readable) {
169 match wait_event.token {
170 Token::InputDataAvailable => {
171 let bytes = rx.read(&mut msg).expect("Failed to read from port");
172 if bytes > 0 {
173 if tx.write_all(&msg.to_ascii_uppercase()[..bytes]).is_err() {
174 break 'monitor_loop;
175 }
176 }
177 }
178 Token::Kill => break 'monitor_loop,
179 }
180 }
181 }
182 })
183 }
184
185 /// Tests vhost-user-console input function with multiport feature.
186 ///
187 /// If we want to test multiport function about input flow,
188 /// we need to prepare monitor threads for each ports.
189 /// The purpose of this thread is to get all data from rx queue, and transmit them to tx queue.
190 /// To increase reliability, monitor thread changes data to uppercase.
191 ///
192 /// Once monitor threads created, VhostUserBackend for console will work as expected.
run_vhost_user_console_multiport_test_input(config: VmConfig) -> anyhow::Result<()>193 fn run_vhost_user_console_multiport_test_input(config: VmConfig) -> anyhow::Result<()> {
194 let socket = NamedTempFile::new().unwrap();
195 let temp_dir = TempDir::new()?;
196
197 // Prepare 2 virtio-console with both input and output
198 let mut file_path = vec![];
199 for idx in 0..2 {
200 let fifo_name_out = format!("vconsole{}.out", idx);
201 let fifo_name_in = format!("vconsole{}.in", idx);
202 file_path.push((
203 temp_dir.path().join(fifo_name_out),
204 temp_dir.path().join(fifo_name_in),
205 ));
206 }
207
208 let mut thread_vec = vec![];
209 for idx in 0..2 {
210 thread_vec.push(generate_workthread_to_monitor_fifo(
211 idx,
212 (*file_path.get(idx).unwrap().0).to_path_buf(),
213 (*file_path.get(idx).unwrap().1).to_path_buf(),
214 ));
215 }
216
217 let vu_config = create_vu_console_multiport_config(socket.path(), file_path.clone());
218 let _vu_device = VhostUserBackend::new(vu_config).unwrap();
219
220 let config = config.extra_args(vec![
221 "--mem".to_owned(),
222 "512".to_owned(),
223 "--vhost-user-console".to_string(),
224 socket.path().to_str().unwrap().to_string(),
225 ]);
226 let mut vm = TestVm::new(config).unwrap();
227
228 // mount sysfs to check details
229 vm.exec_in_guest("mount -t sysfs sysfs /sys")?;
230
231 // Get portlist
232 let result = vm
233 .exec_in_guest("ls /sys/class/virtio-ports/")
234 .expect("No virtio-ports dir");
235 let mut portlist: Vec<&str> = result.stdout.trim_end().split('\n').collect();
236 // Remove serial virtio-console created defaultly
237 portlist.remove(0);
238
239 let file_fd = 5;
240 // Test input flow.
241 for port in portlist.into_iter() {
242 // Bind file_fd to operate /dev/vportXpX, then write to fd, finnally read it.
243 let result = vm
244 .exec_in_guest(
245 format!(
246 "exec {}<>/dev/{} && echo \"hello {}\" >&{} && head -1 <&{}",
247 file_fd, port, port, file_fd, file_fd
248 )
249 .as_str(),
250 )
251 .expect("Failed to echo data to port")
252 .stdout;
253 // Close this fd
254 vm.exec_in_guest(format!("exec {}>&-", file_fd).as_str())
255 .expect("Failed to close device fd");
256 // In monitor thread, tx message will change to uppercase
257 assert_eq!(
258 result.trim_end(),
259 format!("hello {}", port).to_uppercase().as_str()
260 );
261 }
262 for handler in thread_vec.into_iter() {
263 handler.stop();
264 }
265 Ok(())
266 }
267
268 #[test]
vhost_user_console_check_input() -> anyhow::Result<()>269 fn vhost_user_console_check_input() -> anyhow::Result<()> {
270 let config = VmConfig::new();
271 run_vhost_user_console_multiport_test_input(config)?;
272 Ok(())
273 }
274