summaryrefslogtreecommitdiffstats
path: root/src/bin
diff options
context:
space:
mode:
Diffstat (limited to 'src/bin')
-rw-r--r--src/bin/client/connection.rs5
-rw-r--r--src/bin/client/repl.rs123
-rw-r--r--src/bin/client/tls.rs58
-rw-r--r--src/bin/server/handler.rs55
-rw-r--r--src/bin/server/tls.rs9
5 files changed, 211 insertions, 39 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
diff --git a/src/bin/server/handler.rs b/src/bin/server/handler.rs
index 86b09cb..dffae69 100644
--- a/src/bin/server/handler.rs
+++ b/src/bin/server/handler.rs
@@ -1,6 +1,11 @@
1use std::time::Duration;
2
1use crate::tls::TlsStream; 3use crate::tls::TlsStream;
2use zagan_bet::server::core::{ConnectionState, ServerCore}; 4use zagan_bet::server::core::{ConnectionState, ServerCore};
3 5
6/// How long the connection waits for a request before advancing timed games.
7const POLL_INTERVAL: Duration = Duration::from_millis(50);
8
4pub fn handle_client_tls(mut stream: TlsStream) { 9pub fn handle_client_tls(mut stream: TlsStream) {
5 let client_addr = match stream.peer_addr() { 10 let client_addr = match stream.peer_addr() {
6 Ok(addr) => addr.to_string(), 11 Ok(addr) => addr.to_string(),
@@ -19,34 +24,54 @@ pub fn handle_client_tls(mut stream: TlsStream) {
19 } 24 }
20 }; 25 };
21 26
27 if let Err(error) = stream.set_read_timeout(Some(POLL_INTERVAL)) {
28 eprintln!("Could not set read timeout: {error}");
29 return;
30 }
31
22 let mut state = ConnectionState::default(); 32 let mut state = ConnectionState::default();
23 println!("[TLS Client connected] {client_addr}"); 33 println!("[TLS Client connected] {client_addr}");
24 34
25 loop { 35 'connection: loop {
26 let mut line = String::new(); 36 let mut line = String::new();
27 match stream.read_line(&mut line) { 37 match stream.read_line(&mut line) {
28 Ok(0) => break, 38 Ok(0) => break,
29 Ok(_) => {} 39 Ok(_) => {
40 let line = line.trim();
41 if !line.is_empty() {
42 let user = state.user().unwrap_or("guest").to_string();
43 println!("[{user} @ {client_addr}] Received: {line}");
44 let response = core.handle_message(&mut state, line);
45 if let Err(error) = stream.write_line(&response) {
46 eprintln!("Write error: {error}");
47 break;
48 }
49 let user = state.user().unwrap_or("guest");
50 println!("[{user} @ {client_addr}] Sent: {response}");
51 }
52 }
53 Err(error)
54 if matches!(
55 error.kind(),
56 std::io::ErrorKind::WouldBlock | std::io::ErrorKind::TimedOut
57 ) =>
58 {
59 // No request yet: fall through and let timed games advance.
60 }
30 Err(error) => { 61 Err(error) => {
31 eprintln!("Read error: {error}"); 62 eprintln!("Read error: {error}");
32 break; 63 break;
33 } 64 }
34 } 65 }
35 66
36 let line = line.trim(); 67 for push in core.poll(&state) {
37 if line.is_empty() { 68 if let Err(error) = stream.write_line(&push) {
38 continue; 69 eprintln!("Write error: {error}");
39 } 70 break 'connection;
40 71 }
41 let user = state.user().unwrap_or("guest").to_string(); 72 let user = state.user().unwrap_or("guest");
42 println!("[{user} @ {client_addr}] Received: {line}"); 73 println!("[{user} @ {client_addr}] Pushed: {push}");
43 let response = core.handle_message(&mut state, line);
44 if let Err(error) = stream.write_line(&response) {
45 eprintln!("Write error: {error}");
46 break;
47 } 74 }
48 let user = state.user().unwrap_or("guest");
49 println!("[{user} @ {client_addr}] Sent: {response}");
50 } 75 }
51 76
52 core.disconnect(&state); 77 core.disconnect(&state);
diff --git a/src/bin/server/tls.rs b/src/bin/server/tls.rs
index 292ebf7..5d84589 100644
--- a/src/bin/server/tls.rs
+++ b/src/bin/server/tls.rs
@@ -77,6 +77,15 @@ impl TlsStream {
77 pub fn peer_addr(&self) -> std::io::Result<std::net::SocketAddr> { 77 pub fn peer_addr(&self) -> std::io::Result<std::net::SocketAddr> {
78 self.stream.get_ref().peer_addr() 78 self.stream.get_ref().peer_addr()
79 } 79 }
80
81 /// Read timeout for the socket. The connection loop uses a short one so it
82 /// can advance crash rounds while no request is in flight.
83 pub fn set_read_timeout(
84 &self,
85 duration: Option<std::time::Duration>,
86 ) -> std::io::Result<()> {
87 self.stream.get_ref().set_read_timeout(duration)
88 }
80} 89}
81 90
82impl Read for TlsStream { 91impl Read for TlsStream {