Repository: crossterm-rs/crossterm (language: rust)

Issue:
use-dev-tty causes panic
## Description
The new feature ```use-dev-tty``` sometimes causes this panic:
```shell
panicked at 'polling tty: Poll(Os { code: 4, kind: Interrupted, message: "Interrupted system call" })',
XXX/.cargo/registry/src/.../crossterm-0.26.0/src/event/source/unix/tty.rs:135:28  ...
```
https://github.com/crossterm-rs/crossterm/blob/383d9a7827d56990b8d80b23348a2ce18b947948/src/event/source/unix/tty.rs#L135

## How to reproduce
I have not found a minimal example, and my full code is not public. But I can reliably trigger it with the code below:

```toml
[dependencies]
crossterm = {version="0.26.0", features=["event-stream", "use-dev-tty"]}
futures = {version = "0.3"}
tokio = { version = "1", features = ["process", "macros", "rt-multi-thread"]}
```

```rust
use crossterm::event::EventStream;
use futures::{future::FutureExt, stream::StreamExt};
use std::process::Stdio;
use tokio;

#[tokio::main]
async fn main() {
    let mut reader = EventStream::new();

    loop {
        let event = reader.next().fuse();
        tokio::select! {
            maybe_event = event => {
                match maybe_event {
                    Some(Ok(event)) => println!("Event::{:?}\r", event),
                    Some(Err(e)) => println!("Error: {:?}\r", e),
                    None => break,
                }
            }
        };

        // Spawn enough tasks, and the event stream will panic.
        for _ in 0..100 {
            tokio::spawn(async {
                // I don't think the actual command matters.
                tokio::process::Command::new("pwd")
                    // null i/o is not needed, but I want to rule this out.
                    .stdin(Stdio::null())
                    .stdout(Stdio::null())
                    .stderr(Stdio::null())
                    .status()
                    .await
            });
        }
    }
}

```

Note that this won't panic without the tty feature. And the panic seems to only happen with this combination (task + command). I don't know enough about tokio to be of help here.


There's another problem: When the EventStream panics, it will do it silently (not counting the panic message). At that point it will stop sending events, and the main loop is just blocked forever. Shouldn't the event stream return a ```Result<Event>``` to handle cases where the EventSource is found to be dead? In any case, please avoid panics.


## Environment
- Ubuntu 22.10
- Rust 1.69.0--nightly
- Terminal doesn't matter

I've also tested this with the current crossterm on github, with the same results.


---
# Update: 
I've changed the code to be more like the official example, which first fuses the future. This still doesn't stop the loop from being blocked after panic.


Discussion / hints:
I did a bit of searching. Unfortunately I don't know enough about these topics. Please just ignore this if it turns out to be wrong.

It might be that EINTR isn't handled correctly (or at all).

Mio is returning a specific error, which is handled and ignored:
https://github.com/crossterm-rs/crossterm/blob/383d9a7827d56990b8d80b23348a2ce18b947948/src/event/source/unix/mio.rs#L83-L92

Here's the exact point where the panic occurs in the tty version. Ignoring that specific error will avoid the panic (obviously).
This is not a solution, just a way to find the source of the panic!!!
```diff
--- a/src/event/source/unix/tty.rs
+++ b/src/event/source/unix/tty.rs
@@ -132,7 +132,24 @@ impl EventSource for UnixInternalEventSource {
             }
             match poll(&mut fds, timeout.leftover()) {
                 Err(filedescriptor::Error::Io(e)) => return Err(e),
-                res => res.expect("polling tty"),
+                res => match res {
+                    Ok(_res) => {}
+                    Err(fd_err) => {
+                        match fd_err {
+                            filedescriptor::Error::Poll(err)
+                                if err.kind() == io::ErrorKind::Interrupted =>
+                            {
+                                continue
+                            }
+                            other_err => {
+                                // TODO:
+                                // This function already returns Result<T>.
+                                // So don't panic, but instead return an error!
+                                panic!("{:?}", other_err)
+                            }
+                        }
+                    }
+                },
             };
             if fds[0].revents & POLLIN != 0 {
                 loop {

```
cc @yyogo 
Thank you for the report! This is indeed due to not handling eintr correctly - I believe the correct solution is to just ignore it, since we already keep polling until we get an event or the timeout expired.

Fix patch:
diff --git a/src/event/source/unix/tty.rs b/src/event/source/unix/tty.rs
index b9eb43604..ca77e3d89 100644
--- a/src/event/source/unix/tty.rs
+++ b/src/event/source/unix/tty.rs
@@ -131,8 +131,20 @@ impl EventSource for UnixInternalEventSource {
                 return Ok(Some(event));
             }
             match poll(&mut fds, timeout.leftover()) {
-                Err(filedescriptor::Error::Io(e)) => return Err(e),
-                res => res.expect("polling tty"),
+                Err(filedescriptor::Error::Poll(e)) | Err(filedescriptor::Error::Io(e)) => {
+                    match e.kind() {
+                        // retry on EINTR
+                        io::ErrorKind::Interrupted => continue,
+                        _ => return Err(e),
+                    }
+                }
+                Err(e) => {
+                    return Err(std::io::Error::new(
+                        std::io::ErrorKind::Other,
+                        format!("got unexpected error while polling: {:?}", e),
+                    ))
+                }
+                Ok(_) => (),
             };
             if fds[0].revents & POLLIN != 0 {
                 loop {


Test patch: