Repository: bytecodealliance/wasmtime
Pull request #7564 (2023-11-21 01:54:30)

move wasmtime-wasi's unit test for stdin to a separate integration test
fork is always a terrible idea, but when we wrote this test, we couldn't think of an alternative method. alex showed us how `/tests/host_segfault.rs` works, which solves a similar problem for measuring process behavior without forking.

the forking version of this test would occasionally hang in the child's creation of a tokio runtime because std Once is not fork-safe (nor should it be. nothing should be fork-safe. forks are an abomination).

so instead, this is now a separate integration test with `harness = false` that will exec itself in order to run the child.

<!--
Please make sure you include the following information:

- If this work has been discussed elsewhere, please include a link to that
  conversation. If it was discussed in an issue, just mention "issue #...".

- Explain why this change is needed. If the details are in an issue already,
  this can be brief.

Our development process is documented in the Wasmtime book:
https://docs.wasmtime.dev/contributing-development-process.html

Please ensure all communication follows the code of conduct:
https://github.com/bytecodealliance/wasmtime/blob/main/CODE_OF_CONDUCT.md
-->



Patch:
diff --git a/crates/wasi/Cargo.toml b/crates/wasi/Cargo.toml
index bce15799f..f75bc8532 100644
--- a/crates/wasi/Cargo.toml
+++ b/crates/wasi/Cargo.toml
@@ -9,7 +9,7 @@ keywords = ["webassembly", "wasm"]
 repository = "https://github.com/bytecodealliance/wasmtime"
 readme = "README.md"
 edition.workspace = true
-include = ["src/**/*", "README.md", "LICENSE", "build.rs", "witx/*", "wit/**/*"]
+include = ["src/**/*", "README.md", "LICENSE", "build.rs", "witx/*", "wit/**/*", "tests/*"]
 
 [lints]
 workspace = true
@@ -89,3 +89,8 @@ preview1-on-preview2 = [
     "preview2",
     "wiggle",
 ]
+
+[[test]]
+name = "process_stdin"
+harness = false
+
diff --git a/crates/wasi/src/preview2/stdio.rs b/crates/wasi/src/preview2/stdio.rs
index 29ec38f08..f99a5d2a2 100644
--- a/crates/wasi/src/preview2/stdio.rs
+++ b/crates/wasi/src/preview2/stdio.rs
@@ -257,194 +257,3 @@ impl<T: WasiView> terminal_stderr::Host for T {
         }
     }
 }
