Description
When blocking on read_line
from stdin
, if I press Ctrl-C, program execution continues past the blocking read_line
call for a short period before the program is terminated on Windows.
I tried this code:
fn main() -> std::io::Result<()> {
use std::io::BufRead;
println!("Press Ctrl-C");
let mut line = String::new();
let bytes = std::io::stdin().lock().read_line(&mut line)?;
println!("This should not print if Ctrl-C was pressed. Read {} bytes", bytes);
Ok(())
}
I expected to see this happen: Program should exit after pressing Ctrl-C without printing the second message.
Instead, this happened: Program prints This should not print if Ctrl-C was pressed. Read 0 bytes
after pressing Ctrl-C.
This only occurs on Windows. *nix platforms exit without printing the second message.
Since program execution continues after ctrl-c, unexpected results occur. For example, running cargo login
, then pressing ctrl-c
instead of entering a token, cargo
will overwrite the saved token with ""
on Windows.
*nix platforms continue blocking in read_line
when ctrl-c is pressed. Including if a handler for ctrl-c is set up.
Why it's happening
read_line
calls the Windows ReadConsoleW
API. That API returns success with an empty buffer for Ctrl-C (and Ctrl-Break). read_line
then also returns Ok
. In parallel, Windows uses a separate thread that raises an exception for the ctrl-c event that terminates the process. This leads to a short window of time where program execution continues after ctrl-c is pressed.
Potential solutions
Even though ReadConsoleW
returns success, it also sets LastError to ERROR_OPERATION_ABORTED
for this case, so we can detect this case.
The Rust standard library calls ReadConsoleW
here
rust/library/std/src/sys/windows/stdio.rs
Line 295 in cfff31b
Re-try the call immediately
We could re-try the call to ReadConsoleW
immediately for this specific case by adding a check after the ReadConsoleW
call.
loop {
cvt(unsafe {
c::SetLastError(0);
c::ReadConsoleW(
handle,
buf.as_mut_ptr() as c::LPVOID,
buf.len() as u32,
&mut amount,
&mut input_control as c::PCONSOLE_READCONSOLE_CONTROL,
)
})?;
// ReadConsoleW returns success with ERROR_OPERATION_ABORTED for Ctrl-C or Ctrl-Break.
// Explicitly check for that case here and try again.
if amount == 0 {
if unsafe { c::GetLastError() } == c::ERROR_OPERATION_ABORTED {
continue;
}
}
break;
}
Return an error
Alternately, after the call to ReadConsoleW
, we could add a check to detect this error and propagate it:
if amount == 0 {
let err = crate::io::Error::last_os_error();
if err.raw_os_error() == Some(c::ERROR_OPERATION_ABORTED as i32) {
return Err(err);
}
}
Returning an error still differs from *nix, in that it raises an error instead of continuing to block.
Return an error then retry
We could return the error as described above, and change the mapping of ERROR_OPERATION_ABORTED
from ErrorKind::TimedOut
to ErrorKind::Interrupted
.
rust/library/std/src/sys/windows/mod.rs
Line 79 in cfff31b
ErrorKind::Interrupted
would then re-tried the BufRead
infrastructure.
Do nothing
Maybe this is expected behavior and we want a difference between Windows and *nix here.
Meta
Occurs in both stable and nightly.
rustc --version --verbose
:
rustc 1.57.0-nightly (497ee321a 2021-09-09)
binary: rustc
commit-hash: 497ee321af3b8496eaccd7af7b437f18bab81abf
commit-date: 2021-09-09
host: x86_64-pc-windows-msvc
release: 1.57.0-nightly
LLVM version: 13.0.0