rustwebsocketcommand-line-interfacetungstenite

Blocking websocket interrupted by resizing the terminal (os error 4)


Some friends and I are currently working on an online Pong game on website. The game works great but I wanted to make a Rust CLI application allowing the user to play versus online player on the website.

For that, I use websocket from tungstenite crate. I also use ncurses crate to get the user input and term_size crate to know the size of the terminal to adapt how the game will be rendered.

My connection with the websocket server work but when I resized the terminal to test if the "responsive" works, I get an os error 4, which is Interrupted system call. After some research, I found that when resizing a terminal, the OS will send a SIGWINCH signal to the process that gonna interrupt every blocking system calls. I know that tungstenite in a sync crate (so my websocket is blocking) and it's why I choose it instead of tokio (it's my first Rust application, I didn't want to complicated it with threads and async code while I'm not familiar with the language).

I don't really know how to disable this signal (I don't think it's a good idea to do it that way) or how to handle it in a way that won't interrupt the blocking system calls. I'm not sure either if disabling SIGWINCH won't "break" the term_size crate.

This is a part of my code:

initscr();
raw();
keypad(stdscr(), true);
noecho();
timeout(0);
loop {
    match getch() {
        27 => { // ESC
            endwin();
            break;
        },
        ch => {
            let ch = match char::from_u32(ch as u32) {
                Some(ch) => {
                    ch
                },
                None => ' '
            };
            if ch != ' ' { // Send user input to the server
                socket.write_message(Message::Text(r#"{"message":"{ch}"}"#.to_string().replace("{ch}", &ch.to_string())));
            }
        }
    }
    match socket.read_message() { // THIS IS WHERE THE ERROR OCCURED
        Ok(msg) => match msg {
            Message::Text(msg) => {
                // render the game
            },
            _ => {}
        },
        Err(err) => { ... }
    }
}

I tried this but it doesn't change anything:

use std::os::unix::io::RawFd;
use nix::sys::signal::{self, Signal, SigHandler, SigAction, SigSet, SaFlags};
use nix::unistd::Pid;

fn main() {
    let sig_action = SigAction::new(
        SigHandler::SigIgn,
        SaFlags::empty(),
        SigSet::empty(),
    );
    
    unsafe {
        let _ = signal::sigaction(Signal::SIGWINCH, &sig_action).expect("Failed to ignore SIGWINCH");
    }
    
    // The rest of my code are here (like authentification, selection to create or join a game, etc...)

Solution

  • In general, gracefully dealing with EINTR (the error you're getting) is part of writing code on Unix. In theory, it can occur for most system calls, including most forms of I/O.

    It is possible on some systems to use sigaction with the SA_RESTART flag to restart some system calls; which those are is dependent on the OS. However, if you're using a terminal library, it's not your code which will be trapping SIGWINCH, but the terminal library, so your code still needs to gracefully handle EINTR.

    The good news is that if you got EINTR, then no data was sent or received; if there had been some data sent or received, then you would have gotten a short read or write indicating that partial amount. So, if you do get EINTR, you just need to retry in a loop until you get a success or a different error. So it will look something like this:

    loop {
        match getch() {
            27 => { // ESC
                endwin();
                break;
            },
            ch => {
                let ch = match char::from_u32(ch as u32) {
                    Some(ch) => {
                        ch
                    },
                    None => ' '
                };
                if ch != ' ' { // Send user input to the server
                    socket.write_message(Message::Text(r#"{"message":"{ch}"}"#.to_string().replace("{ch}", &ch.to_string())));
                }
            }
        }
        loop {
            match socket.read_message() {
                Ok(msg) => match msg {
                    Message::Text(msg) => {
                        // render the game
                    },
                    _ => {}
                },
                Err(err) if err.kind() == ErrorKind::Interrupted => continue,
                Err(err) => { ... }
            }
            break;
        }
    }