xref: /aosp_15_r20/external/crosvm/e2e_tests/tests/console.rs (revision bb4ee6a4ae7042d18b07a98463b9c8b875e44b39)
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