Skip to content

Commit 593cd5e

Browse files
Allow qemu test to work under Windows without WSL (#463)
* xtask: Add ChildWrapper This wraps the QEMU child process so that the process is killed on drop. This avoids leaving a dangling qemu behind if an unexpected error happens during the test. Also add a context message to the spawn error, makes it easier to see what's wrong if for example qemu isn't in $PATH. * xtask: Improve echo service lifecycle If the program ends while the echo service is still blocked in `recv_from`, it panics on Windows: ``` thread '<unnamed>' panicked at 'failed to receive packet: Os { code: 10004, kind: Uncategorized, message: "A blocking operation was interrupted by a call to WSACancelBlockingCall." }', xtask\src\net.rs:17:14 ``` Fix by using adding a timeout to the reads so that a shutdown flag can be checked. Also wrap the service in a struct to ensure join on drop. * xtask: Add platform module This module contains functions for determining the host platform. These are intended to be used instead of `#[cfg(...)]` blocks. Such blocks can hide a compilation problem on a different platform, whereas using these functions will allow stuff to be compiled for all platforms. * xtask: Restrict KVM to Linux KVM is a Linux feature, as far as I know not available on any other OSes. * xtask: Enable QEMU under Windows without WSL This updates the `Pipe` struct to work with the Windows version of named pipes as well as Unix pipes. The `Io` struct has also been modified a little bit so that JSON can be sent without a trailing newline, as that causes QEMU to hang on Windows. Fixes #461
1 parent fff4644 commit 593cd5e

File tree

7 files changed

+234
-79
lines changed

7 files changed

+234
-79
lines changed

xtask/Cargo.toml

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@ publish = false
66

77
[dependencies]
88
anyhow = "1.0.51"
9-
cfg-if = "1.0.0"
109
clap = { version = "3.2.1", features = ["derive"] }
1110
# The latest fatfs release (0.3.5) is old, use git instead to pick up some fixes.
1211
fatfs = { git = "https://github.com/rafalh/rust-fatfs.git", rev = "87fc1ed5074a32b4e0344fcdde77359ef9e75432" }

xtask/src/disk.rs

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
#![cfg(unix)]
2-
31
use anyhow::Result;
42
use fatfs::{Date, DateTime, FileSystem, FormatVolumeOptions, FsOptions, StdIoWrapper, Time};
53
use mbrman::{MBRPartitionEntry, CHS, MBR};

xtask/src/main.rs

Lines changed: 8 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@ mod cargo;
33
mod disk;
44
mod net;
55
mod opt;
6+
mod pipe;
7+
mod platform;
68
mod qemu;
79
mod util;
810

911
use anyhow::Result;
1012
use cargo::{fix_nested_cargo_env, Cargo, CargoAction, Feature, Package};
11-
use cfg_if::cfg_if;
1213
use clap::Parser;
1314
use opt::{Action, BuildOpt, ClippyOpt, DocOpt, MiriOpt, Opt, QemuOpt};
1415
use std::process::Command;
@@ -87,7 +88,11 @@ fn run_miri(opt: &MiriOpt) -> Result<()> {
8788
/// Build uefi-test-runner and run it in QEMU.
8889
fn run_vm_tests(opt: &QemuOpt) -> Result<()> {
8990
let mut features = vec![Feature::Qemu];
90-
if opt.ci {
91+
92+
// Always enable the ci feature when not building on Linux so that
93+
// the MP test is skipped. That test doesn't work with kvm disabled
94+
// (see https://github.com/rust-osdev/uefi-rs/issues/103).
95+
if opt.ci || !platform::is_linux() {
9196
features.push(Feature::Ci);
9297
}
9398

@@ -103,13 +108,7 @@ fn run_vm_tests(opt: &QemuOpt) -> Result<()> {
103108
};
104109
run_cmd(cargo.command()?)?;
105110

106-
cfg_if! {
107-
if #[cfg(unix)] {
108-
qemu::run_qemu(*opt.target, opt)
109-
} else {
110-
panic!("vm tests are only supported on unix targets");
111-
}
112-
}
111+
qemu::run_qemu(*opt.target, opt)
113112
}
114113

115114
/// Run unit tests and doctests on the host. Most of uefi-rs is tested

xtask/src/net.rs

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,68 @@
1-
#![cfg(unix)]
2-
31
use std::net::UdpSocket;
2+
use std::sync::{Arc, Mutex};
3+
use std::thread::{self, JoinHandle};
4+
use std::time::Duration;
5+
6+
/// Run a simple echo service that listens on UDP port 21572 and
7+
/// reverses the incoming messages.
8+
pub struct EchoService {
9+
stop_requested: Arc<Mutex<bool>>,
10+
11+
// `JoinHandle::join` consumes the handle, so put it in an option so
12+
// that we can call `join` in `drop`.
13+
join_handle: Option<JoinHandle<()>>,
14+
}
15+
16+
impl Drop for EchoService {
17+
fn drop(&mut self) {
18+
self.stop();
19+
self.join_handle
20+
.take()
21+
.unwrap()
22+
.join()
23+
.expect("failed to join echo service thread");
24+
}
25+
}
26+
27+
impl EchoService {
28+
/// Start the server.
29+
pub fn start() -> Self {
30+
let stop_requested = Arc::new(Mutex::new(false));
31+
let stop_requested_copy = stop_requested.clone();
32+
let join_handle = thread::spawn(|| reverse_echo_service(stop_requested_copy));
33+
Self {
34+
stop_requested,
35+
join_handle: Some(join_handle),
36+
}
37+
}
438

5-
/// Start a thread that listens on UDP port 21572 and simulates a simple echo
6-
/// service that reverses the incoming messages.
7-
pub fn start_reverse_echo_service() {
8-
std::thread::spawn(reverse_echo_service);
39+
/// Request that the server stop.
40+
pub fn stop(&self) {
41+
let mut guard = self.stop_requested.lock().unwrap();
42+
*guard = true;
43+
}
944
}
1045

11-
fn reverse_echo_service() {
46+
fn reverse_echo_service(stop_requested: Arc<Mutex<bool>>) {
1247
let socket = UdpSocket::bind(("127.0.0.1", 21572)).expect("failed to bind to UDP socket");
1348

49+
// Set a timeout so that the service can periodically check if a
50+
// stop has been requested.
51+
socket
52+
.set_read_timeout(Some(Duration::from_millis(100)))
53+
.expect("failed to set read timeout");
54+
1455
let mut buffer = [0; 257];
1556
loop {
57+
if *stop_requested.lock().unwrap() {
58+
break;
59+
}
60+
1661
// Receive a packet.
17-
let (len, addr) = socket
18-
.recv_from(&mut buffer)
19-
.expect("failed to receive packet");
62+
let (len, addr) = match socket.recv_from(&mut buffer) {
63+
Ok((len, addr)) => (len, addr),
64+
Err(_) => continue,
65+
};
2066
let buffer = &mut buffer[..len];
2167

2268
// Extract header information.

xtask/src/pipe.rs

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
use crate::platform;
2+
use crate::qemu::Io;
3+
use anyhow::Result;
4+
use fs_err::{File, OpenOptions};
5+
use std::path::{Path, PathBuf};
6+
use std::thread;
7+
use std::time::Duration;
8+
9+
pub struct Pipe {
10+
qemu_arg: String,
11+
input_path: PathBuf,
12+
output_path: PathBuf,
13+
}
14+
15+
impl Pipe {
16+
/// Prepare to set up a two-way communication pipe. This is called
17+
/// before launching QEMU. On Unix this uses `mkfifo` to create two
18+
/// pipes; on Windows QEMU itself will create the duplex pipe.
19+
pub fn new(dir: &Path, base_name: &'static str) -> Result<Self> {
20+
if platform::is_unix() {
21+
let qemu_arg = format!("pipe:{}", dir.join(base_name).to_str().unwrap());
22+
let input_path = dir.join(format!("{}.in", base_name));
23+
let output_path = dir.join(format!("{}.out", base_name));
24+
25+
// This part has to be conditionally compiled because the
26+
// `nix` interfaces don't exist when compiling under
27+
// Windows.
28+
#[cfg(unix)]
29+
{
30+
let mode = nix::sys::stat::Mode::from_bits(0o666).unwrap();
31+
nix::unistd::mkfifo(&input_path, mode)?;
32+
nix::unistd::mkfifo(&output_path, mode)?;
33+
}
34+
35+
Ok(Self {
36+
qemu_arg,
37+
input_path,
38+
output_path,
39+
})
40+
} else if platform::is_windows() {
41+
// No need to call the equivalent of `mkfifo` here; QEMU acts as
42+
// the named pipe server and creates the pipe itself.
43+
44+
Ok(Self {
45+
// QEMU adds the "\\.\pipe\" prefix automatically.
46+
qemu_arg: format!("pipe:{}", base_name),
47+
48+
// On Windows the pipe is duplex, so only one path
49+
// needed.
50+
input_path: format!(r"\\.\pipe\{}", base_name).into(),
51+
output_path: PathBuf::new(),
52+
})
53+
} else {
54+
unimplemented!();
55+
}
56+
}
57+
58+
pub fn qemu_arg(&self) -> &str {
59+
&self.qemu_arg
60+
}
61+
62+
/// Create an `Io` object for performing reads and writes.
63+
pub fn open_io(&self) -> Result<Io<File, File>> {
64+
let reader;
65+
let writer;
66+
67+
if platform::is_unix() {
68+
reader = File::open(&self.output_path)?;
69+
writer = OpenOptions::new().write(true).open(&self.input_path)?;
70+
} else if platform::is_windows() {
71+
// Connect to the pipe, then clone the resulting `File` so
72+
// that we can wrap the read side in a `BufReader`. The
73+
// reader and writer must share the same underlying
74+
// `Handle`, so this is different than opening the pipe
75+
// twice.
76+
writer = windows_open_pipe(&self.input_path)?;
77+
reader = writer.try_clone()?;
78+
} else {
79+
unimplemented!();
80+
}
81+
82+
Ok(Io::new(reader, writer))
83+
}
84+
}
85+
86+
/// Attempt to connect to a duplex named pipe in byte mode.
87+
fn windows_open_pipe(path: &Path) -> Result<File> {
88+
let max_attempts = 100;
89+
let mut attempt = 0;
90+
loop {
91+
attempt += 1;
92+
93+
match OpenOptions::new().read(true).write(true).open(&path) {
94+
Ok(file) => return Ok(file),
95+
Err(err) => {
96+
if attempt >= max_attempts {
97+
return Err(err)?;
98+
} else {
99+
// Sleep before trying again.
100+
thread::sleep(Duration::from_millis(100));
101+
}
102+
}
103+
}
104+
}
105+
}

xtask/src/platform.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
//! Functions to determine the host platform.
2+
//!
3+
//! Use the functions where possible instead of `#[cfg(...)]` so that
4+
//! code for all platforms gets checked at compile time.
5+
6+
use std::env::consts;
7+
8+
pub fn is_linux() -> bool {
9+
consts::OS == "linux"
10+
}
11+
12+
pub fn is_unix() -> bool {
13+
consts::FAMILY == "unix"
14+
}
15+
16+
pub fn is_windows() -> bool {
17+
consts::FAMILY == "windows"
18+
}

0 commit comments

Comments
 (0)