-
-#[cfg(all(unix, test))]
-mod test {
-    use crate::preview2::HostInputStream;
-    use libc;
-    use std::fs::File;
-    use std::io::{BufRead, BufReader, Write};
-    use std::os::fd::FromRawFd;
-
-    fn test_child_stdin<T, P>(child: T, parent: P)
-    where
-        T: FnOnce(File),
-        P: FnOnce(File, BufReader<File>),
-    {
-        unsafe {
-            // Make pipe for emulating stdin.
-            let mut stdin_fds: [libc::c_int; 2] = [0; 2];
-            assert_eq!(
-                libc::pipe(stdin_fds.as_mut_ptr()),
-                0,
-                "Failed to create stdin pipe"
-            );
-            let [stdin_read, stdin_write] = stdin_fds;
-
-            // Make pipe for getting results.
-            let mut result_fds: [libc::c_int; 2] = [0; 2];
-            assert_eq!(
-                libc::pipe(result_fds.as_mut_ptr()),
-                0,
-                "Failed to create result pipe"
-            );
-            let [result_read, result_write] = result_fds;
-
-            let child_pid = libc::fork();
-            if child_pid == 0 {
-                libc::close(stdin_write);
-                libc::close(result_read);
-
-                libc::close(libc::STDIN_FILENO);
-                libc::dup2(stdin_read, libc::STDIN_FILENO);
-
-                let result_write = File::from_raw_fd(result_write);
-                child(result_write);
-            } else {
-                libc::close(stdin_read);
-                libc::close(result_write);
-
-                let stdin_write = File::from_raw_fd(stdin_write);
-                let result_read = BufReader::new(File::from_raw_fd(result_read));
-                parent(stdin_write, result_read);
-            }
-        }
-    }
-
-    // This could even be parameterized somehow to use the worker thread stdin vs the asyncfd
-    // stdin.
-    fn test_stdin_by_forking<S, T>(mk_stdin: T)
-    where
-        S: HostInputStream,
-        T: Fn() -> S,
-    {
-        test_child_stdin(
-            |mut result_write| {
-                let mut child_running = true;
-                while child_running {
-                    tokio::runtime::Builder::new_multi_thread()
-                        .enable_all()
-                        .build()
-                        .unwrap()
-                        .block_on(async {
-                            'task: loop {
-                                println!("child: creating stdin");
-                                let mut stdin = mk_stdin();
-
-                                println!("child: checking that stdin is not ready");
-                                assert!(
-                                    tokio::time::timeout(
-                                        std::time::Duration::from_millis(100),
-                                        stdin.ready()
-                                    )
-                                    .await
-                                    .is_err(),
-                                    "stdin available too soon"
-                                );
-
-                                writeln!(&mut result_write, "start").unwrap();
-
-                                println!("child: started");
-
-                                let mut buffer = String::new();
-                                loop {
-                                    println!("child: waiting for stdin to be ready");
-                                    stdin.ready().await;
-
-                                    println!("child: reading input");
-                                    // We can't effectively test for the case where stdin was closed, so panic if it is...
-                                    let bytes = stdin.read(1024).unwrap();
-
-                                    println!("child got: {:?}", bytes);
-
-                                    buffer.push_str(std::str::from_utf8(bytes.as_ref()).unwrap());
-                                    if let Some((line, rest)) = buffer.split_once('\n') {
-                                        if line == "all done" {
-                                            writeln!(&mut result_write, "done").unwrap();
-                                            println!("child: exiting...");
-                                            child_running = false;
-                                            break 'task;
-                                        } else if line == "restart_runtime" {
-                                            writeln!(&mut result_write, "restarting").unwrap();
-                                            println!("child: restarting runtime...");
-                                            break 'task;
-                                        } else if line == "restart_task" {
-                                            writeln!(&mut result_write, "restarting").unwrap();
-                                            println!("child: restarting task...");
-                                            continue 'task;
-                                        } else {
-                                            writeln!(&mut result_write, "{}", line).unwrap();
-                                        }
-
-                                        buffer = rest.to_owned();
-                                    }
-                                }
-                            }
-                        });
-                    println!("runtime exited");
-                }
-                println!("child exited");
-            },
-            |mut stdin_write, mut result_read| {
-                let mut line = String::new();
-                result_read.read_line(&mut line).unwrap();
-                assert_eq!(line, "start\n");
-
-                for i in 0..5 {
-                    let message = format!("some bytes {}\n", i);
-                    stdin_write.write_all(message.as_bytes()).unwrap();
-                    line.clear();
-                    result_read.read_line(&mut line).unwrap();
-                    assert_eq!(line, message);
-                }
-
-                writeln!(&mut stdin_write, "restart_task").unwrap();
-                line.clear();
-                result_read.read_line(&mut line).unwrap();
-                assert_eq!(line, "restarting\n");
-                line.clear();
-
-                result_read.read_line(&mut line).unwrap();
-                assert_eq!(line, "start\n");
-
-                for i in 0..10 {
-                    let message = format!("more bytes {}\n", i);
-                    stdin_write.write_all(message.as_bytes()).unwrap();
-                    line.clear();
-                    result_read.read_line(&mut line).unwrap();
-                    assert_eq!(line, message);
-                }
-
-                writeln!(&mut stdin_write, "restart_runtime").unwrap();
-                line.clear();
-                result_read.read_line(&mut line).unwrap();
-                assert_eq!(line, "restarting\n");
-                line.clear();
-
-                result_read.read_line(&mut line).unwrap();
-                assert_eq!(line, "start\n");
-
-                for i in 0..17 {
-                    let message = format!("even more bytes {}\n", i);
-                    stdin_write.write_all(message.as_bytes()).unwrap();
-                    line.clear();
-                    result_read.read_line(&mut line).unwrap();
-                    assert_eq!(line, message);
-                }
-
-                writeln!(&mut stdin_write, "all done").unwrap();
-
-                line.clear();
-                result_read.read_line(&mut line).unwrap();
-                assert_eq!(line, "done\n");
-            },
-        )
-    }
-
-    // This test doesn't work under qemu because of the use of fork in the test helper.
-    #[test]
-    #[cfg_attr(not(target_arch = "x86_64"), ignore)]
-    fn test_worker_thread_stdin() {
-        test_stdin_by_forking(super::worker_thread_stdin::stdin);
-    }
-}


