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...)
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;
}
}