diff options
Diffstat (limited to 'src/bin/client')
| -rw-r--r-- | src/bin/client/connection.rs | 5 | ||||
| -rw-r--r-- | src/bin/client/repl.rs | 123 | ||||
| -rw-r--r-- | src/bin/client/tls.rs | 58 |
3 files changed, 162 insertions, 24 deletions
diff --git a/src/bin/client/connection.rs b/src/bin/client/connection.rs index 6459de5..363a2a9 100644 --- a/src/bin/client/connection.rs +++ b/src/bin/client/connection.rs | |||
| @@ -125,6 +125,11 @@ impl TlsStream { | |||
| 125 | self.inner.write_line(msg) | 125 | self.inner.write_line(msg) |
| 126 | } | 126 | } |
| 127 | 127 | ||
| 128 | /// One line if the socket already has one, without blocking. | ||
| 129 | pub fn read_available_line(&mut self) -> std::io::Result<Option<String>> { | ||
| 130 | self.inner.read_available_line() | ||
| 131 | } | ||
| 132 | |||
| 128 | pub fn set_read_timeout( | 133 | pub fn set_read_timeout( |
| 129 | &self, | 134 | &self, |
| 130 | duration: Option<std::time::Duration>, | 135 | duration: Option<std::time::Duration>, |
diff --git a/src/bin/client/repl.rs b/src/bin/client/repl.rs index f85af47..7fa462a 100644 --- a/src/bin/client/repl.rs +++ b/src/bin/client/repl.rs | |||
| @@ -102,29 +102,76 @@ impl ClientState { | |||
| 102 | self.apply_prompt(); | 102 | self.apply_prompt(); |
| 103 | 103 | ||
| 104 | loop { | 104 | loop { |
| 105 | let event = read().unwrap(); | 105 | let mut quit = false; |
| 106 | match event { | 106 | |
| 107 | Event::Key(key) => { | 107 | if crossterm::event::poll(std::time::Duration::from_millis(25)).unwrap_or(false) { |
| 108 | if key.kind != KeyEventKind::Press { | 108 | match read().unwrap() { |
| 109 | continue; | 109 | Event::Key(key) => { |
| 110 | } | 110 | if key.kind == KeyEventKind::Press { |
| 111 | let Some(key) = map_key(key) else { | 111 | if let Some(key) = map_key(key) { |
| 112 | continue; | 112 | let effects = self.core.handle_key(key); |
| 113 | }; | 113 | quit = self.apply_effects(effects); |
| 114 | let effects = self.core.handle_key(key); | 114 | self.apply_prompt(); |
| 115 | if self.apply_effects(effects) { | 115 | } |
| 116 | break; | 116 | } |
| 117 | } | 117 | } |
| 118 | self.apply_prompt(); | 118 | Event::Resize(_, _) => self.apply_prompt(), |
| 119 | _ => {} | ||
| 120 | } | ||
| 121 | } | ||
| 122 | |||
| 123 | if !quit && self.core.expects_push() { | ||
| 124 | quit = self.pump_async(); | ||
| 125 | } | ||
| 126 | |||
| 127 | if !quit { | ||
| 128 | let effects = self.core.tick(); | ||
| 129 | if !effects.is_empty() { | ||
| 130 | quit = self.apply_effects(effects); | ||
| 119 | } | 131 | } |
| 120 | Event::Resize(_, _) => self.apply_prompt(), | 132 | } |
| 121 | _ => {} | 133 | |
| 134 | if quit { | ||
| 135 | break; | ||
| 122 | } | 136 | } |
| 123 | } | 137 | } |
| 124 | 138 | ||
| 125 | restore_terminal(&self.cleanup_done); | 139 | restore_terminal(&self.cleanup_done); |
| 126 | } | 140 | } |
| 127 | 141 | ||
| 142 | /// Read any server-initiated lines that are already waiting and apply them. | ||
| 143 | /// Returns true when an effect asked the program to quit. | ||
| 144 | fn pump_async(&mut self) -> bool { | ||
| 145 | loop { | ||
| 146 | let Some(mut stream) = self.stream.take() else { | ||
| 147 | return false; | ||
| 148 | }; | ||
| 149 | let line = match stream.read_available_line() { | ||
| 150 | Ok(line) => { | ||
| 151 | self.stream = Some(stream); | ||
| 152 | line | ||
| 153 | } | ||
| 154 | Err(_) => { | ||
| 155 | self.core.set_connected(false); | ||
| 156 | self.stream = None; | ||
| 157 | return false; | ||
| 158 | } | ||
| 159 | }; | ||
| 160 | |||
| 161 | let Some(line) = line else { | ||
| 162 | return false; | ||
| 163 | }; | ||
| 164 | |||
| 165 | let effects = match self.core.handle_async_line(&line) { | ||
| 166 | Some(effects) => effects, | ||
| 167 | None => self.core.handle_reply(&line), | ||
| 168 | }; | ||
| 169 | if self.apply_effects(effects) { | ||
| 170 | return true; | ||
| 171 | } | ||
| 172 | } | ||
| 173 | } | ||
| 174 | |||
| 128 | fn apply_effects(&mut self, effects: Vec<Effect>) -> bool { | 175 | fn apply_effects(&mut self, effects: Vec<Effect>) -> bool { |
| 129 | for effect in effects { | 176 | for effect in effects { |
| 130 | match effect { | 177 | match effect { |
| @@ -250,6 +297,9 @@ impl ClientState { | |||
| 250 | let roulette = lines | 297 | let roulette = lines |
| 251 | .first() | 298 | .first() |
| 252 | .is_some_and(|first| first.contains("ROULETTE")); | 299 | .is_some_and(|first| first.contains("ROULETTE")); |
| 300 | let crash = lines | ||
| 301 | .first() | ||
| 302 | .is_some_and(|first| first.contains("CRASH")); | ||
| 253 | let rendered: Vec<String> = lines | 303 | let rendered: Vec<String> = lines |
| 254 | .iter() | 304 | .iter() |
| 255 | .map(|line| { | 305 | .map(|line| { |
| @@ -263,6 +313,18 @@ impl ClientState { | |||
| 263 | } else { | 313 | } else { |
| 264 | style_roulette_numbers(line) | 314 | style_roulette_numbers(line) |
| 265 | } | 315 | } |
| 316 | } else if crash { | ||
| 317 | if line.contains("CRASHED") { | ||
| 318 | line.as_str().bold().red().to_string() | ||
| 319 | } else if line.starts_with("Cashed out") { | ||
| 320 | line.as_str().bold().green().to_string() | ||
| 321 | } else if line.starts_with("Multiplier:") { | ||
| 322 | line.as_str().yellow().to_string() | ||
| 323 | } else if line.starts_with("ERROR:") { | ||
| 324 | line.as_str().red().to_string() | ||
| 325 | } else { | ||
| 326 | line.clone() | ||
| 327 | } | ||
| 266 | } else if line.contains('{') { | 328 | } else if line.contains('{') { |
| 267 | style_winning_cells(line) | 329 | style_winning_cells(line) |
| 268 | } else if line.contains("WIN!") || line.starts_with("Won ") { | 330 | } else if line.contains("WIN!") || line.starts_with("Won ") { |
| @@ -361,22 +423,37 @@ impl ClientState { | |||
| 361 | return "ERROR Not connected".to_string(); | 423 | return "ERROR Not connected".to_string(); |
| 362 | }; | 424 | }; |
| 363 | 425 | ||
| 426 | // A crash event can be pushed while a request is in flight. Route those | ||
| 427 | // lines as they arrive and keep waiting for the actual reply. | ||
| 428 | let mut pushed: Vec<String> = Vec::new(); | ||
| 364 | let result = (|| -> std::io::Result<String> { | 429 | let result = (|| -> std::io::Result<String> { |
| 365 | stream.write_line(line)?; | 430 | stream.write_line(line)?; |
| 366 | let mut response = String::new(); | 431 | loop { |
| 367 | let read = stream.read_line(&mut response)?; | 432 | let mut response = String::new(); |
| 368 | if read == 0 { | 433 | let read = stream.read_line(&mut response)?; |
| 369 | return Err(std::io::Error::new( | 434 | if read == 0 { |
| 370 | std::io::ErrorKind::UnexpectedEof, | 435 | return Err(std::io::Error::new( |
| 371 | "connection closed", | 436 | std::io::ErrorKind::UnexpectedEof, |
| 372 | )); | 437 | "connection closed", |
| 438 | )); | ||
| 439 | } | ||
| 440 | let response = response.trim_end().to_string(); | ||
| 441 | if self.core.is_async(&response) { | ||
| 442 | pushed.push(response); | ||
| 443 | continue; | ||
| 444 | } | ||
| 445 | return Ok(response); | ||
| 373 | } | 446 | } |
| 374 | Ok(response) | ||
| 375 | })(); | 447 | })(); |
| 376 | 448 | ||
| 377 | match result { | 449 | match result { |
| 378 | Ok(response) => { | 450 | Ok(response) => { |
| 379 | self.stream = Some(stream); | 451 | self.stream = Some(stream); |
| 452 | for line in pushed { | ||
| 453 | if let Some(effects) = self.core.handle_async_line(&line) { | ||
| 454 | self.apply_effects(effects); | ||
| 455 | } | ||
| 456 | } | ||
| 380 | response | 457 | response |
| 381 | } | 458 | } |
| 382 | Err(error) => { | 459 | Err(error) => { |
diff --git a/src/bin/client/tls.rs b/src/bin/client/tls.rs index 8c052f7..b46f52b 100644 --- a/src/bin/client/tls.rs +++ b/src/bin/client/tls.rs | |||
| @@ -64,6 +64,7 @@ impl TlsConnector { | |||
| 64 | sock, | 64 | sock, |
| 65 | tls: conn, | 65 | tls: conn, |
| 66 | lines: LineBuffer::new(), | 66 | lines: LineBuffer::new(), |
| 67 | read_timeout: Some(std::time::Duration::from_secs(10)), | ||
| 67 | })), | 68 | })), |
| 68 | }) | 69 | }) |
| 69 | } | 70 | } |
| @@ -150,6 +151,7 @@ struct Session { | |||
| 150 | sock: TcpStream, | 151 | sock: TcpStream, |
| 151 | tls: rustls::ClientConnection, | 152 | tls: rustls::ClientConnection, |
| 152 | lines: LineBuffer, | 153 | lines: LineBuffer, |
| 154 | read_timeout: Option<std::time::Duration>, | ||
| 153 | } | 155 | } |
| 154 | 156 | ||
| 155 | impl Session { | 157 | impl Session { |
| @@ -202,7 +204,61 @@ impl TlsStream { | |||
| 202 | &self, | 204 | &self, |
| 203 | duration: Option<std::time::Duration>, | 205 | duration: Option<std::time::Duration>, |
| 204 | ) -> std::io::Result<()> { | 206 | ) -> std::io::Result<()> { |
| 205 | self.session.lock().sock.set_read_timeout(duration) | 207 | let mut s = self.session.lock(); |
| 208 | s.read_timeout = duration; | ||
| 209 | s.sock.set_read_timeout(duration) | ||
| 210 | } | ||
| 211 | |||
| 212 | /// Return one complete line if the socket already has one, without | ||
| 213 | /// blocking. Used to pick up server pushes between requests. | ||
| 214 | pub fn read_available_line(&mut self) -> std::io::Result<Option<String>> { | ||
| 215 | let mut s = self.session.lock(); | ||
| 216 | if let Some(line) = s.lines.next_line() { | ||
| 217 | return Ok(Some(line)); | ||
| 218 | } | ||
| 219 | |||
| 220 | // `set_read_timeout(Some(0))` is rejected by std, so poll with the | ||
| 221 | // shortest timeout the socket accepts instead of a nonblocking mode. | ||
| 222 | s.sock | ||
| 223 | .set_read_timeout(Some(std::time::Duration::from_millis(1)))?; | ||
| 224 | let drained = (|| -> std::io::Result<()> { | ||
| 225 | loop { | ||
| 226 | let mut buf = [0u8; 4096]; | ||
| 227 | match s.tls.reader().read(&mut buf) { | ||
| 228 | Ok(0) => return Ok(()), | ||
| 229 | Ok(n) => s.lines.push(&buf[..n]), | ||
| 230 | // The reader only drains plaintext rustls has already | ||
| 231 | // decrypted; `drive` moves bytes off the socket. | ||
| 232 | Err(error) if error.kind() == std::io::ErrorKind::WouldBlock => { | ||
| 233 | match s.drive() { | ||
| 234 | Ok(()) => continue, | ||
| 235 | Err(error) | ||
| 236 | if matches!( | ||
| 237 | error.kind(), | ||
| 238 | std::io::ErrorKind::WouldBlock | ||
| 239 | | std::io::ErrorKind::TimedOut | ||
| 240 | ) => | ||
| 241 | { | ||
| 242 | return Ok(()); | ||
| 243 | } | ||
| 244 | Err(error) => return Err(error), | ||
| 245 | } | ||
| 246 | } | ||
| 247 | Err(error) | ||
| 248 | if matches!( | ||
| 249 | error.kind(), | ||
| 250 | std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut | ||
| 251 | ) => | ||
| 252 | { | ||
| 253 | return Ok(()); | ||
| 254 | } | ||
| 255 | Err(error) => return Err(error), | ||
| 256 | } | ||
| 257 | } | ||
| 258 | })(); | ||
| 259 | s.sock.set_read_timeout(s.read_timeout)?; | ||
| 260 | drained?; | ||
| 261 | Ok(s.lines.next_line()) | ||
| 206 | } | 262 | } |
| 207 | } | 263 | } |
| 208 | 264 | ||
