@tishlang/tish-format 3.10.3 → 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.
- package/bin/tish-format +0 -0
- package/crates/tish/src/main.rs +65 -25
- package/crates/tish_builtins/src/globals.rs +3 -3
- package/crates/tish_builtins/src/symbol.rs +86 -4
- package/crates/tish_bytecode/src/compiler.rs +22 -6
- package/crates/tish_bytecode/tests/fundecl_slot_binding.rs +115 -0
- package/crates/tish_core/src/shape.rs +55 -2
- package/crates/tish_core/src/value.rs +276 -8
- package/crates/tish_eval/src/timers.rs +66 -2
- package/crates/tish_ffi/src/lib.rs +74 -12
- package/crates/tish_ffi/tests/loader.rs +35 -0
- package/crates/tish_fmt/Cargo.toml +1 -1
- package/crates/tish_lsp/src/main.rs +91 -2
- package/crates/tish_runtime/src/fs_ext.rs +28 -19
- package/crates/tish_runtime/src/http_hyper.rs +18 -2
- package/crates/tish_runtime/src/lib.rs +4 -0
- package/crates/tish_runtime/src/net.rs +125 -97
- package/crates/tish_runtime/src/process_spawn.rs +326 -113
- package/crates/tish_runtime/src/pty.rs +161 -107
- package/crates/tish_runtime/src/stream_buf.rs +419 -0
- package/crates/tish_runtime/src/ws.rs +431 -52
- package/crates/tish_vm/src/jit.rs +473 -67
- package/crates/tish_vm/src/lib.rs +6 -0
- package/crates/tish_vm/src/vm.rs +3 -1
- package/crates/tish_vm/tests/fundecl_slot_no_frame_cycle.rs +152 -0
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-fmt +0 -0
- package/platform/darwin-x64/tish-fmt +0 -0
- package/platform/linux-arm64/tish-fmt +0 -0
- package/platform/linux-x64/tish-fmt +0 -0
- package/platform/win32-x64/tish-fmt.exe +0 -0
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
//! WebSocket module for Tish (tish:ws).
|
|
2
2
|
//!
|
|
3
3
|
//! Node.js `ws`-compatible API:
|
|
4
|
-
//! - **Server**: `Server({ port })` — has `clients` (array
|
|
5
|
-
//!
|
|
4
|
+
//! - **Server**: `Server({ port })` — has `clients` (array of LIVE conns; closed ones are pruned
|
|
5
|
+
//! lazily), `on('connection', fn)`, `listen()`, `acceptTimeout(server, ms)`, `close()`
|
|
6
|
+
//! - **Connection**: `send(data)` (returns `false` when the conn is gone OR its bounded outbound
|
|
7
|
+
//! queue is full — see `TISH_WS_SEND_BUF_BYTES`), `close()`, `readyState` (1=OPEN),
|
|
8
|
+
//! `receive()` / `receiveTimeout(ms)`
|
|
6
9
|
//! - **Broadcast** (Node pattern): `server.clients.forEach(ws => ws.send(data))` or iterate room conns and `wsSend(ws, data)` (same as `ws.send(data)`)
|
|
7
10
|
|
|
8
11
|
use tishlang_core::VmRef;
|
|
9
12
|
use std::collections::HashMap;
|
|
10
|
-
use std::sync::atomic::{AtomicU32, Ordering};
|
|
13
|
+
use std::sync::atomic::{AtomicU32, AtomicUsize, Ordering};
|
|
11
14
|
use std::sync::mpsc;
|
|
12
15
|
use std::sync::{Arc, Mutex};
|
|
13
16
|
use std::time::{Duration, Instant};
|
|
@@ -80,32 +83,74 @@ fn ws_debug() -> bool {
|
|
|
80
83
|
}
|
|
81
84
|
|
|
82
85
|
struct ConnState {
|
|
83
|
-
send_tx: tokio_mpsc::
|
|
86
|
+
send_tx: tokio_mpsc::Sender<String>,
|
|
84
87
|
recv_rx: mpsc::Receiver<String>,
|
|
88
|
+
/// Bytes accepted by `conn_send` and not yet written to the socket. Shared with the write task,
|
|
89
|
+
/// which decrements as each write completes; `conn_send` refuses (returns false) once
|
|
90
|
+
/// `ws_send_buf_bytes()` is exceeded so a peer that stops reading can't grow the queue forever.
|
|
91
|
+
queued_bytes: Arc<AtomicUsize>,
|
|
92
|
+
/// Abort handle for the read pump task. `unregister` aborts it so an explicit `close()` frees
|
|
93
|
+
/// the task, socket fd and read buffer instead of leaving the task parked on `read.next()`
|
|
94
|
+
/// until the REMOTE peer disconnects.
|
|
95
|
+
read_abort: Option<tokio::task::AbortHandle>,
|
|
85
96
|
#[allow(dead_code)]
|
|
86
97
|
open: bool,
|
|
87
98
|
}
|
|
88
99
|
|
|
100
|
+
/// A listening `tish:ws` server: the queue of accepted connection ids plus the accept loop's
|
|
101
|
+
/// shutdown signal (see `server_close`).
|
|
102
|
+
struct ServerState {
|
|
103
|
+
conn_rx: mpsc::Receiver<u32>,
|
|
104
|
+
shutdown_tx: Option<tokio::sync::oneshot::Sender<()>>,
|
|
105
|
+
}
|
|
106
|
+
|
|
89
107
|
lazy_static! {
|
|
90
108
|
static ref CONNS: Mutex<HashMap<u32, ConnState>> = Mutex::new(HashMap::new());
|
|
91
|
-
static ref SERVER_RECV: Mutex<HashMap<u32,
|
|
109
|
+
static ref SERVER_RECV: Mutex<HashMap<u32, ServerState>> = Mutex::new(HashMap::new());
|
|
92
110
|
}
|
|
93
111
|
|
|
94
|
-
fn register(
|
|
112
|
+
fn register(
|
|
113
|
+
send_tx: tokio_mpsc::Sender<String>,
|
|
114
|
+
recv_rx: mpsc::Receiver<String>,
|
|
115
|
+
queued_bytes: Arc<AtomicUsize>,
|
|
116
|
+
) -> u32 {
|
|
95
117
|
let id = next_conn_id();
|
|
96
118
|
CONNS.lock().unwrap().insert(
|
|
97
119
|
id,
|
|
98
120
|
ConnState {
|
|
99
121
|
send_tx,
|
|
100
122
|
recv_rx,
|
|
123
|
+
queued_bytes,
|
|
124
|
+
read_abort: None,
|
|
101
125
|
open: true,
|
|
102
126
|
},
|
|
103
127
|
);
|
|
104
128
|
id
|
|
105
129
|
}
|
|
106
130
|
|
|
131
|
+
/// Store the read pump's abort handle on an already-registered connection so `unregister` can
|
|
132
|
+
/// cancel it. If the connection has vanished in the meantime (instant disconnect), abort right away.
|
|
133
|
+
fn attach_read_task(id: u32, task: &tokio::task::JoinHandle<()>) {
|
|
134
|
+
let handle = task.abort_handle();
|
|
135
|
+
if let Ok(mut guard) = CONNS.lock() {
|
|
136
|
+
match guard.get_mut(&id) {
|
|
137
|
+
Some(state) => state.read_abort = Some(handle),
|
|
138
|
+
None => handle.abort(),
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
107
143
|
fn unregister(id: u32) {
|
|
108
|
-
CONNS.lock().
|
|
144
|
+
let state = CONNS.lock().ok().and_then(|mut g| g.remove(&id));
|
|
145
|
+
if let Some(state) = state {
|
|
146
|
+
// Cancel the read pump. Without this an explicit close() left the task parked on
|
|
147
|
+
// `read.next().await` forever, pinning the socket fd and the tungstenite read buffer until
|
|
148
|
+
// the remote peer disconnected. Dropping `state` also drops `send_tx`, which ends the write
|
|
149
|
+
// task — and the write task drives a Close frame + socket shutdown on its way out.
|
|
150
|
+
if let Some(handle) = state.read_abort {
|
|
151
|
+
handle.abort();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
109
154
|
}
|
|
110
155
|
|
|
111
156
|
/// Bridge an already-handshaked WebSocket stream into the `CONNS` registry — spawning the read and
|
|
@@ -115,34 +160,50 @@ pub(crate) fn register_ws_stream<S>(ws_stream: tokio_tungstenite::WebSocketStrea
|
|
|
115
160
|
where
|
|
116
161
|
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
|
117
162
|
{
|
|
118
|
-
let (send_tx, mut send_rx) = tokio_mpsc::
|
|
163
|
+
let (send_tx, mut send_rx) = tokio_mpsc::channel::<String>(SEND_QUEUE_MAX_MSGS);
|
|
119
164
|
let (recv_tx, recv_rx) = mpsc::sync_channel::<String>(64);
|
|
120
|
-
let
|
|
165
|
+
let queued_bytes = Arc::new(AtomicUsize::new(0));
|
|
166
|
+
let id = register(send_tx, recv_rx, Arc::clone(&queued_bytes));
|
|
121
167
|
let recv_tx_task = Arc::new(recv_tx);
|
|
122
168
|
let (mut write, mut read) = ws_stream.split();
|
|
123
|
-
tokio::spawn(async move {
|
|
169
|
+
let read_task = tokio::spawn(async move {
|
|
124
170
|
while let Some(Ok(msg)) = read.next().await {
|
|
125
171
|
match msg {
|
|
126
172
|
tokio_tungstenite::tungstenite::Message::Text(t) => {
|
|
127
|
-
|
|
173
|
+
// A dropped receiver means the connection was closed and nobody will ever read
|
|
174
|
+
// these frames — stop pumping instead of discarding them forever.
|
|
175
|
+
if recv_tx_task.send(t.to_string()).is_err() {
|
|
176
|
+
break;
|
|
177
|
+
}
|
|
128
178
|
}
|
|
129
179
|
// Binary frames are delivered too (utf8-lossy) — remote pty streams raw bytes and
|
|
130
180
|
// some servers frame as Binary; dropping them silently loses output. Ping/Pong/Close
|
|
131
181
|
// are handled by the stream (the loop ends when it yields None on close).
|
|
132
182
|
tokio_tungstenite::tungstenite::Message::Binary(b) => {
|
|
133
|
-
|
|
183
|
+
if recv_tx_task
|
|
184
|
+
.send(String::from_utf8_lossy(&b).into_owned())
|
|
185
|
+
.is_err()
|
|
186
|
+
{
|
|
187
|
+
break;
|
|
188
|
+
}
|
|
134
189
|
}
|
|
135
190
|
_ => {}
|
|
136
191
|
}
|
|
137
192
|
}
|
|
138
193
|
unregister(id);
|
|
139
194
|
});
|
|
195
|
+
attach_read_task(id, &read_task);
|
|
140
196
|
tokio::spawn(async move {
|
|
141
197
|
while let Some(text) = send_rx.recv().await {
|
|
198
|
+
let n = text.len();
|
|
142
199
|
let _ = write
|
|
143
200
|
.send(tokio_tungstenite::tungstenite::Message::Text(text.into()))
|
|
144
201
|
.await;
|
|
202
|
+
queued_bytes.fetch_sub(n, Ordering::AcqRel);
|
|
145
203
|
}
|
|
204
|
+
// Channel closed: the connection was unregistered (explicit close() or remote disconnect).
|
|
205
|
+
// Drive the closing handshake so the socket actually shuts down instead of leaking the fd.
|
|
206
|
+
let _ = write.close().await;
|
|
146
207
|
});
|
|
147
208
|
id
|
|
148
209
|
}
|
|
@@ -208,6 +269,51 @@ fn max_ws_connections() -> usize {
|
|
|
208
269
|
})
|
|
209
270
|
}
|
|
210
271
|
|
|
272
|
+
/// Can another connection register under `max_ws_connections()`? Shared accounting point for the
|
|
273
|
+
/// `tish:ws` listener AND the `serve()` HTTP→WS upgrade path (http_hyper), which previously
|
|
274
|
+
/// registered pre-auth connections with no cap at all.
|
|
275
|
+
pub(crate) fn has_ws_capacity() -> bool {
|
|
276
|
+
CONNS
|
|
277
|
+
.lock()
|
|
278
|
+
.map(|c| c.len() < max_ws_connections())
|
|
279
|
+
.unwrap_or(false)
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/// Slot count of the bounded per-connection send queue. The byte budget (`ws_send_buf_bytes`) is
|
|
283
|
+
/// the primary limit; this bounds per-message channel overhead when frames are tiny.
|
|
284
|
+
const SEND_QUEUE_MAX_MSGS: usize = 1024;
|
|
285
|
+
|
|
286
|
+
/// Per-connection outbound byte budget. `conn_send` refuses (returns false) once this many bytes
|
|
287
|
+
/// sit queued waiting on the socket, so a peer that stops reading gets backpressure instead of
|
|
288
|
+
/// unbounded queue growth. A single message is always admitted into an EMPTY queue even when it
|
|
289
|
+
/// exceeds the budget (so a legitimately large frame can still be sent); the queue is therefore
|
|
290
|
+
/// bounded by `budget + one message`. Override with `TISH_WS_SEND_BUF_BYTES`; default 4 MiB.
|
|
291
|
+
fn ws_send_buf_bytes() -> usize {
|
|
292
|
+
use std::sync::OnceLock;
|
|
293
|
+
static MAX: OnceLock<usize> = OnceLock::new();
|
|
294
|
+
*MAX.get_or_init(|| {
|
|
295
|
+
std::env::var("TISH_WS_SEND_BUF_BYTES")
|
|
296
|
+
.ok()
|
|
297
|
+
.and_then(|v| v.parse().ok())
|
|
298
|
+
.unwrap_or(4 * 1024 * 1024)
|
|
299
|
+
})
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/// Shared tungstenite config for every connection (server-accept, `serve()` upgrade, client):
|
|
303
|
+
/// 16 KiB read/write buffers instead of the 128 KiB defaults — the read buffer is allocated eagerly
|
|
304
|
+
/// per connection, so the defaults authorize ~1.3 GB across the 10k-conn cap before any traffic —
|
|
305
|
+
/// and a finite `max_write_buffer_size` so a failing socket can't grow the write buffer without
|
|
306
|
+
/// bound.
|
|
307
|
+
pub(crate) fn ws_config() -> tokio_tungstenite::tungstenite::protocol::WebSocketConfig {
|
|
308
|
+
tokio_tungstenite::tungstenite::protocol::WebSocketConfig::default()
|
|
309
|
+
.read_buffer_size(16 * 1024)
|
|
310
|
+
.write_buffer_size(16 * 1024)
|
|
311
|
+
.max_write_buffer_size(ws_send_buf_bytes().saturating_mul(2).max(1 << 20))
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
/// Queue `data` for the connection's write task. Returns `false` when the connection is gone OR
|
|
315
|
+
/// when the outbound queue is full (byte budget `ws_send_buf_bytes()` / `SEND_QUEUE_MAX_MSGS`
|
|
316
|
+
/// slots) — callers already treat `false` as "not delivered". Never blocks the caller.
|
|
211
317
|
fn conn_send(id: u32, data: String) -> bool {
|
|
212
318
|
let guard = match CONNS.lock() {
|
|
213
319
|
Ok(g) => g,
|
|
@@ -217,7 +323,21 @@ fn conn_send(id: u32, data: String) -> bool {
|
|
|
217
323
|
Some(s) if s.open => s,
|
|
218
324
|
_ => return false,
|
|
219
325
|
};
|
|
220
|
-
|
|
326
|
+
let len = data.len();
|
|
327
|
+
let queued = state.queued_bytes.fetch_add(len, Ordering::AcqRel);
|
|
328
|
+
// Refuse over-budget sends, but always admit one message into an empty queue (see
|
|
329
|
+
// `ws_send_buf_bytes`) — mirrors tungstenite's own "buffer + one message" convention.
|
|
330
|
+
if queued > 0 && queued.saturating_add(len) > ws_send_buf_bytes() {
|
|
331
|
+
state.queued_bytes.fetch_sub(len, Ordering::AcqRel);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
match state.send_tx.try_send(data) {
|
|
335
|
+
Ok(()) => true,
|
|
336
|
+
Err(_) => {
|
|
337
|
+
state.queued_bytes.fetch_sub(len, Ordering::AcqRel);
|
|
338
|
+
false
|
|
339
|
+
}
|
|
340
|
+
}
|
|
221
341
|
}
|
|
222
342
|
|
|
223
343
|
/// Default timeout for receive() so the main thread blocks and keeps the process/runtime alive.
|
|
@@ -242,7 +362,13 @@ fn conn_receive_timeout(id: u32, timeout_ms: u64) -> Option<String> {
|
|
|
242
362
|
};
|
|
243
363
|
if !guard.contains_key(&id) {
|
|
244
364
|
drop(guard);
|
|
245
|
-
|
|
365
|
+
// Dead/unknown id: return promptly, but pace tight caller loops that poll a closed
|
|
366
|
+
// conn — sleeping never past the caller's own deadline. `receiveTimeout(0)` must
|
|
367
|
+
// cost 0ms, not a flat 50ms.
|
|
368
|
+
let remaining = deadline.saturating_duration_since(Instant::now());
|
|
369
|
+
if !remaining.is_zero() {
|
|
370
|
+
std::thread::sleep(remaining.min(poll_interval));
|
|
371
|
+
}
|
|
246
372
|
return None;
|
|
247
373
|
}
|
|
248
374
|
guard.get(&id).unwrap().recv_rx.try_recv()
|
|
@@ -290,10 +416,30 @@ fn conn_id_from_value(v: &Value) -> Option<u32> {
|
|
|
290
416
|
}
|
|
291
417
|
}
|
|
292
418
|
|
|
419
|
+
/// Drop conn-array entries whose connection is no longer registered (closed or disconnected).
|
|
420
|
+
/// Called lazily from the tish thread — at the `server.clients` push sites and before broadcasts —
|
|
421
|
+
/// and NEVER from the tokio read task: `VmRef` is `Rc` in single-threaded builds, so cross-thread
|
|
422
|
+
/// pruning would be unsound. Keeps `server.clients` O(live) instead of O(ever-accepted) and matches
|
|
423
|
+
/// Node `ws` semantics, where closed sockets leave `clients` (#698).
|
|
424
|
+
fn prune_closed_conns(list: &mut Vec<Value>) {
|
|
425
|
+
let Ok(guard) = CONNS.lock() else { return };
|
|
426
|
+
list.retain(|c| {
|
|
427
|
+
conn_id_from_value(c)
|
|
428
|
+
.map(|id| guard.contains_key(&id))
|
|
429
|
+
.unwrap_or(false)
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
293
433
|
/// Native broadcast: send data to all conns in array except `except`. Avoids Tish-side method calls.
|
|
294
434
|
pub fn ws_broadcast_native(args: &[Value]) -> Value {
|
|
295
435
|
let conns = match args.first() {
|
|
296
|
-
Some(Value::Array(a)) =>
|
|
436
|
+
Some(Value::Array(a)) => {
|
|
437
|
+
// Prune first so broadcast cost stays proportional to LIVE connections, not to every
|
|
438
|
+
// connection ever accepted (#698). Sequential borrows — the mutable one ends before
|
|
439
|
+
// the clone's shared borrow starts.
|
|
440
|
+
prune_closed_conns(&mut a.borrow_mut());
|
|
441
|
+
a.borrow().clone()
|
|
442
|
+
}
|
|
297
443
|
_ => return Value::Null,
|
|
298
444
|
};
|
|
299
445
|
let except = args.get(1).cloned().unwrap_or(Value::Null);
|
|
@@ -417,13 +563,15 @@ pub fn web_socket_client(args: &[Value]) -> Value {
|
|
|
417
563
|
}
|
|
418
564
|
// wss:// needs rustls' process-default crypto provider installed first.
|
|
419
565
|
ensure_crypto_provider();
|
|
420
|
-
let (send_tx, mut send_rx) = tokio_mpsc::
|
|
566
|
+
let (send_tx, mut send_rx) = tokio_mpsc::channel::<String>(SEND_QUEUE_MAX_MSGS);
|
|
421
567
|
let (recv_tx, recv_rx) = mpsc::sync_channel::<String>(64);
|
|
422
568
|
let recv_tx = Arc::new(recv_tx);
|
|
569
|
+
let queued_bytes = Arc::new(AtomicUsize::new(0));
|
|
423
570
|
|
|
424
571
|
let id = with_ws_client_rt(|rt| {
|
|
425
572
|
rt.block_on(async move {
|
|
426
|
-
let connect =
|
|
573
|
+
let connect =
|
|
574
|
+
tokio_tungstenite::connect_async_with_config(&url, Some(ws_config()), false);
|
|
427
575
|
let connected = if connect_timeout_ms > 0 {
|
|
428
576
|
match tokio::time::timeout(Duration::from_millis(connect_timeout_ms), connect).await {
|
|
429
577
|
Ok(r) => r,
|
|
@@ -456,18 +604,22 @@ pub fn web_socket_client(args: &[Value]) -> Value {
|
|
|
456
604
|
return None;
|
|
457
605
|
}
|
|
458
606
|
};
|
|
459
|
-
let id = register(send_tx, recv_rx);
|
|
607
|
+
let id = register(send_tx, recv_rx, Arc::clone(&queued_bytes));
|
|
460
608
|
let (mut write, mut read) = ws_stream.split();
|
|
461
609
|
let recv_tx = Arc::clone(&recv_tx);
|
|
462
610
|
let url_closed = url.clone();
|
|
463
|
-
tokio::spawn(async move {
|
|
611
|
+
let read_task = tokio::spawn(async move {
|
|
464
612
|
while let Some(Ok(msg)) = read.next().await {
|
|
465
613
|
match msg {
|
|
466
614
|
tokio_tungstenite::tungstenite::Message::Text(t) => {
|
|
467
|
-
|
|
615
|
+
if recv_tx.send(t.to_string()).is_err() {
|
|
616
|
+
break;
|
|
617
|
+
}
|
|
468
618
|
}
|
|
469
619
|
tokio_tungstenite::tungstenite::Message::Binary(b) => {
|
|
470
|
-
|
|
620
|
+
if recv_tx.send(String::from_utf8_lossy(&b).into_owned()).is_err() {
|
|
621
|
+
break;
|
|
622
|
+
}
|
|
471
623
|
}
|
|
472
624
|
_ => {}
|
|
473
625
|
}
|
|
@@ -477,12 +629,17 @@ pub fn web_socket_client(args: &[Value]) -> Value {
|
|
|
477
629
|
}
|
|
478
630
|
unregister(id);
|
|
479
631
|
});
|
|
632
|
+
attach_read_task(id, &read_task);
|
|
480
633
|
tokio::spawn(async move {
|
|
481
634
|
while let Some(text) = send_rx.recv().await {
|
|
635
|
+
let n = text.len();
|
|
482
636
|
let _ = write
|
|
483
637
|
.send(tokio_tungstenite::tungstenite::Message::Text(text.into()))
|
|
484
638
|
.await;
|
|
639
|
+
queued_bytes.fetch_sub(n, Ordering::AcqRel);
|
|
485
640
|
}
|
|
641
|
+
// See `register_ws_stream`: drive the closing handshake so close() frees the fd.
|
|
642
|
+
let _ = write.close().await;
|
|
486
643
|
});
|
|
487
644
|
Some(id)
|
|
488
645
|
})
|
|
@@ -504,11 +661,18 @@ pub fn web_socket_server_listen(args: &[Value]) -> Value {
|
|
|
504
661
|
|
|
505
662
|
let (bind_tx, bind_rx) = mpsc::sync_channel::<bool>(1);
|
|
506
663
|
let (conn_tx, conn_rx) = mpsc::channel::<u32>();
|
|
664
|
+
let (shutdown_tx, mut shutdown_rx) = tokio::sync::oneshot::channel::<()>();
|
|
507
665
|
let handle = next_server_handle();
|
|
508
666
|
|
|
509
667
|
{
|
|
510
668
|
let mut map = SERVER_RECV.lock().unwrap();
|
|
511
|
-
map.insert(
|
|
669
|
+
map.insert(
|
|
670
|
+
handle,
|
|
671
|
+
ServerState {
|
|
672
|
+
conn_rx,
|
|
673
|
+
shutdown_tx: Some(shutdown_tx),
|
|
674
|
+
},
|
|
675
|
+
);
|
|
512
676
|
}
|
|
513
677
|
|
|
514
678
|
std::thread::spawn(move || {
|
|
@@ -534,35 +698,45 @@ pub fn web_socket_server_listen(args: &[Value]) -> Value {
|
|
|
534
698
|
println!("WebSocket server listening on ws://0.0.0.0:{}", port);
|
|
535
699
|
|
|
536
700
|
loop {
|
|
537
|
-
|
|
701
|
+
// `server.close()` fires (or drops) the shutdown signal — exit the loop so the
|
|
702
|
+
// listener, this runtime and its thread are actually freed (`TcpListener::accept`
|
|
703
|
+
// is cancel-safe).
|
|
704
|
+
let accepted = tokio::select! {
|
|
705
|
+
_ = &mut shutdown_rx => break,
|
|
706
|
+
r = listener.accept() => r,
|
|
707
|
+
};
|
|
708
|
+
let (stream, _) = match accepted {
|
|
538
709
|
Ok(s) => s,
|
|
539
710
|
Err(_) => break,
|
|
540
711
|
};
|
|
541
712
|
// Cap total connections — unbounded accepts would exhaust tasks/memory.
|
|
542
|
-
if
|
|
713
|
+
if !has_ws_capacity() {
|
|
543
714
|
drop(stream);
|
|
544
715
|
continue;
|
|
545
716
|
}
|
|
546
|
-
let ws_stream =
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
717
|
+
let ws_stream =
|
|
718
|
+
match tokio_tungstenite::accept_async_with_config(stream, Some(ws_config()))
|
|
719
|
+
.await
|
|
720
|
+
{
|
|
721
|
+
Ok(ws) => {
|
|
722
|
+
if ws_debug() {
|
|
723
|
+
eprintln!(
|
|
724
|
+
"[tish ws] server accepted connection (handshake OK): port {}",
|
|
725
|
+
port
|
|
726
|
+
);
|
|
727
|
+
}
|
|
728
|
+
ws
|
|
553
729
|
}
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
730
|
+
Err(e) => {
|
|
731
|
+
if ws_debug() {
|
|
732
|
+
eprintln!(
|
|
733
|
+
"[tish ws] server accept_async failed: {} (port {})",
|
|
734
|
+
e, port
|
|
735
|
+
);
|
|
736
|
+
}
|
|
737
|
+
continue;
|
|
562
738
|
}
|
|
563
|
-
|
|
564
|
-
}
|
|
565
|
-
};
|
|
739
|
+
};
|
|
566
740
|
let id = register_ws_stream(ws_stream);
|
|
567
741
|
if conn_tx.send(id).is_err() {
|
|
568
742
|
break;
|
|
@@ -590,11 +764,11 @@ pub fn web_socket_server_accept(args: &[Value]) -> Value {
|
|
|
590
764
|
Ok(g) => g,
|
|
591
765
|
Err(_) => return Value::Null,
|
|
592
766
|
};
|
|
593
|
-
let
|
|
594
|
-
Some(
|
|
767
|
+
let st = match map.get_mut(&handle) {
|
|
768
|
+
Some(s) => s,
|
|
595
769
|
None => return Value::Null,
|
|
596
770
|
};
|
|
597
|
-
match
|
|
771
|
+
match st.conn_rx.recv() {
|
|
598
772
|
Ok(id) => conn_object(id),
|
|
599
773
|
Err(_) => Value::Null,
|
|
600
774
|
}
|
|
@@ -614,16 +788,36 @@ pub fn web_socket_server_accept_timeout(args: &[Value]) -> Value {
|
|
|
614
788
|
Ok(g) => g,
|
|
615
789
|
Err(_) => return Value::Null,
|
|
616
790
|
};
|
|
617
|
-
let
|
|
618
|
-
Some(
|
|
791
|
+
let st = match map.get_mut(&handle) {
|
|
792
|
+
Some(s) => s,
|
|
619
793
|
None => return Value::Null,
|
|
620
794
|
};
|
|
621
|
-
match
|
|
795
|
+
match st
|
|
796
|
+
.conn_rx
|
|
797
|
+
.recv_timeout(std::time::Duration::from_millis(timeout_ms))
|
|
798
|
+
{
|
|
622
799
|
Ok(id) => conn_object(id),
|
|
623
800
|
Err(_) => Value::Null,
|
|
624
801
|
}
|
|
625
802
|
}
|
|
626
803
|
|
|
804
|
+
/// Tear down a listening server: stop the accept loop (freeing the `TcpListener`, its dedicated
|
|
805
|
+
/// thread and tokio runtime — previously they lived for the whole process, #698), and unregister
|
|
806
|
+
/// any accepted-but-unclaimed connections still sitting in the queue.
|
|
807
|
+
fn server_close(handle: u32) {
|
|
808
|
+
let state = SERVER_RECV.lock().ok().and_then(|mut m| m.remove(&handle));
|
|
809
|
+
if let Some(mut st) = state {
|
|
810
|
+
if let Some(tx) = st.shutdown_tx.take() {
|
|
811
|
+
let _ = tx.send(());
|
|
812
|
+
}
|
|
813
|
+
// Connections the accept loop registered but the program never picked up would otherwise
|
|
814
|
+
// stay in CONNS (holding their tasks + fd) until the remote peer disconnects.
|
|
815
|
+
while let Ok(id) = st.conn_rx.try_recv() {
|
|
816
|
+
unregister(id);
|
|
817
|
+
}
|
|
818
|
+
}
|
|
819
|
+
}
|
|
820
|
+
|
|
627
821
|
/// `Server(options)` — object with `_handle`, `_onConnection`, `on`, `listen`, `clients` (Node.js-compatible).
|
|
628
822
|
pub fn web_socket_server_construct(args: &[Value]) -> Value {
|
|
629
823
|
let handle_val = web_socket_server_listen(args);
|
|
@@ -679,7 +873,14 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
|
|
|
679
873
|
if matches!(ws, Value::Null) {
|
|
680
874
|
break;
|
|
681
875
|
}
|
|
682
|
-
|
|
876
|
+
{
|
|
877
|
+
// Lazy prune on the tish thread: closed conns leave `clients` before each push, so
|
|
878
|
+
// the array tracks LIVE connections (Node `ws` semantics) instead of growing with
|
|
879
|
+
// every connection ever accepted (#698).
|
|
880
|
+
let mut list = clients_listen.borrow_mut();
|
|
881
|
+
prune_closed_conns(&mut list);
|
|
882
|
+
list.push(ws.clone());
|
|
883
|
+
}
|
|
683
884
|
if let Value::Function(f) = cb {
|
|
684
885
|
let _ = f.call(&[ws]);
|
|
685
886
|
}
|
|
@@ -701,11 +902,32 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
|
|
|
701
902
|
let timeout_ms = args.get(1).cloned().unwrap_or(Value::Number(100.0));
|
|
702
903
|
let ws = web_socket_server_accept_timeout(&[handle_n, timeout_ms]);
|
|
703
904
|
if !matches!(ws, Value::Null) {
|
|
704
|
-
|
|
905
|
+
// Same lazy prune as the listen() push site (#698).
|
|
906
|
+
let mut list = clients_accept.borrow_mut();
|
|
907
|
+
prune_closed_conns(&mut list);
|
|
908
|
+
list.push(ws.clone());
|
|
705
909
|
}
|
|
706
910
|
ws
|
|
707
911
|
});
|
|
708
912
|
|
|
913
|
+
let handle_u32 = match &handle_val {
|
|
914
|
+
Value::Number(n) if n.is_finite() && *n >= 0.0 => *n as u32,
|
|
915
|
+
_ => 0,
|
|
916
|
+
};
|
|
917
|
+
let clients_close = clients.clone();
|
|
918
|
+
let close_fn = Value::native(move |args: &[Value]| {
|
|
919
|
+
server_close(handle_u32);
|
|
920
|
+
// Null the handle so a listen() loop exits on its next iteration, and clear `clients`
|
|
921
|
+
// (the accept loop is stopped, so nothing repopulates it).
|
|
922
|
+
if let Some(Value::Object(so)) = args.first() {
|
|
923
|
+
so.borrow_mut()
|
|
924
|
+
.strings
|
|
925
|
+
.insert(Arc::from("_handle"), Value::Null);
|
|
926
|
+
}
|
|
927
|
+
clients_close.borrow_mut().clear();
|
|
928
|
+
Value::Null
|
|
929
|
+
});
|
|
930
|
+
|
|
709
931
|
let mut m: ObjectMap = ObjectMap::default();
|
|
710
932
|
m.insert(Arc::from("_handle"), handle_val);
|
|
711
933
|
m.insert(Arc::from("_onConnection"), Value::Null);
|
|
@@ -713,6 +935,7 @@ pub fn web_socket_server_construct(args: &[Value]) -> Value {
|
|
|
713
935
|
m.insert(Arc::from("on"), on_fn);
|
|
714
936
|
m.insert(Arc::from("listen"), listen_fn);
|
|
715
937
|
m.insert(Arc::from("acceptTimeout"), accept_timeout_fn);
|
|
938
|
+
m.insert(Arc::from("close"), close_fn);
|
|
716
939
|
Value::object(m)
|
|
717
940
|
}
|
|
718
941
|
|
|
@@ -826,14 +1049,170 @@ mod tests {
|
|
|
826
1049
|
/// socket and emit EOF exactly once).
|
|
827
1050
|
#[test]
|
|
828
1051
|
fn conn_is_open_reflects_registry() {
|
|
829
|
-
let (tx, _rx) = tokio_mpsc::
|
|
1052
|
+
let (tx, _rx) = tokio_mpsc::channel::<String>(4);
|
|
830
1053
|
let (_stx, srx) = mpsc::sync_channel::<String>(1);
|
|
831
|
-
let id = register(tx, srx);
|
|
1054
|
+
let id = register(tx, srx, Arc::new(AtomicUsize::new(0)));
|
|
832
1055
|
assert!(conn_is_open(id), "just-registered conn should be open");
|
|
833
1056
|
unregister(id);
|
|
834
1057
|
assert!(!conn_is_open(id), "unregistered conn should be closed");
|
|
835
1058
|
}
|
|
836
1059
|
|
|
1060
|
+
/// The bounded send queue must refuse (return false) once the byte budget is reached instead of
|
|
1061
|
+
/// queueing forever — the backpressure contract callers rely on (`sent === false` → drop the
|
|
1062
|
+
/// peer). Nothing drains the channel here, simulating a peer that stopped reading.
|
|
1063
|
+
#[test]
|
|
1064
|
+
fn conn_send_refuses_when_queue_full() {
|
|
1065
|
+
let (tx, _rx) = tokio_mpsc::channel::<String>(SEND_QUEUE_MAX_MSGS);
|
|
1066
|
+
let (_stx, srx) = mpsc::sync_channel::<String>(1);
|
|
1067
|
+
let id = register(tx, srx, Arc::new(AtomicUsize::new(0)));
|
|
1068
|
+
|
|
1069
|
+
let chunk = "x".repeat(64 * 1024);
|
|
1070
|
+
let budget = ws_send_buf_bytes();
|
|
1071
|
+
let mut accepted = 0usize;
|
|
1072
|
+
while conn_send(id, chunk.clone()) {
|
|
1073
|
+
accepted += 1;
|
|
1074
|
+
assert!(
|
|
1075
|
+
accepted * chunk.len() <= budget + chunk.len(),
|
|
1076
|
+
"queue admitted {} bytes — budget {} never enforced",
|
|
1077
|
+
accepted * chunk.len(),
|
|
1078
|
+
budget
|
|
1079
|
+
);
|
|
1080
|
+
}
|
|
1081
|
+
assert!(
|
|
1082
|
+
accepted > 0,
|
|
1083
|
+
"an empty queue must admit at least one message"
|
|
1084
|
+
);
|
|
1085
|
+
assert!(
|
|
1086
|
+
(accepted + 1) * chunk.len() > budget,
|
|
1087
|
+
"refused after only {} bytes (budget {})",
|
|
1088
|
+
accepted * chunk.len(),
|
|
1089
|
+
budget
|
|
1090
|
+
);
|
|
1091
|
+
// Once full, even a tiny send is refused (only an EMPTY queue admits unconditionally).
|
|
1092
|
+
assert!(
|
|
1093
|
+
!conn_send(id, "y".into()),
|
|
1094
|
+
"full queue must refuse further sends"
|
|
1095
|
+
);
|
|
1096
|
+
unregister(id);
|
|
1097
|
+
assert!(!conn_send(id, "z".into()), "closed conn must refuse sends");
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
/// `server.clients` housekeeping: `prune_closed_conns` drops entries whose connection is no
|
|
1101
|
+
/// longer registered (and non-conn junk), keeping only live conns — Node `ws` semantics (#698).
|
|
1102
|
+
#[test]
|
|
1103
|
+
fn prune_closed_conns_drops_dead_entries() {
|
|
1104
|
+
let (tx, _rx) = tokio_mpsc::channel::<String>(4);
|
|
1105
|
+
let (_stx, srx) = mpsc::sync_channel::<String>(1);
|
|
1106
|
+
let live = register(tx, srx, Arc::new(AtomicUsize::new(0)));
|
|
1107
|
+
let (tx2, _rx2) = tokio_mpsc::channel::<String>(4);
|
|
1108
|
+
let (_stx2, srx2) = mpsc::sync_channel::<String>(1);
|
|
1109
|
+
let dead = register(tx2, srx2, Arc::new(AtomicUsize::new(0)));
|
|
1110
|
+
unregister(dead);
|
|
1111
|
+
|
|
1112
|
+
let mut list = vec![conn_object(live), conn_object(dead), Value::Null];
|
|
1113
|
+
prune_closed_conns(&mut list);
|
|
1114
|
+
assert_eq!(list.len(), 1, "only the live conn should remain");
|
|
1115
|
+
assert_eq!(conn_id_from_value(&list[0]), Some(live));
|
|
1116
|
+
unregister(live);
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/// `receiveTimeout` on a dead/unknown id must honor the caller's timeout — it used to sleep a
|
|
1120
|
+
/// flat 50ms, so a pump loop polling N zombie conns paid N x 50ms per tick.
|
|
1121
|
+
#[test]
|
|
1122
|
+
fn receive_timeout_dead_id_honors_caller_timeout() {
|
|
1123
|
+
let t0 = Instant::now();
|
|
1124
|
+
assert!(conn_receive_timeout(u32::MAX, 0).is_none());
|
|
1125
|
+
assert!(
|
|
1126
|
+
t0.elapsed() < Duration::from_millis(40),
|
|
1127
|
+
"receiveTimeout(0) on a dead id must return immediately, took {:?}",
|
|
1128
|
+
t0.elapsed()
|
|
1129
|
+
);
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/// close() must actually close the socket: the SERVER side must observe the stream ending and
|
|
1133
|
+
/// unregister its conn. Before the fix, close() only removed the local registry entry — the
|
|
1134
|
+
/// read task stayed parked on the open socket forever (fd + task + buffers leaked per close).
|
|
1135
|
+
#[test]
|
|
1136
|
+
fn client_close_shuts_down_the_socket() {
|
|
1137
|
+
let port: u16 = 18_745;
|
|
1138
|
+
let opts = {
|
|
1139
|
+
let mut m: ObjectMap = ObjectMap::default();
|
|
1140
|
+
m.insert(Arc::from("port"), Value::Number(port as f64));
|
|
1141
|
+
Value::object(m)
|
|
1142
|
+
};
|
|
1143
|
+
let handle = match web_socket_server_listen(std::slice::from_ref(&opts)) {
|
|
1144
|
+
Value::Number(h) => h as u32,
|
|
1145
|
+
_ => panic!("listen failed"),
|
|
1146
|
+
};
|
|
1147
|
+
let (id_tx, id_rx) = mpsc::channel::<u32>();
|
|
1148
|
+
let server = thread::spawn(move || {
|
|
1149
|
+
let ws = web_socket_server_accept(&[Value::Number(handle as f64)]);
|
|
1150
|
+
let _ = id_tx.send(conn_id_from_value(&ws).expect("server conn id"));
|
|
1151
|
+
});
|
|
1152
|
+
|
|
1153
|
+
thread::sleep(Duration::from_millis(100));
|
|
1154
|
+
let url = format!("ws://127.0.0.1:{}/", port);
|
|
1155
|
+
let client = web_socket_client(&[Value::String(url.into())]);
|
|
1156
|
+
let Value::Object(co) = client else {
|
|
1157
|
+
panic!("client connect failed");
|
|
1158
|
+
};
|
|
1159
|
+
let server_id = id_rx
|
|
1160
|
+
.recv_timeout(Duration::from_secs(5))
|
|
1161
|
+
.expect("server never accepted");
|
|
1162
|
+
assert!(conn_is_open(server_id), "server conn should start open");
|
|
1163
|
+
|
|
1164
|
+
let Some(Value::Function(close_f)) = co.borrow().strings.get("close").cloned() else {
|
|
1165
|
+
panic!("no close");
|
|
1166
|
+
};
|
|
1167
|
+
let _ = close_f.call(&[]);
|
|
1168
|
+
|
|
1169
|
+
let deadline = Instant::now() + Duration::from_secs(5);
|
|
1170
|
+
while conn_is_open(server_id) && Instant::now() < deadline {
|
|
1171
|
+
thread::sleep(Duration::from_millis(25));
|
|
1172
|
+
}
|
|
1173
|
+
assert!(
|
|
1174
|
+
!conn_is_open(server_id),
|
|
1175
|
+
"server still sees the conn open after the client's close() — the socket was not shut down"
|
|
1176
|
+
);
|
|
1177
|
+
let _ = server.join();
|
|
1178
|
+
}
|
|
1179
|
+
|
|
1180
|
+
/// server.close() must stop the accept loop and free the TcpListener (previously a bound
|
|
1181
|
+
/// server's listener, thread and runtime lived for the whole process — #698).
|
|
1182
|
+
#[test]
|
|
1183
|
+
fn server_close_frees_the_listener() {
|
|
1184
|
+
let port: u16 = 18_746;
|
|
1185
|
+
let opts = {
|
|
1186
|
+
let mut m: ObjectMap = ObjectMap::default();
|
|
1187
|
+
m.insert(Arc::from("port"), Value::Number(port as f64));
|
|
1188
|
+
Value::object(m)
|
|
1189
|
+
};
|
|
1190
|
+
let srv = web_socket_server_construct(std::slice::from_ref(&opts));
|
|
1191
|
+
let Value::Object(so) = &srv else {
|
|
1192
|
+
panic!("server construct failed");
|
|
1193
|
+
};
|
|
1194
|
+
let Some(Value::Function(close_f)) = so.borrow().strings.get("close").cloned() else {
|
|
1195
|
+
panic!("no close method on server object");
|
|
1196
|
+
};
|
|
1197
|
+
let _ = close_f.call(std::slice::from_ref(&srv));
|
|
1198
|
+
|
|
1199
|
+
let deadline = Instant::now() + Duration::from_secs(3);
|
|
1200
|
+
let mut rebound = false;
|
|
1201
|
+
while Instant::now() < deadline {
|
|
1202
|
+
if std::net::TcpListener::bind(("0.0.0.0", port)).is_ok() {
|
|
1203
|
+
rebound = true;
|
|
1204
|
+
break;
|
|
1205
|
+
}
|
|
1206
|
+
thread::sleep(Duration::from_millis(50));
|
|
1207
|
+
}
|
|
1208
|
+
assert!(rebound, "port still held after server.close()");
|
|
1209
|
+
// And the handle is gone: a subsequent accept returns Null instead of blocking.
|
|
1210
|
+
assert!(matches!(
|
|
1211
|
+
web_socket_server_accept_timeout(&[Value::Number(0.0), Value::Number(0.0)]),
|
|
1212
|
+
Value::Null
|
|
1213
|
+
));
|
|
1214
|
+
}
|
|
1215
|
+
|
|
837
1216
|
#[test]
|
|
838
1217
|
fn ws_echo_roundtrip() {
|
|
839
1218
|
let port: u16 = 18_742;
|