summaryrefslogtreecommitdiffstats
path: root/src/bin/client
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--src/bin/client/connection.rs5
-rw-r--r--src/bin/client/repl.rs123
-rw-r--r--src/bin/client/tls.rs58
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
155impl Session { 157impl 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