Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
arlosi opened this issue Sep 22, 2021 · 4 comments · Fixed by #89433
Closed

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

arlosi opened this issue Sep 22, 2021 · 4 comments · Fixed by #89433
Labels
C-bug Category: This is a bug. O-windows Operating system: Windows T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.

Comments

@arlosi
Copy link
Contributor

arlosi commented Sep 22, 2021

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
@arlosi arlosi added the C-bug Category: This is a bug. label Sep 22, 2021
@wesleywiser wesleywiser added O-windows Operating system: Windows T-libs-api Relevant to the library API team, which will review and decide on the PR/issue. labels Sep 22, 2021
@jyn514
Copy link
Member

jyn514 commented Sep 23, 2021

Is this specific to reading stdin? Lots of things about this seem strange (launching a separate thread?!) but I would be really surprised to hear this only happens for stdin and nothing else. If so adding the check in read_line won't be sufficient.

@arlosi
Copy link
Contributor Author

arlosi commented Sep 23, 2021

I think the issue is specific to stdin. I don't think you could (for example) embed a ctrl+c in a file and have it terminate the reading process.

Windows does seem to launch a thread to handle ctrl-c. From https://docs.microsoft.com/en-us/windows/console/ctrl-c-and-ctrl-break-signals

The system creates a new thread in each client process to handle the event

My issue description might not have been quite clear. read_line doesn't directly call ReadConsoleW. There are several layers in between. The read_16s function (which calls ReadConsoleW) has a Windows-specific workaround for making ctrl-z work, so I thought it may be the right place to work around this ctrl-c issue too.

// Configure the `pInputControl` parameter to not only return on `\r\n` but also Ctrl-Z, the

@jyn514
Copy link
Member

jyn514 commented Sep 23, 2021

I think the issue is specific to stdin. I don't think you could (for example) embed a ctrl+c in a file and have it terminate the reading process.

Hmm, what happens if the user presses Ctrl+C while the program is reading a file? Won't the program execution continue for a few milliseconds with only a partial read?

@arlosi
Copy link
Contributor Author

arlosi commented Sep 24, 2021

Hmm, what happens if the user presses Ctrl+C while the program is reading a file? Won't the program execution continue for a few milliseconds with only a partial read?

Yes, but there's not really noticeable to the user. The file reading wouldn't be waiting for user input in order to begin. The few milliseconds delay in that case would be the same as any other latency (keyboard, os, etc.).

The problem with reading from stdin is that the call is blocked waiting for user input and pressing ctrl-c unblocks it on Windows, while on other platforms, it continues to block.

The behavior becomes even more clear if you install a ctrl-c handler (such as with the ctrlc crate), then do a read_line.

    ctrlc::set_handler(|| println!("Caught ctrl-c")).unwrap();
    let bytes = std::io::stdin().lock().read_line(&mut line)?;

On other platforms, the program will just print "Caught ctrl-c" for each press, and continue blocking. While on Windows execution will also continue past the read_line call.

matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Oct 14, 2021
Fix ctrl-c causing reads of stdin to return empty on Windows.

Pressing ctrl+c (or ctrl+break) on Windows caused a blocking read of stdin to unblock and return empty, unlike other platforms which continue to block.

On ctrl-c, `ReadConsoleW` will return success, but also set `LastError` to `ERROR_OPERATION_ABORTED`.

This change detects this case, and re-tries the call to `ReadConsoleW`.

Fixes rust-lang#89177. See issue for further details.

Tested on Windows 7 and Windows 10 with both MSVC and GNU toolchains
matthiaskrgr added a commit to matthiaskrgr/rust that referenced this issue Oct 14, 2021
Fix ctrl-c causing reads of stdin to return empty on Windows.

Pressing ctrl+c (or ctrl+break) on Windows caused a blocking read of stdin to unblock and return empty, unlike other platforms which continue to block.

On ctrl-c, `ReadConsoleW` will return success, but also set `LastError` to `ERROR_OPERATION_ABORTED`.

This change detects this case, and re-tries the call to `ReadConsoleW`.

Fixes rust-lang#89177. See issue for further details.

Tested on Windows 7 and Windows 10 with both MSVC and GNU toolchains
@bors bors closed this as completed in 273e522 Oct 14, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C-bug Category: This is a bug. O-windows Operating system: Windows T-libs-api Relevant to the library API team, which will review and decide on the PR/issue.
Projects
None yet
Development

Successfully merging a pull request may close this issue.

3 participants