Test patch:
diff --git a/crates/wasi/tests/process_stdin.rs b/crates/wasi/tests/process_stdin.rs
new file mode 100644
index 000000000..5835335f3
--- /dev/null
+++ b/crates/wasi/tests/process_stdin.rs
@@ -0,0 +1,165 @@
+use std::io::{BufRead, Write};
+use std::process::Command;
+use wasmtime_wasi::preview2::{HostInputStream, Subscribe};
+
+const VAR_NAME: &str = "__CHILD_PROCESS";
+
+fn main() {
+    if cfg!(miri) {
+        return;
+    }
+    // Skip this tests if it looks like we're in a cross-compiled situation and
+    // we're emulating this test for a different platform. In that scenario
+    // emulators (like QEMU) tend to not report signals the same way and such.
+    if std::env::vars()
+        .filter(|(k, _v)| k.starts_with("CARGO_TARGET") && k.ends_with("RUNNER"))
+        .count()
+        > 0
+    {
+        return;
+    }
+
+    match std::env::var(VAR_NAME) {
+        Ok(_) => child_process(),
+        Err(_) => parent_process(),
+    }
+
+    fn child_process() {
+        let mut result_write = std::io::stderr();
+        let mut child_running = true;
+        while child_running {
+            tokio::runtime::Builder::new_multi_thread()
+                .enable_all()
+                .build()
+                .unwrap()
+                .block_on(async {
+                    'task: loop {
+                        println!("child: creating stdin");
+                        let mut stdin = wasmtime_wasi::preview2::stdin();
+
+                        println!("child: checking that stdin is not ready");
+                        assert!(
+                            tokio::time::timeout(
+                                std::time::Duration::from_millis(100),
+                                stdin.ready()
+                            )
+                            .await
+                            .is_err(),
+                            "stdin available too soon"
+                        );
+
+                        writeln!(&mut result_write, "start").unwrap();
+
+                        println!("child: started");
+
+                        let mut buffer = String::new();
+                        loop {
+                            println!("child: waiting for stdin to be ready");
+                            stdin.ready().await;
+
+                            println!("child: reading input");
+                            // We can't effectively test for the case where stdin was closed, so panic if it is...
+                            let bytes = stdin.read(1024).unwrap();
+
+                            println!("child got: {:?}", bytes);
+
+                            buffer.push_str(std::str::from_utf8(bytes.as_ref()).unwrap());
+                            if let Some((line, rest)) = buffer.split_once('\n') {
+                                if line == "all done" {
+                                    writeln!(&mut result_write, "done").unwrap();
+                                    println!("child: exiting...");
+                                    child_running = false;
+                                    break 'task;
+                                } else if line == "restart_runtime" {
+                                    writeln!(&mut result_write, "restarting").unwrap();
+                                    println!("child: restarting runtime...");
+                                    break 'task;
+                                } else if line == "restart_task" {
+                                    writeln!(&mut result_write, "restarting").unwrap();
+                                    println!("child: restarting task...");
+                                    continue 'task;
+                                } else {
+                                    writeln!(&mut result_write, "{}", line).unwrap();
+                                }
+
+                                buffer = rest.to_owned();
+                            }
+                        }
+                    }
+                });
+            println!("child: runtime exited");
+        }
+        println!("child: exiting");
+    }
+}
+
+fn parent_process() {
+    let me = std::env::current_exe().unwrap();
+    let mut cmd = Command::new(me);
+    cmd.env(VAR_NAME, "1");
+    cmd.stdin(std::process::Stdio::piped());
+
+    if std::env::args().any(|arg| arg == "--nocapture") {
+        cmd.stdout(std::process::Stdio::inherit());
+    } else {
+        cmd.stdout(std::process::Stdio::null());
+    }
+
+    cmd.stderr(std::process::Stdio::piped());
+    let mut child = cmd.spawn().unwrap();
+
+    let mut stdin_write = child.stdin.take().unwrap();
+    let mut result_read = std::io::BufReader::new(child.stderr.take().unwrap());
+
+    let mut line = String::new();
+    result_read.read_line(&mut line).unwrap();
+    assert_eq!(line, "start\n");
+
+    for i in 0..5 {
+        let message = format!("some bytes {}\n", i);
+        stdin_write.write_all(message.as_bytes()).unwrap();
+        line.clear();
+        result_read.read_line(&mut line).unwrap();
+        assert_eq!(line, message);
+    }
+
+    writeln!(&mut stdin_write, "restart_task").unwrap();
+    line.clear();
+    result_read.read_line(&mut line).unwrap();
+    assert_eq!(line, "restarting\n");
+    line.clear();
+
+    result_read.read_line(&mut line).unwrap();
+    assert_eq!(line, "start\n");
+
+    for i in 0..10 {
+        let message = format!("more bytes {}\n", i);
+        stdin_write.write_all(message.as_bytes()).unwrap();
+        line.clear();
+        result_read.read_line(&mut line).unwrap();
+        assert_eq!(line, message);
+    }
+
+    writeln!(&mut stdin_write, "restart_runtime").unwrap();
+    line.clear();
+    result_read.read_line(&mut line).unwrap();
+    assert_eq!(line, "restarting\n");
+    line.clear();
+
+    result_read.read_line(&mut line).unwrap();
+    assert_eq!(line, "start\n");
+
+    for i in 0..17 {
+        let message = format!("even more bytes {}\n", i);
+        stdin_write.write_all(message.as_bytes()).unwrap();
+        line.clear();
+        result_read.read_line(&mut line).unwrap();
+        assert_eq!(line, message);
+    }
+
+    writeln!(&mut stdin_write, "all done").unwrap();
+
+    line.clear();
+    result_read.read_line(&mut line).unwrap();
+    assert_eq!(line, "done\n");
+}