Code change:
It looks like the use of overlapped I/O on stdio ports may cause some problems in Windows. When I try to shell out to using on the stderr port, the command runs successfully but hangs at the end. pointed me to this commit as the likely culprit: Here's the smallest test case I could figure out how to create: I wanted to try to come up with a minimal Node script to shell out to, as opposed to . Unfortunately, I'm not good enough at debugging npm and Node to figure out exactly what they're doing that causes the hang.
Ok I've tested this and it's a stable-to-stable regression that happened between 1.8 and 1.9. I believe the 1.9 release is indeed the first one with , the likely change which is a culprit. Tagging appropriately as such.
It looks like Rust is passing overlapped-mode pipe handles to the child process. The function returns two overlapped HANDLES. I'm not sure the overlapped mode can be changed after the fact -- maybe would work -- but I don't see it used anywhere, and I'd wonder about its use on the server handle. If the child inherits overlapped-mode handles, it would need to do async I/O to stdin/stdout/stderr. By convention, it does sync I/O using lpOverlapped==NULL, but that has undefined behavior if the handles are overlapped pipes. FWIW: the server and client ends of a named pipe aren't quite symmetric, but I'm guessing it doesn't matter whether the child process receives a server pipe or a client pipe? The interesting thing is that writing to a broken pipe returns , but reading from one returns . Apparently this is true for both ends. (I apparently wrote for this back in June'16.) I'd probably pass the client end to the child process, because it doesn't make sense that a program can call DisconnectNamedPipe on its stdio handles. (DisconnectNamedPipe doesn't work on client handles.) I made a Rust + C++ test case demonstrating how passing overlapped handles to the child breaks:
Fascinating! Thanks for the links That perfectly explains I think what's going on here. I think I can see the fix here, so I'll work on that pronto.
Yeah, I think the fix is easy -- use non-overlapped handles for the child but overlapped handles for the parent.

Related discussion:
text: <ins> // Copyright 2017 The Rust Project Developers. See the COPYRIGHT // file at the top-level directory of this distribution and at // http://rust-lang.org/COPYRIGHT. // // Licensed under the Apache License, Version 2.0 <LICENSE-APACHE or // http://www.apache.org/licenses/LICENSE-2.0> or the MIT license // <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your // option. This file may not be copied, modified, or distributed // except according to those terms. use std::env; use std::io::prelude::*; use std::process::Command; use std::thread; const THREADS: usize = 20; const WRITES: usize = 100; const WRITE_SIZE: usize = 1024 * 32; fn main() { let args = env::args().collect::<Vec<_>>(); if args.len() == 1 { parent(); } else { child(); } } fn parent() { let me = env::current_exe().unwrap(); let mut cmd = Command::new(me); cmd.arg("run-the-test"); let output = cmd.output().unwrap(); assert!(output.status.success()); assert_eq!(output.stderr.len(), 0); assert_eq!(output.stdout.len(), WRITES * THREADS * WRITE_SIZE); for byte in output.stdout.iter() { assert_eq!(*byte, b'a'); } } fn child() { let threads = (0..THREADS).map(|_| { thread::spawn(|| { let buf = [b'a'; WRITE_SIZE]; for _ in 0..WRITES { write_all(&buf); } }) }).collect::<Vec<_>>(); for thread in threads { thread.join().unwrap(); } } #[cfg(unix)] fn write_all(buf: &[u8]) { use std::fs::File; use std::mem; use std::os::unix::prelude::*; let mut file = unsafe { File::from_raw_fd(1) }; let res = file.write_all(buf); mem::forget(file); res.unwrap(); } #[cfg(windows)] fn write_all(buf: &[u8]) { use std::fs::File; use std::mem; use std::os::windows::raw::*; use std::os::windows::prelude::*; const STD_OUTPUT_HANDLE: u32 = (-11i32) as u32; extern "system" { fn GetStdHandle(handle: u32) -> HANDLE; } let mut file = unsafe { let handle = GetStdHandle(STD_OUTPUT_HANDLE); assert!(!handle.is_null()); File::from_raw_handle(handle) }; let res = file.write_all(buf); mem::forget(file); res.unwrap(); } </ins>