@tishlang/tish-lsp 3.10.4 → 3.10.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -491,6 +491,17 @@ fn handle_ws_upgrade(mut req: Request<hyper::body::Incoming>) -> Response<Full<B
491
491
  use tokio_tungstenite::tungstenite::handshake::derive_accept_key;
492
492
  use tokio_tungstenite::tungstenite::protocol::Role;
493
493
 
494
+ // Enforce the same connection cap as the tish:ws listener BEFORE the 101. This path runs
495
+ // pre-auth — the VM handler never sees the request — so without the check an unauthenticated
496
+ // client could register unbounded permanent connections (tasks + fd + buffers each) on any
497
+ // serve() app built with http-hyper + ws (#707).
498
+ if !crate::ws::has_ws_capacity() {
499
+ return simple_error_response(
500
+ StatusCode::SERVICE_UNAVAILABLE,
501
+ "websocket connection limit reached",
502
+ );
503
+ }
504
+
494
505
  let accept = match req.headers().get(hyper::header::SEC_WEBSOCKET_KEY) {
495
506
  Some(k) => derive_accept_key(k.as_bytes()),
496
507
  None => return simple_error_response(StatusCode::BAD_REQUEST, "missing Sec-WebSocket-Key"),
@@ -501,8 +512,13 @@ fn handle_ws_upgrade(mut req: Request<hyper::body::Incoming>) -> Response<Full<B
501
512
  match on_upgrade.await {
502
513
  Ok(upgraded) => {
503
514
  let io = TokioIo::new(upgraded);
504
- let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(io, Role::Server, None)
505
- .await;
515
+ // Shared config: 16 KiB buffers instead of tungstenite's eager 128 KiB default.
516
+ let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(
517
+ io,
518
+ Role::Server,
519
+ Some(crate::ws::ws_config()),
520
+ )
521
+ .await;
506
522
  let id = crate::ws::register_ws_stream(ws);
507
523
  crate::ws::enqueue_upgraded_conn(id);
508
524
  }
@@ -1773,6 +1773,10 @@ pub use tty::{
1773
1773
  tty_enter_alt_screen, tty_is_tty, tty_leave_alt_screen, tty_read, tty_read_line, tty_set_raw_mode, tty_size,
1774
1774
  };
1775
1775
 
1776
+ /// Shared reader-thread buffer + UTF-8 drain for the byte-stream modules (pty / process / net).
1777
+ #[cfg(any(feature = "pty", feature = "process", feature = "net"))]
1778
+ mod stream_buf;
1779
+
1776
1780
  #[cfg(feature = "pty")]
1777
1781
  pub mod pty;
1778
1782
  #[cfg(feature = "pty")]
@@ -18,24 +18,28 @@
18
18
  //! - `probe(host, port, timeoutMs?) -> bool` (can we connect?)
19
19
  //! - `sleep(ms) -> null` (blocking backoff for the poll loops above)
20
20
  //!
21
- //! Each connection has a reader thread filling a UTF-8-boundary-drained buffer, exactly like
22
- //! pty.rs / process_spawn.rs. Global OnceLock<Mutex<HashMap>> registries; errors surface as
21
+ //! Each connection has a reader thread filling a bounded, UTF-8-boundary-drained buffer shared
22
+ //! with pty.rs / process_spawn.rs (see `stream_buf.rs` for the cap/backpressure and
23
+ //! invalid-byte semantics). Global OnceLock<Mutex<HashMap>> registries; errors surface as
23
24
  //! null/false.
25
+ //!
26
+ //! Connection lifecycle: `close(id)` shuts the socket down (`Shutdown::Both`) so the reader
27
+ //! thread — which holds a dup of the fd — unblocks and exits, freeing thread + fd + buffer
28
+ //! together, and the peer sees a FIN. Entries whose peer disconnected and whose buffer was
29
+ //! read to EOF (`read` returned `null`) are swept when the next connection registers, so
30
+ //! read-to-EOF-and-forget cannot strand CLOSE_WAIT fds; a swept id keeps returning `null`
31
+ //! from `read`, exactly as before.
24
32
 
25
33
  use std::collections::HashMap;
26
- use std::io::{ErrorKind, Read, Write};
34
+ use std::io::{ErrorKind, Write};
27
35
  use std::net::{TcpListener, TcpStream, ToSocketAddrs};
28
36
  use std::sync::atomic::{AtomicU64, Ordering};
29
- use std::sync::{Arc, Condvar, Mutex, OnceLock};
37
+ use std::sync::{Mutex, OnceLock};
30
38
  use std::time::{Duration, Instant};
31
39
 
32
40
  use tishlang_core::{ObjectMap, Value};
33
41
 
34
- struct StreamBuf {
35
- data: Vec<u8>,
36
- eof: bool,
37
- }
38
- type SharedBuf = Arc<(Mutex<StreamBuf>, Condvar)>;
42
+ use crate::stream_buf::{drain_stream, is_drained, new_buf, retire_buf, spawn_reader, SharedBuf};
39
43
 
40
44
  struct Conn {
41
45
  writer: Mutex<TcpStream>,
@@ -82,95 +86,20 @@ fn arg_timeout(args: &[Value], i: usize) -> u64 {
82
86
  }
83
87
  }
84
88
 
85
- fn new_buf() -> SharedBuf {
86
- Arc::new((
87
- Mutex::new(StreamBuf {
88
- data: Vec::new(),
89
- eof: false,
90
- }),
91
- Condvar::new(),
92
- ))
93
- }
94
-
95
- // Reader thread: block on the socket, append bytes, wake waiters; mark EOF on 0/err.
96
- fn spawn_reader(mut stream: TcpStream, buf: SharedBuf) {
97
- std::thread::spawn(move || {
98
- let mut tmp = [0u8; 8192];
99
- loop {
100
- match stream.read(&mut tmp) {
101
- Ok(0) => {
102
- let (lock, cv) = &*buf;
103
- if let Ok(mut b) = lock.lock() {
104
- b.eof = true;
105
- }
106
- cv.notify_all();
107
- break;
108
- }
109
- Ok(n) => {
110
- let (lock, cv) = &*buf;
111
- if let Ok(mut b) = lock.lock() {
112
- b.data.extend_from_slice(&tmp[..n]);
113
- }
114
- cv.notify_all();
115
- }
116
- Err(_) => {
117
- let (lock, cv) = &*buf;
118
- if let Ok(mut b) = lock.lock() {
119
- b.eof = true;
120
- }
121
- cv.notify_all();
122
- break;
123
- }
124
- }
125
- }
126
- });
127
- }
128
-
129
- fn drain_stream(buf: &SharedBuf, timeout_ms: u64) -> Value {
130
- let (lock, cv) = &**buf;
131
- let mut b = match lock.lock() {
132
- Ok(b) => b,
133
- Err(_) => return Value::Null,
134
- };
135
- if b.data.is_empty() && !b.eof && timeout_ms > 0 {
136
- let res = cv.wait_timeout_while(b, Duration::from_millis(timeout_ms), |b| {
137
- b.data.is_empty() && !b.eof
138
- });
139
- b = match res {
140
- Ok((g, _)) => g,
141
- Err(e) => e.into_inner().0,
142
- };
143
- }
144
- if b.data.is_empty() {
145
- if b.eof {
146
- return Value::Null;
147
- }
148
- return Value::String("".into());
149
- }
150
- let valid = match std::str::from_utf8(&b.data) {
151
- Ok(_) => b.data.len(),
152
- Err(e) => e.valid_up_to(),
153
- };
154
- if valid == 0 {
155
- if b.eof {
156
- let s = String::from_utf8_lossy(&b.data).into_owned();
157
- b.data.clear();
158
- return Value::String(s.into());
159
- }
160
- return Value::String("".into());
161
- }
162
- let out: Vec<u8> = b.data.drain(..valid).collect();
163
- Value::String(String::from_utf8(out).unwrap_or_default().into())
164
- }
165
-
166
- // Register a connected stream (reader thread + writer clone) and return its id.
89
+ // Register a connected stream (reader thread + writer clone) and return its id. Also sweeps
90
+ // out entries whose peer disconnected and whose buffer was fully read (`read` already
91
+ // returned `null` for them), so read-to-EOF-and-forget cannot strand CLOSE_WAIT fds — the
92
+ // sweep never touches a conn with undrained data or a live reader, so an id that has not yet
93
+ // returned `null` keeps its full read contract.
167
94
  fn register_conn(stream: TcpStream) -> Option<u64> {
168
95
  let reader = stream.try_clone().ok()?;
169
96
  let _ = reader.set_nonblocking(false);
170
97
  let buf = new_buf();
171
98
  spawn_reader(reader, buf.clone());
172
99
  let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
173
- conns().lock().ok()?.insert(
100
+ let mut g = conns().lock().ok()?;
101
+ g.retain(|_, c| !is_drained(&c.buf));
102
+ g.insert(
174
103
  id,
175
104
  Conn {
176
105
  writer: Mutex::new(stream),
@@ -262,9 +191,12 @@ pub fn net_write(args: &[Value]) -> Value {
262
191
  }
263
192
  }
264
193
 
265
- /// `close(id)` → drop a connection (its reader thread ends on EOF) or a listener. Conn and
266
- /// listener ids share one counter, so an id names at most one of the two; closing a listener frees
267
- /// its port — the reserve-ephemeral-then-release pattern a TCP DAP `spawn` uses so the child can
194
+ /// `close(id)` → close a connection or a listener. For a connection this actually shuts the
195
+ /// socket down (`Shutdown::Both`, the `http.rs` idiom): the reader thread — which holds a dup
196
+ /// of the fd — unblocks out of its `read()`, exits, and frees thread + fd + buffer together,
197
+ /// and the peer sees a FIN instead of a silently half-alive socket. Conn and listener ids
198
+ /// share one counter, so an id names at most one of the two; closing a listener frees its
199
+ /// port — the reserve-ephemeral-then-release pattern a TCP DAP `spawn` uses so the child can
268
200
  /// bind it. Returns whether anything live was removed.
269
201
  pub fn net_close(args: &[Value]) -> Value {
270
202
  let id = match arg_u64(args, 0) {
@@ -272,10 +204,15 @@ pub fn net_close(args: &[Value]) -> Value {
272
204
  None => return Value::Bool(false),
273
205
  };
274
206
  let removed_conn = match conns().lock() {
275
- Ok(mut g) => g.remove(&id).is_some(),
207
+ Ok(mut g) => g.remove(&id),
276
208
  Err(_) => return Value::Bool(false),
277
209
  };
278
- if removed_conn {
210
+ if let Some(c) = removed_conn {
211
+ if let Ok(w) = c.writer.lock() {
212
+ let _ = w.shutdown(std::net::Shutdown::Both);
213
+ }
214
+ // Wake a reader parked at the buffer cap (shutdown only unblocks a read-blocked one).
215
+ retire_buf(&c.buf);
279
216
  return Value::Bool(true);
280
217
  }
281
218
  let removed_listener = match listeners().lock() {
@@ -468,6 +405,97 @@ mod tests {
468
405
  assert!(matches!(net_close(&[Value::Number(server)]), Value::Bool(true)));
469
406
  }
470
407
 
408
+ fn listen_pair() -> (f64, f64, f64) {
409
+ let l = net_listen(&[Value::Number(0.0)]);
410
+ let (lid, port) = match &l {
411
+ Value::Object(m) => {
412
+ let b = m.borrow();
413
+ let id = match b.strings.get("id") {
414
+ Some(Value::Number(i)) => *i,
415
+ _ => panic!("no id"),
416
+ };
417
+ let port = match b.strings.get("port") {
418
+ Some(Value::Number(p)) => *p,
419
+ _ => panic!("no port"),
420
+ };
421
+ (id, port)
422
+ }
423
+ other => panic!("listen failed: {:?}", other),
424
+ };
425
+ let client = match net_connect(&[Value::String("127.0.0.1".into()), Value::Number(port)]) {
426
+ Value::Number(n) => n,
427
+ other => panic!("connect failed: {:?}", other),
428
+ };
429
+ let server = match net_accept(&[Value::Number(lid), Value::Number(1000.0)]) {
430
+ Value::Number(n) => n,
431
+ other => panic!("accept failed: {:?}", other),
432
+ };
433
+ (lid, client, server)
434
+ }
435
+
436
+ #[test]
437
+ fn close_shuts_down_the_socket_so_the_peer_sees_eof() {
438
+ // Pre-fix, net_close only dropped the writer fd: the reader thread's dup kept the
439
+ // socket alive, no FIN was ever sent, and the peer's reads blocked forever.
440
+ let (lid, client, server) = listen_pair();
441
+ assert!(matches!(
442
+ net_close(&[Value::Number(server)]),
443
+ Value::Bool(true)
444
+ ));
445
+ let mut saw_eof = false;
446
+ for _ in 0..100 {
447
+ if let Value::Null = net_read(&[Value::Number(client), Value::Number(100.0)]) {
448
+ saw_eof = true;
449
+ break;
450
+ }
451
+ }
452
+ assert!(saw_eof, "peer never saw EOF after close — no FIN was sent");
453
+ let _ = net_close(&[Value::Number(client)]);
454
+ let _ = net_close(&[Value::Number(lid)]);
455
+ }
456
+
457
+ #[test]
458
+ fn conn_read_to_eof_is_swept_on_next_register() {
459
+ // The natural "read until null, forget the id" pattern: pre-fix the Conn entry (and
460
+ // its CLOSE_WAIT fd) survived forever.
461
+ let (lid, client, server) = listen_pair();
462
+ assert!(matches!(
463
+ net_write(&[Value::Number(server), Value::String("bye".into())]),
464
+ Value::Bool(true)
465
+ ));
466
+ assert!(matches!(
467
+ net_close(&[Value::Number(server)]),
468
+ Value::Bool(true)
469
+ ));
470
+ // Drain the client to EOF (null) — and never close it.
471
+ let mut got = String::new();
472
+ let mut saw_eof = false;
473
+ for _ in 0..100 {
474
+ match net_read(&[Value::Number(client), Value::Number(100.0)]) {
475
+ Value::String(s) => got.push_str(&s),
476
+ Value::Null => {
477
+ saw_eof = true;
478
+ break;
479
+ }
480
+ _ => {}
481
+ }
482
+ }
483
+ assert!(saw_eof, "client never reached EOF");
484
+ assert!(got.contains("bye"), "payload lost before EOF: {:?}", got);
485
+ // Registering the next connection sweeps the drained entry.
486
+ let (lid2, client2, server2) = listen_pair();
487
+ assert!(
488
+ !conns().lock().unwrap().contains_key(&(client as u64)),
489
+ "drained-at-EOF conn was not swept"
490
+ );
491
+ // The read-to-EOF-then-read-again contract: the swept id still reads as null.
492
+ assert!(matches!(net_read(&[Value::Number(client)]), Value::Null));
493
+ let _ = net_close(&[Value::Number(client2)]);
494
+ let _ = net_close(&[Value::Number(server2)]);
495
+ let _ = net_close(&[Value::Number(lid)]);
496
+ let _ = net_close(&[Value::Number(lid2)]);
497
+ }
498
+
471
499
  #[test]
472
500
  fn probe_and_unknown() {
473
501
  // Nothing listening on this port -> probe false.