Skip to content

Execution continues after stdin read_line when ctrl-c is pressed on Windows #89177

Closed
@arlosi

Description

@arlosi

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

c::ReadConsoleW(

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.

| c::ERROR_OPERATION_ABORTED

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    C-bugCategory: This is a bug.O-windowsOperating system: WindowsT-libs-apiRelevant to the library API team, which will review and decide on the PR/issue.

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions