@tishlang/tish-lsp 2.38.0 → 2.39.0
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-lsp +0 -0
- package/crates/tish_compile/src/codegen.rs +6 -1
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_runtime/src/http_hyper.rs +70 -0
- package/crates/tish_runtime/src/lib.rs +1 -1
- package/crates/tish_runtime/src/ws.rs +94 -21
- package/package.json +1 -1
- package/platform/darwin-arm64/tish-lsp +0 -0
- package/platform/darwin-x64/tish-lsp +0 -0
- package/platform/linux-arm64/tish-lsp +0 -0
- package/platform/linux-x64/tish-lsp +0 -0
- package/platform/win32-x64/tish-lsp.exe +0 -0
package/bin/tish-lsp
CHANGED
|
Binary file
|
|
@@ -1702,6 +1702,7 @@ impl Codegen {
|
|
|
1702
1702
|
"Server" => Some("Value::native(|args: &[Value]| tish_ws_server_construct(args))"),
|
|
1703
1703
|
"wsSend" => Some("Value::native(|args: &[Value]| Value::Bool(tishlang_runtime::ws_send_native(args.first().unwrap_or(&Value::Null), &args.get(1).map(|v| v.to_display_string()).unwrap_or_default())))"),
|
|
1704
1704
|
"wsBroadcast" => Some("Value::native(|args: &[Value]| tishlang_runtime::ws_broadcast_native(args))"),
|
|
1705
|
+
"wsAccept" => Some("Value::native(|args: &[Value]| tishlang_runtime::ws_serve_accept(args))"),
|
|
1705
1706
|
_ => None,
|
|
1706
1707
|
},
|
|
1707
1708
|
"tish:tty" if self.has_feature("tty") => match export_name {
|
|
@@ -2268,6 +2269,10 @@ impl Codegen {
|
|
|
2268
2269
|
self.write("use tishlang_runtime::register_static_route as tish_register_static_route;\n");
|
|
2269
2270
|
if self.is_async {
|
|
2270
2271
|
self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve, promise_object as tish_promise_object, await_promise as tish_await_promise, await_promise_throw as tish_await_promise_throw};\n");
|
|
2272
|
+
} else if self.has_feature("ws") {
|
|
2273
|
+
// ws programs get `Promise.spawn` (send-values OS-thread spawn) even when not async,
|
|
2274
|
+
// so a server can run a background accept/pump loop alongside the blocking `serve()`.
|
|
2275
|
+
self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve, promise_object as tish_promise_object};\n");
|
|
2271
2276
|
} else {
|
|
2272
2277
|
self.write("use tishlang_runtime::{fetch_promise as tish_fetch_promise, fetch_all_promise as tish_fetch_all_promise, http_serve as tish_http_serve};\n");
|
|
2273
2278
|
}
|
|
@@ -2693,7 +2698,7 @@ impl Codegen {
|
|
|
2693
2698
|
"let fetch = Value::native(|args: &[Value]| tish_fetch_promise(args.to_vec()));",
|
|
2694
2699
|
);
|
|
2695
2700
|
self.writeln("let fetchAll = Value::native(|args: &[Value]| tish_fetch_all_promise(args.to_vec()));");
|
|
2696
|
-
if self.is_async {
|
|
2701
|
+
if self.is_async || self.has_feature("ws") {
|
|
2697
2702
|
self.writeln("let Promise = tish_promise_object();");
|
|
2698
2703
|
}
|
|
2699
2704
|
// `serve` supports two shapes:
|
|
@@ -289,6 +289,9 @@ fn worker_thread(
|
|
|
289
289
|
// headers slower than this instead of pinning the worker task.
|
|
290
290
|
.header_read_timeout(std::time::Duration::from_secs(30))
|
|
291
291
|
.serve_connection(io, svc)
|
|
292
|
+
// Required for `hyper::upgrade::on` to hand off the socket after a 101 — without
|
|
293
|
+
// this, a WebSocket upgrade sends the 101 but the upgrade future never resolves.
|
|
294
|
+
.with_upgrades()
|
|
292
295
|
.await
|
|
293
296
|
{
|
|
294
297
|
// Chatty connection errors (resets, client timeouts) are
|
|
@@ -314,6 +317,13 @@ async fn handle_request(
|
|
|
314
317
|
return Ok(static_route_response(route));
|
|
315
318
|
}
|
|
316
319
|
|
|
320
|
+
// WebSocket upgrade: complete the handshake here and hand the socket to `ws.rs` — the VM handler
|
|
321
|
+
// never sees it. The program picks the connection up via `wsAccept`. (Single-port WS, issue #495.)
|
|
322
|
+
#[cfg(feature = "ws")]
|
|
323
|
+
if is_ws_upgrade(&req) {
|
|
324
|
+
return Ok(handle_ws_upgrade(req));
|
|
325
|
+
}
|
|
326
|
+
|
|
317
327
|
// Slow path: cross the mpsc to the VM thread.
|
|
318
328
|
let (method, url, path, query, headers, body_bytes) = extract_request(req).await;
|
|
319
329
|
// Lossy UTF-8 decode to match the tiny_http path (`http.rs`): a body with invalid UTF-8 becomes
|
|
@@ -455,6 +465,66 @@ fn simple_error_response(status: StatusCode, msg: &str) -> Response<Full<Bytes>>
|
|
|
455
465
|
.unwrap()
|
|
456
466
|
}
|
|
457
467
|
|
|
468
|
+
/// A well-formed WebSocket upgrade handshake request (RFC 6455): `Upgrade: websocket`,
|
|
469
|
+
/// `Connection: Upgrade`, and a `Sec-WebSocket-Key`.
|
|
470
|
+
#[cfg(feature = "ws")]
|
|
471
|
+
fn is_ws_upgrade(req: &Request<hyper::body::Incoming>) -> bool {
|
|
472
|
+
let h = req.headers();
|
|
473
|
+
let upgrade_ws = h
|
|
474
|
+
.get(hyper::header::UPGRADE)
|
|
475
|
+
.and_then(|v| v.to_str().ok())
|
|
476
|
+
.map(|s| s.eq_ignore_ascii_case("websocket"))
|
|
477
|
+
.unwrap_or(false);
|
|
478
|
+
let conn_upgrade = h
|
|
479
|
+
.get(hyper::header::CONNECTION)
|
|
480
|
+
.and_then(|v| v.to_str().ok())
|
|
481
|
+
.map(|s| s.to_ascii_lowercase().split(',').any(|p| p.trim() == "upgrade"))
|
|
482
|
+
.unwrap_or(false);
|
|
483
|
+
upgrade_ws && conn_upgrade && h.contains_key(hyper::header::SEC_WEBSOCKET_KEY)
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/// Complete a WebSocket handshake on the HTTP port: reply `101` with the derived accept key, then
|
|
487
|
+
/// (after the response flushes) take over the connection, wrap it as a server-role WebSocket, and
|
|
488
|
+
/// register it with `ws.rs` so the program can pick it up via `wsAccept`.
|
|
489
|
+
#[cfg(feature = "ws")]
|
|
490
|
+
fn handle_ws_upgrade(mut req: Request<hyper::body::Incoming>) -> Response<Full<Bytes>> {
|
|
491
|
+
use tokio_tungstenite::tungstenite::handshake::derive_accept_key;
|
|
492
|
+
use tokio_tungstenite::tungstenite::protocol::Role;
|
|
493
|
+
|
|
494
|
+
let accept = match req.headers().get(hyper::header::SEC_WEBSOCKET_KEY) {
|
|
495
|
+
Some(k) => derive_accept_key(k.as_bytes()),
|
|
496
|
+
None => return simple_error_response(StatusCode::BAD_REQUEST, "missing Sec-WebSocket-Key"),
|
|
497
|
+
};
|
|
498
|
+
|
|
499
|
+
let on_upgrade = hyper::upgrade::on(&mut req);
|
|
500
|
+
tokio::spawn(async move {
|
|
501
|
+
match on_upgrade.await {
|
|
502
|
+
Ok(upgraded) => {
|
|
503
|
+
let io = TokioIo::new(upgraded);
|
|
504
|
+
let ws = tokio_tungstenite::WebSocketStream::from_raw_socket(io, Role::Server, None)
|
|
505
|
+
.await;
|
|
506
|
+
let id = crate::ws::register_ws_stream(ws);
|
|
507
|
+
crate::ws::enqueue_upgraded_conn(id);
|
|
508
|
+
}
|
|
509
|
+
Err(e) => {
|
|
510
|
+
if std::env::var_os("TISH_HTTP_DEBUG").is_some() {
|
|
511
|
+
eprintln!("[tish http/hyper] ws upgrade failed: {}", e);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
});
|
|
516
|
+
|
|
517
|
+
Response::builder()
|
|
518
|
+
.status(StatusCode::SWITCHING_PROTOCOLS)
|
|
519
|
+
.header(hyper::header::UPGRADE, "websocket")
|
|
520
|
+
.header(hyper::header::CONNECTION, "Upgrade")
|
|
521
|
+
.header(hyper::header::SEC_WEBSOCKET_ACCEPT, accept)
|
|
522
|
+
.body(Full::new(Bytes::new()))
|
|
523
|
+
.unwrap_or_else(|_| {
|
|
524
|
+
simple_error_response(StatusCode::INTERNAL_SERVER_ERROR, "ws upgrade build failed")
|
|
525
|
+
})
|
|
526
|
+
}
|
|
527
|
+
|
|
458
528
|
/// Marker used by external callers (like the bench) to gate the backend
|
|
459
529
|
/// choice. Returns true when `TISH_HTTP_BACKEND=hyper` is in the env.
|
|
460
530
|
pub fn is_enabled_via_env() -> bool {
|
|
@@ -1735,7 +1735,7 @@ pub use pty::{pty_kill, pty_pid, pty_read, pty_resize, pty_spawn, pty_write};
|
|
|
1735
1735
|
#[cfg(feature = "ws")]
|
|
1736
1736
|
pub use ws::{
|
|
1737
1737
|
web_socket_client, web_socket_server_accept, web_socket_server_construct,
|
|
1738
|
-
web_socket_server_listen, ws_broadcast_native, ws_send_native,
|
|
1738
|
+
web_socket_server_listen, ws_broadcast_native, ws_send_native, ws_serve_accept,
|
|
1739
1739
|
};
|
|
1740
1740
|
|
|
1741
1741
|
#[cfg(feature = "http")]
|
|
@@ -82,6 +82,83 @@ fn unregister(id: u32) {
|
|
|
82
82
|
CONNS.lock().unwrap().remove(&id);
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
+
/// Bridge an already-handshaked WebSocket stream into the `CONNS` registry — spawning the read and
|
|
86
|
+
/// write pump tasks — and return its connection id. Shared by the `tish:ws` server-accept loop and
|
|
87
|
+
/// the `serve()` HTTP→WS upgrade path (see `http_hyper.rs`). Must be called inside a tokio runtime.
|
|
88
|
+
pub(crate) fn register_ws_stream<S>(ws_stream: tokio_tungstenite::WebSocketStream<S>) -> u32
|
|
89
|
+
where
|
|
90
|
+
S: tokio::io::AsyncRead + tokio::io::AsyncWrite + Unpin + Send + 'static,
|
|
91
|
+
{
|
|
92
|
+
let (send_tx, mut send_rx) = tokio_mpsc::unbounded_channel::<String>();
|
|
93
|
+
let (recv_tx, recv_rx) = mpsc::sync_channel::<String>(64);
|
|
94
|
+
let id = register(send_tx, recv_rx);
|
|
95
|
+
let recv_tx_task = Arc::new(recv_tx);
|
|
96
|
+
let (mut write, mut read) = ws_stream.split();
|
|
97
|
+
tokio::spawn(async move {
|
|
98
|
+
while let Some(Ok(msg)) = read.next().await {
|
|
99
|
+
if let tokio_tungstenite::tungstenite::Message::Text(t) = msg {
|
|
100
|
+
let _ = recv_tx_task.send(t.to_string());
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
unregister(id);
|
|
104
|
+
});
|
|
105
|
+
tokio::spawn(async move {
|
|
106
|
+
while let Some(text) = send_rx.recv().await {
|
|
107
|
+
let _ = write
|
|
108
|
+
.send(tokio_tungstenite::tungstenite::Message::Text(text.into()))
|
|
109
|
+
.await;
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
id
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
lazy_static! {
|
|
116
|
+
/// Connections upgraded from HTTP by `serve()` (http_hyper), waiting for the program to pick them
|
|
117
|
+
/// up via `wsAccept`. Per-process: an upgraded socket stays in whatever SO_REUSEPORT worker
|
|
118
|
+
/// accepted it, so each worker drains its own queue and there's no cross-process routing problem.
|
|
119
|
+
static ref UPGRADE_QUEUE: (Mutex<std::collections::VecDeque<u32>>, std::sync::Condvar) =
|
|
120
|
+
(Mutex::new(std::collections::VecDeque::new()), std::sync::Condvar::new());
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/// Enqueue a `serve()`-upgraded connection id for `wsAccept` to hand to the program.
|
|
124
|
+
pub(crate) fn enqueue_upgraded_conn(id: u32) {
|
|
125
|
+
let (m, cv) = &*UPGRADE_QUEUE;
|
|
126
|
+
if let Ok(mut q) = m.lock() {
|
|
127
|
+
q.push_back(id);
|
|
128
|
+
}
|
|
129
|
+
cv.notify_all();
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/// `wsAccept(timeoutMs?)` — return the next connection upgraded on the HTTP server's port (see the
|
|
133
|
+
/// `serve()` HTTP→WS upgrade path) as a normal `tish:ws` connection object, or `Null` if none arrives
|
|
134
|
+
/// within the timeout (default 0 = non-blocking).
|
|
135
|
+
pub fn ws_serve_accept(args: &[Value]) -> Value {
|
|
136
|
+
let timeout_ms = match args.first() {
|
|
137
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => (*n as u64).min(3_600_000),
|
|
138
|
+
_ => 0,
|
|
139
|
+
};
|
|
140
|
+
let (m, cv) = &*UPGRADE_QUEUE;
|
|
141
|
+
let mut q = match m.lock() {
|
|
142
|
+
Ok(q) => q,
|
|
143
|
+
Err(_) => return Value::Null,
|
|
144
|
+
};
|
|
145
|
+
if q.is_empty() && timeout_ms > 0 {
|
|
146
|
+
let res = cv.wait_timeout_while(
|
|
147
|
+
q,
|
|
148
|
+
std::time::Duration::from_millis(timeout_ms),
|
|
149
|
+
|q| q.is_empty(),
|
|
150
|
+
);
|
|
151
|
+
q = match res {
|
|
152
|
+
Ok((g, _)) => g,
|
|
153
|
+
Err(e) => e.into_inner().0,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
match q.pop_front() {
|
|
157
|
+
Some(id) => conn_object(id),
|
|
158
|
+
None => Value::Null,
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
85
162
|
/// Max simultaneous WebSocket connections. Each accepted conn registers in `CONNS` and
|
|
86
163
|
/// spawns tasks + channels, freed only on close — unbounded accepts exhaust tasks/memory.
|
|
87
164
|
/// Override with `TISH_WS_MAX_CONNS`; default 10000.
|
|
@@ -399,27 +476,7 @@ pub fn web_socket_server_listen(args: &[Value]) -> Value {
|
|
|
399
476
|
continue;
|
|
400
477
|
}
|
|
401
478
|
};
|
|
402
|
-
let
|
|
403
|
-
let (recv_tx, recv_rx) = mpsc::sync_channel::<String>(64);
|
|
404
|
-
let id = register(send_tx, recv_rx);
|
|
405
|
-
let recv_tx = Arc::new(recv_tx);
|
|
406
|
-
let recv_tx_task = Arc::clone(&recv_tx);
|
|
407
|
-
let (mut write, mut read) = ws_stream.split();
|
|
408
|
-
tokio::spawn(async move {
|
|
409
|
-
while let Some(Ok(msg)) = read.next().await {
|
|
410
|
-
if let tokio_tungstenite::tungstenite::Message::Text(t) = msg {
|
|
411
|
-
let _ = recv_tx_task.send(t.to_string());
|
|
412
|
-
}
|
|
413
|
-
}
|
|
414
|
-
unregister(id);
|
|
415
|
-
});
|
|
416
|
-
tokio::spawn(async move {
|
|
417
|
-
while let Some(text) = send_rx.recv().await {
|
|
418
|
-
let _ = write
|
|
419
|
-
.send(tokio_tungstenite::tungstenite::Message::Text(text.into()))
|
|
420
|
-
.await;
|
|
421
|
-
}
|
|
422
|
-
});
|
|
479
|
+
let id = register_ws_stream(ws_stream);
|
|
423
480
|
if conn_tx.send(id).is_err() {
|
|
424
481
|
break;
|
|
425
482
|
}
|
|
@@ -578,6 +635,22 @@ mod tests {
|
|
|
578
635
|
use std::thread;
|
|
579
636
|
use std::time::Duration;
|
|
580
637
|
|
|
638
|
+
/// The serve() HTTP→WS upgrade queue: `wsAccept` hands out enqueued conns FIFO and returns Null
|
|
639
|
+
/// when empty. (End-to-end upgrade over a live hyper port is covered by the native smoke test.)
|
|
640
|
+
#[test]
|
|
641
|
+
fn upgrade_queue_drains_then_empty() {
|
|
642
|
+
assert!(matches!(ws_serve_accept(&[Value::Number(0.0)]), Value::Null));
|
|
643
|
+
enqueue_upgraded_conn(4242);
|
|
644
|
+
match ws_serve_accept(&[Value::Number(0.0)]) {
|
|
645
|
+
Value::Object(o) => {
|
|
646
|
+
let b = o.borrow();
|
|
647
|
+
assert!(matches!(b.strings.get("_id"), Some(Value::Number(n)) if *n == 4242.0));
|
|
648
|
+
}
|
|
649
|
+
other => panic!("expected a conn object, got {:?}", other),
|
|
650
|
+
}
|
|
651
|
+
assert!(matches!(ws_serve_accept(&[Value::Number(0.0)]), Value::Null));
|
|
652
|
+
}
|
|
653
|
+
|
|
581
654
|
#[test]
|
|
582
655
|
fn ws_echo_roundtrip() {
|
|
583
656
|
let port: u16 = 18_742;
|
package/package.json
CHANGED
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|