use std::fs::OpenOptions;
use std::io::{self, Write};
use std::os::windows::io::{AsRawHandle, FromRawHandle, OwnedHandle};
use std::process::Command;
use std::sync::{Arc, Mutex};
use windows_sys::Win32::Foundation::HANDLE;
use windows_sys::Win32::System::Console::{
AllocConsole, FreeConsole, GetConsoleMode, SetConsoleMode, SetStdHandle,
DISABLE_NEWLINE_AUTO_RETURN, ENABLE_VIRTUAL_TERMINAL_PROCESSING, STD_OUTPUT_HANDLE,
};
use windows_sys::Win32::System::Pipes::CreatePipe;
use super::{output::AttachStdout, RawTerminal};
const CONSOLE_MODE_CHILD_ENV: &str = "RMUX_INTERNAL_CONSOLE_MODE_OWNER_TEST_CHILD";
const CONSOLE_MODE_TEST_NAME: &str =
"attach::console_mode_ownership_tests::managed_attach_stdout_has_single_console_mode_owner";
#[test]
fn managed_attach_stdout_has_single_console_mode_owner() {
if std::env::var_os(CONSOLE_MODE_CHILD_ENV).is_none() {
let status = Command::new(std::env::current_exe().expect("current test executable"))
.args(["--exact", CONSOLE_MODE_TEST_NAME, "--nocapture"])
.env(CONSOLE_MODE_CHILD_ENV, "1")
.status()
.expect("spawn isolated console-mode test child");
assert!(status.success(), "console-mode test child failed: {status}");
return;
}
unsafe {
let _ = FreeConsole();
assert_ne!(
AllocConsole(),
0,
"AllocConsole failed: {}",
io::Error::last_os_error()
);
}
let conout = OpenOptions::new()
.read(true)
.write(true)
.open("CONOUT$")
.expect("open isolated console output");
let output_handle = conout.as_raw_handle() as HANDLE;
let set_stdout = unsafe {
SetStdHandle(STD_OUTPUT_HANDLE, output_handle)
};
assert_ne!(
set_stdout,
0,
"SetStdHandle failed: {}",
io::Error::last_os_error()
);
let original = read_console_mode(output_handle);
let initial = original & !ENABLE_VIRTUAL_TERMINAL_PROCESSING & !DISABLE_NEWLINE_AUTO_RETURN;
write_console_mode(output_handle, initial);
let terminal = RawTerminal::enter().expect("enter managed raw terminal");
let stdout = AttachStdout::for_managed_terminal(io::sink());
drop(stdout);
drop(terminal);
assert_eq!(
read_console_mode(output_handle),
initial,
"writer-first teardown must restore the caller's exact output mode"
);
let terminal = RawTerminal::enter().expect("re-enter managed raw terminal");
let stdout = AttachStdout::for_managed_terminal(io::sink());
drop(terminal);
drop(stdout);
assert_eq!(
read_console_mode(output_handle),
initial,
"terminal-first teardown must not let the writer restore a stale raw mode"
);
exercise_redirected_stdout(output_handle, initial);
let _ = unsafe {
SetStdHandle(STD_OUTPUT_HANDLE, output_handle)
};
write_console_mode(output_handle, original);
}
fn exercise_redirected_stdout(output_handle: HANDLE, initial_mode: u32) {
let mut pipe_read: HANDLE = std::ptr::null_mut();
let mut pipe_write: HANDLE = std::ptr::null_mut();
let pipe_created = unsafe {
CreatePipe(&mut pipe_read, &mut pipe_write, std::ptr::null_mut(), 0)
};
assert_ne!(
pipe_created,
0,
"CreatePipe failed: {}",
io::Error::last_os_error()
);
let _pipe_read = unsafe {
OwnedHandle::from_raw_handle(pipe_read)
};
let pipe_write = unsafe {
OwnedHandle::from_raw_handle(pipe_write)
};
let redirect_stdout = unsafe {
SetStdHandle(STD_OUTPUT_HANDLE, pipe_write.as_raw_handle() as HANDLE)
};
assert_ne!(
redirect_stdout,
0,
"redirect SetStdHandle failed: {}",
io::Error::last_os_error()
);
write_console_mode(output_handle, initial_mode);
let terminal = RawTerminal::enter().expect("enter with redirected stdout");
let fallback = RecordingWriter::default();
let observed = fallback.clone();
let mut stdout = AttachStdout::for_managed_terminal(fallback);
let payload = b"redirected-attach-output";
stdout
.write_all(payload)
.expect("write redirected attach output");
stdout.flush().expect("flush redirected attach output");
assert_eq!(
observed.snapshot(),
payload,
"a valid non-console stdout must receive attach output through the caller's writer"
);
drop(stdout);
drop(terminal);
assert_eq!(
read_console_mode(output_handle),
initial_mode,
"redirected stdout handling must not leave the outer console mode changed"
);
}
#[derive(Clone, Default)]
struct RecordingWriter {
bytes: Arc<Mutex<Vec<u8>>>,
}
impl RecordingWriter {
fn snapshot(&self) -> Vec<u8> {
self.bytes.lock().expect("recording writer mutex").clone()
}
}
impl Write for RecordingWriter {
fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
self.bytes
.lock()
.map_err(|_| io::Error::other("recording writer mutex poisoned"))?
.extend_from_slice(bytes);
Ok(bytes.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
fn read_console_mode(handle: HANDLE) -> u32 {
let mut mode = 0_u32;
let ok = unsafe {
GetConsoleMode(handle, &mut mode)
};
assert_ne!(
ok,
0,
"GetConsoleMode failed: {}",
io::Error::last_os_error()
);
mode
}
fn write_console_mode(handle: HANDLE, mode: u32) {
let ok = unsafe {
SetConsoleMode(handle, mode)
};
assert_ne!(
ok,
0,
"SetConsoleMode failed: {}",
io::Error::last_os_error()
);
}