@tishlang/tish-lsp 3.0.0 → 3.1.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/tests/scope_merge.rs +31 -0
- package/crates/tish_build_utils/src/lib.rs +131 -6
- package/crates/tish_compile/src/codegen.rs +213 -16
- package/crates/tish_compile/src/resolve.rs +12 -2
- package/crates/tish_core/src/shape.rs +28 -3
- package/crates/tish_lsp/Cargo.toml +1 -1
- package/crates/tish_native/src/build.rs +4 -0
- package/crates/tish_runtime/Cargo.toml +20 -1
- package/crates/tish_runtime/src/crypto.rs +127 -0
- package/crates/tish_runtime/src/encoding.rs +157 -0
- package/crates/tish_runtime/src/fs_ext.rs +27 -0
- package/crates/tish_runtime/src/lib.rs +32 -0
- package/crates/tish_runtime/src/net.rs +482 -0
- package/crates/tish_runtime/src/process_spawn.rs +508 -0
- package/crates/tish_runtime/src/ws.rs +206 -23
- package/crates/tish_runtime/src/zip.rs +196 -0
- 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
|
@@ -0,0 +1,482 @@
|
|
|
1
|
+
//! Raw TCP sockets for Tish (`tish:net`), behind the `net` feature.
|
|
2
|
+
//!
|
|
3
|
+
//! A client (`connect`) and a server (`listen`/`accept`) over plain TCP — the layer beneath
|
|
4
|
+
//! `tish:http` (HTTP) and `tish:ws` (WebSocket). Needed for protocols that ride a bare socket: the
|
|
5
|
+
//! Debug Adapter Protocol's TCP transports (a spawned adapter that binds a port; js-debug's reverse
|
|
6
|
+
//! `startDebugging` connect-back), an OAuth loopback callback listener, and connect probes (is a
|
|
7
|
+
//! forwarded port up yet?).
|
|
8
|
+
//!
|
|
9
|
+
//! import { connect, read, write, close, listen, accept, probe, sleep } from 'tish:net'
|
|
10
|
+
//!
|
|
11
|
+
//! - `connect(host, port, timeoutMs?) -> id | null`
|
|
12
|
+
//! - `read(id, timeoutMs?) -> string | null` ("" = live but no data yet; null = EOF/unknown)
|
|
13
|
+
//! - `write(id, data) -> bool`
|
|
14
|
+
//! - `close(id) -> bool`
|
|
15
|
+
//! - `listen(port, host?) -> { id, port } | null` (host defaults 127.0.0.1; port 0 = OS-assigned,
|
|
16
|
+
//! returned so a loopback caller can learn it)
|
|
17
|
+
//! - `accept(listenerId, timeoutMs?) -> id | null` (a new connection id, or null on timeout)
|
|
18
|
+
//! - `probe(host, port, timeoutMs?) -> bool` (can we connect?)
|
|
19
|
+
//! - `sleep(ms) -> null` (blocking backoff for the poll loops above)
|
|
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
|
|
23
|
+
//! null/false.
|
|
24
|
+
|
|
25
|
+
use std::collections::HashMap;
|
|
26
|
+
use std::io::{ErrorKind, Read, Write};
|
|
27
|
+
use std::net::{TcpListener, TcpStream, ToSocketAddrs};
|
|
28
|
+
use std::sync::atomic::{AtomicU64, Ordering};
|
|
29
|
+
use std::sync::{Arc, Condvar, Mutex, OnceLock};
|
|
30
|
+
use std::time::{Duration, Instant};
|
|
31
|
+
|
|
32
|
+
use tishlang_core::{ObjectMap, Value};
|
|
33
|
+
|
|
34
|
+
struct StreamBuf {
|
|
35
|
+
data: Vec<u8>,
|
|
36
|
+
eof: bool,
|
|
37
|
+
}
|
|
38
|
+
type SharedBuf = Arc<(Mutex<StreamBuf>, Condvar)>;
|
|
39
|
+
|
|
40
|
+
struct Conn {
|
|
41
|
+
writer: Mutex<TcpStream>,
|
|
42
|
+
buf: SharedBuf,
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
fn conns() -> &'static Mutex<HashMap<u64, Conn>> {
|
|
46
|
+
static C: OnceLock<Mutex<HashMap<u64, Conn>>> = OnceLock::new();
|
|
47
|
+
C.get_or_init(|| Mutex::new(HashMap::new()))
|
|
48
|
+
}
|
|
49
|
+
fn listeners() -> &'static Mutex<HashMap<u64, TcpListener>> {
|
|
50
|
+
static L: OnceLock<Mutex<HashMap<u64, TcpListener>>> = OnceLock::new();
|
|
51
|
+
L.get_or_init(|| Mutex::new(HashMap::new()))
|
|
52
|
+
}
|
|
53
|
+
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
|
54
|
+
|
|
55
|
+
// --- Value helpers (mirror process_spawn.rs) ---
|
|
56
|
+
|
|
57
|
+
fn arg_u64(args: &[Value], i: usize) -> Option<u64> {
|
|
58
|
+
match args.get(i) {
|
|
59
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => Some(*n as u64),
|
|
60
|
+
_ => None,
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
fn arg_u16(args: &[Value], i: usize) -> Option<u16> {
|
|
65
|
+
match args.get(i) {
|
|
66
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 && *n <= 65535.0 => Some(*n as u16),
|
|
67
|
+
_ => None,
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
fn arg_str(args: &[Value], i: usize) -> Option<String> {
|
|
72
|
+
match args.get(i) {
|
|
73
|
+
Some(Value::String(s)) => Some(s.to_string()),
|
|
74
|
+
_ => None,
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
fn arg_timeout(args: &[Value], i: usize) -> u64 {
|
|
79
|
+
match args.get(i) {
|
|
80
|
+
Some(Value::Number(n)) if n.is_finite() && *n >= 0.0 => *n as u64,
|
|
81
|
+
_ => 0,
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
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.
|
|
167
|
+
fn register_conn(stream: TcpStream) -> Option<u64> {
|
|
168
|
+
let reader = stream.try_clone().ok()?;
|
|
169
|
+
let _ = reader.set_nonblocking(false);
|
|
170
|
+
let buf = new_buf();
|
|
171
|
+
spawn_reader(reader, buf.clone());
|
|
172
|
+
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
|
|
173
|
+
conns().lock().ok()?.insert(
|
|
174
|
+
id,
|
|
175
|
+
Conn {
|
|
176
|
+
writer: Mutex::new(stream),
|
|
177
|
+
buf,
|
|
178
|
+
},
|
|
179
|
+
);
|
|
180
|
+
Some(id)
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/// `connect(host, port, timeoutMs?)` → a connection id, or `null`.
|
|
184
|
+
pub fn net_connect(args: &[Value]) -> Value {
|
|
185
|
+
let host = match arg_str(args, 0) {
|
|
186
|
+
Some(h) => h,
|
|
187
|
+
None => return Value::Null,
|
|
188
|
+
};
|
|
189
|
+
let port = match arg_u16(args, 1) {
|
|
190
|
+
Some(p) => p,
|
|
191
|
+
None => return Value::Null,
|
|
192
|
+
};
|
|
193
|
+
let timeout = arg_timeout(args, 2);
|
|
194
|
+
let mut addrs = match (host.as_str(), port).to_socket_addrs() {
|
|
195
|
+
Ok(a) => a,
|
|
196
|
+
Err(_) => return Value::Null,
|
|
197
|
+
};
|
|
198
|
+
let addr = match addrs.next() {
|
|
199
|
+
Some(a) => a,
|
|
200
|
+
None => return Value::Null,
|
|
201
|
+
};
|
|
202
|
+
let stream = if timeout > 0 {
|
|
203
|
+
TcpStream::connect_timeout(&addr, Duration::from_millis(timeout))
|
|
204
|
+
} else {
|
|
205
|
+
TcpStream::connect(addr)
|
|
206
|
+
};
|
|
207
|
+
match stream {
|
|
208
|
+
Ok(s) => match register_conn(s) {
|
|
209
|
+
Some(id) => Value::Number(id as f64),
|
|
210
|
+
None => Value::Null,
|
|
211
|
+
},
|
|
212
|
+
Err(_) => Value::Null,
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/// `read(id, timeoutMs?)` → available bytes as a string, `""`, or `null` at EOF/unknown.
|
|
217
|
+
pub fn net_read(args: &[Value]) -> Value {
|
|
218
|
+
let id = match arg_u64(args, 0) {
|
|
219
|
+
Some(x) => x,
|
|
220
|
+
None => return Value::Null,
|
|
221
|
+
};
|
|
222
|
+
let timeout_ms = arg_timeout(args, 1);
|
|
223
|
+
let buf = {
|
|
224
|
+
let g = match conns().lock() {
|
|
225
|
+
Ok(g) => g,
|
|
226
|
+
Err(_) => return Value::Null,
|
|
227
|
+
};
|
|
228
|
+
match g.get(&id) {
|
|
229
|
+
Some(c) => c.buf.clone(),
|
|
230
|
+
None => return Value::Null,
|
|
231
|
+
}
|
|
232
|
+
};
|
|
233
|
+
drain_stream(&buf, timeout_ms)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/// `write(id, data)` → returns whether the write succeeded.
|
|
237
|
+
pub fn net_write(args: &[Value]) -> Value {
|
|
238
|
+
let id = match arg_u64(args, 0) {
|
|
239
|
+
Some(x) => x,
|
|
240
|
+
None => return Value::Bool(false),
|
|
241
|
+
};
|
|
242
|
+
let data = match args.get(1) {
|
|
243
|
+
Some(Value::String(s)) => s.to_string(),
|
|
244
|
+
Some(v) => v.to_display_string(),
|
|
245
|
+
None => return Value::Bool(false),
|
|
246
|
+
};
|
|
247
|
+
let g = match conns().lock() {
|
|
248
|
+
Ok(g) => g,
|
|
249
|
+
Err(_) => return Value::Bool(false),
|
|
250
|
+
};
|
|
251
|
+
let c = match g.get(&id) {
|
|
252
|
+
Some(c) => c,
|
|
253
|
+
None => return Value::Bool(false),
|
|
254
|
+
};
|
|
255
|
+
let mut w = match c.writer.lock() {
|
|
256
|
+
Ok(w) => w,
|
|
257
|
+
Err(_) => return Value::Bool(false),
|
|
258
|
+
};
|
|
259
|
+
match w.write_all(data.as_bytes()).and_then(|_| w.flush()) {
|
|
260
|
+
Ok(_) => Value::Bool(true),
|
|
261
|
+
Err(_) => Value::Bool(false),
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
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
|
|
268
|
+
/// bind it. Returns whether anything live was removed.
|
|
269
|
+
pub fn net_close(args: &[Value]) -> Value {
|
|
270
|
+
let id = match arg_u64(args, 0) {
|
|
271
|
+
Some(x) => x,
|
|
272
|
+
None => return Value::Bool(false),
|
|
273
|
+
};
|
|
274
|
+
let removed_conn = match conns().lock() {
|
|
275
|
+
Ok(mut g) => g.remove(&id).is_some(),
|
|
276
|
+
Err(_) => return Value::Bool(false),
|
|
277
|
+
};
|
|
278
|
+
if removed_conn {
|
|
279
|
+
return Value::Bool(true);
|
|
280
|
+
}
|
|
281
|
+
let removed_listener = match listeners().lock() {
|
|
282
|
+
Ok(mut g) => g.remove(&id).is_some(),
|
|
283
|
+
Err(_) => return Value::Bool(false),
|
|
284
|
+
};
|
|
285
|
+
Value::Bool(removed_listener)
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/// `listen(port, host?)` → `{ id, port }` (the actual bound port, so port-0 loopback callers can
|
|
289
|
+
/// learn the OS-assigned one), or `null`.
|
|
290
|
+
pub fn net_listen(args: &[Value]) -> Value {
|
|
291
|
+
let port = match arg_u16(args, 0) {
|
|
292
|
+
Some(p) => p,
|
|
293
|
+
None => return Value::Null,
|
|
294
|
+
};
|
|
295
|
+
let host = arg_str(args, 1).unwrap_or_else(|| "127.0.0.1".to_string());
|
|
296
|
+
let listener = match TcpListener::bind((host.as_str(), port)) {
|
|
297
|
+
Ok(l) => l,
|
|
298
|
+
Err(_) => return Value::Null,
|
|
299
|
+
};
|
|
300
|
+
let bound = match listener.local_addr() {
|
|
301
|
+
Ok(a) => a.port(),
|
|
302
|
+
Err(_) => port,
|
|
303
|
+
};
|
|
304
|
+
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
|
|
305
|
+
match listeners().lock() {
|
|
306
|
+
Ok(mut g) => {
|
|
307
|
+
g.insert(id, listener);
|
|
308
|
+
}
|
|
309
|
+
Err(_) => return Value::Null,
|
|
310
|
+
}
|
|
311
|
+
let mut m = ObjectMap::default();
|
|
312
|
+
m.insert(std::sync::Arc::from("id"), Value::Number(id as f64));
|
|
313
|
+
m.insert(std::sync::Arc::from("port"), Value::Number(bound as f64));
|
|
314
|
+
Value::object(m)
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/// `accept(listenerId, timeoutMs?)` → a new connection id, or `null` on timeout/unknown.
|
|
318
|
+
pub fn net_accept(args: &[Value]) -> Value {
|
|
319
|
+
let lid = match arg_u64(args, 0) {
|
|
320
|
+
Some(x) => x,
|
|
321
|
+
None => return Value::Null,
|
|
322
|
+
};
|
|
323
|
+
let timeout = arg_timeout(args, 1);
|
|
324
|
+
// Clone the listener so we don't hold the registry lock across the poll.
|
|
325
|
+
let listener = {
|
|
326
|
+
let g = match listeners().lock() {
|
|
327
|
+
Ok(g) => g,
|
|
328
|
+
Err(_) => return Value::Null,
|
|
329
|
+
};
|
|
330
|
+
match g.get(&lid).and_then(|l| l.try_clone().ok()) {
|
|
331
|
+
Some(l) => l,
|
|
332
|
+
None => return Value::Null,
|
|
333
|
+
}
|
|
334
|
+
};
|
|
335
|
+
if listener.set_nonblocking(true).is_err() {
|
|
336
|
+
return Value::Null;
|
|
337
|
+
}
|
|
338
|
+
let deadline = Instant::now() + Duration::from_millis(timeout.max(1));
|
|
339
|
+
loop {
|
|
340
|
+
match listener.accept() {
|
|
341
|
+
Ok((stream, _)) => {
|
|
342
|
+
let _ = stream.set_nonblocking(false);
|
|
343
|
+
return match register_conn(stream) {
|
|
344
|
+
Some(id) => Value::Number(id as f64),
|
|
345
|
+
None => Value::Null,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
Err(e) if e.kind() == ErrorKind::WouldBlock => {
|
|
349
|
+
if Instant::now() >= deadline {
|
|
350
|
+
return Value::Null;
|
|
351
|
+
}
|
|
352
|
+
std::thread::sleep(Duration::from_millis(10));
|
|
353
|
+
}
|
|
354
|
+
Err(_) => return Value::Null,
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/// `probe(host, port, timeoutMs?)` → whether a TCP connection can be established (then dropped).
|
|
360
|
+
pub fn net_probe(args: &[Value]) -> Value {
|
|
361
|
+
let host = match arg_str(args, 0) {
|
|
362
|
+
Some(h) => h,
|
|
363
|
+
None => return Value::Bool(false),
|
|
364
|
+
};
|
|
365
|
+
let port = match arg_u16(args, 1) {
|
|
366
|
+
Some(p) => p,
|
|
367
|
+
None => return Value::Bool(false),
|
|
368
|
+
};
|
|
369
|
+
let timeout = arg_timeout(args, 2);
|
|
370
|
+
let mut addrs = match (host.as_str(), port).to_socket_addrs() {
|
|
371
|
+
Ok(a) => a,
|
|
372
|
+
Err(_) => return Value::Bool(false),
|
|
373
|
+
};
|
|
374
|
+
let addr = match addrs.next() {
|
|
375
|
+
Some(a) => a,
|
|
376
|
+
None => return Value::Bool(false),
|
|
377
|
+
};
|
|
378
|
+
let d = if timeout > 0 { timeout } else { 2000 };
|
|
379
|
+
match TcpStream::connect_timeout(&addr, Duration::from_millis(d)) {
|
|
380
|
+
Ok(_) => Value::Bool(true),
|
|
381
|
+
Err(_) => Value::Bool(false),
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/// `sleep(ms)` → block the current OS thread for `ms` milliseconds (draining due timers first),
|
|
386
|
+
/// then return null. A blocking backoff for the socket poll loops that live here — a TCP DAP
|
|
387
|
+
/// adapter needs a moment to bind before `connect` succeeds, and `accept` pollers pace between
|
|
388
|
+
/// tries. Colocated with the socket ops because that's what needs it; unlike `setTimeout` (which
|
|
389
|
+
/// only fires when a blocking op happens to yield), this actually sleeps, so it is usable from a
|
|
390
|
+
/// `Promise.spawn` pump thread. Capped at 60s so a bad argument can't wedge the thread.
|
|
391
|
+
pub fn net_sleep(args: &[Value]) -> Value {
|
|
392
|
+
let ms = arg_timeout(args, 0).min(60_000);
|
|
393
|
+
if ms > 0 {
|
|
394
|
+
crate::timers::sleep_with_drain(ms);
|
|
395
|
+
}
|
|
396
|
+
Value::Null
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
#[cfg(test)]
|
|
400
|
+
mod tests {
|
|
401
|
+
use super::*;
|
|
402
|
+
|
|
403
|
+
#[test]
|
|
404
|
+
fn listen_connect_roundtrip() {
|
|
405
|
+
// Bind an ephemeral loopback listener, connect a client, exchange bytes both ways.
|
|
406
|
+
let l = net_listen(&[Value::Number(0.0)]);
|
|
407
|
+
let port = match &l {
|
|
408
|
+
Value::Object(m) => match m.borrow().strings.get("port") {
|
|
409
|
+
Some(Value::Number(p)) => *p,
|
|
410
|
+
other => panic!("no port: {:?}", other),
|
|
411
|
+
},
|
|
412
|
+
other => panic!("listen failed: {:?}", other),
|
|
413
|
+
};
|
|
414
|
+
let lid = match &l {
|
|
415
|
+
Value::Object(m) => match m.borrow().strings.get("id") {
|
|
416
|
+
Some(Value::Number(i)) => *i,
|
|
417
|
+
_ => panic!("no id"),
|
|
418
|
+
},
|
|
419
|
+
_ => panic!("no obj"),
|
|
420
|
+
};
|
|
421
|
+
|
|
422
|
+
let client = match net_connect(&[Value::String("127.0.0.1".into()), Value::Number(port)]) {
|
|
423
|
+
Value::Number(n) => n,
|
|
424
|
+
other => panic!("connect failed: {:?}", other),
|
|
425
|
+
};
|
|
426
|
+
let server = match net_accept(&[Value::Number(lid), Value::Number(1000.0)]) {
|
|
427
|
+
Value::Number(n) => n,
|
|
428
|
+
other => panic!("accept failed/timed out: {:?}", other),
|
|
429
|
+
};
|
|
430
|
+
|
|
431
|
+
// client -> server
|
|
432
|
+
assert!(matches!(
|
|
433
|
+
net_write(&[Value::Number(client), Value::String("ping\n".into())]),
|
|
434
|
+
Value::Bool(true)
|
|
435
|
+
));
|
|
436
|
+
let mut got = String::new();
|
|
437
|
+
for _ in 0..100 {
|
|
438
|
+
match net_read(&[Value::Number(server), Value::Number(50.0)]) {
|
|
439
|
+
Value::String(s) => got.push_str(&s),
|
|
440
|
+
Value::Null => break,
|
|
441
|
+
_ => {}
|
|
442
|
+
}
|
|
443
|
+
if got.contains("ping") {
|
|
444
|
+
break;
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
assert!(got.contains("ping"), "server did not receive: {:?}", got);
|
|
448
|
+
|
|
449
|
+
// server -> client
|
|
450
|
+
assert!(matches!(
|
|
451
|
+
net_write(&[Value::Number(server), Value::String("pong\n".into())]),
|
|
452
|
+
Value::Bool(true)
|
|
453
|
+
));
|
|
454
|
+
let mut back = String::new();
|
|
455
|
+
for _ in 0..100 {
|
|
456
|
+
match net_read(&[Value::Number(client), Value::Number(50.0)]) {
|
|
457
|
+
Value::String(s) => back.push_str(&s),
|
|
458
|
+
Value::Null => break,
|
|
459
|
+
_ => {}
|
|
460
|
+
}
|
|
461
|
+
if back.contains("pong") {
|
|
462
|
+
break;
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
assert!(back.contains("pong"), "client did not receive: {:?}", back);
|
|
466
|
+
|
|
467
|
+
assert!(matches!(net_close(&[Value::Number(client)]), Value::Bool(true)));
|
|
468
|
+
assert!(matches!(net_close(&[Value::Number(server)]), Value::Bool(true)));
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
#[test]
|
|
472
|
+
fn probe_and_unknown() {
|
|
473
|
+
// Nothing listening on this port -> probe false.
|
|
474
|
+
assert!(matches!(
|
|
475
|
+
net_probe(&[Value::String("127.0.0.1".into()), Value::Number(1.0), Value::Number(200.0)]),
|
|
476
|
+
Value::Bool(false)
|
|
477
|
+
));
|
|
478
|
+
assert!(matches!(net_read(&[Value::Number(9_999_999.0)]), Value::Null));
|
|
479
|
+
assert!(matches!(net_write(&[Value::Number(9_999_999.0), Value::String("x".into())]), Value::Bool(false)));
|
|
480
|
+
assert!(matches!(net_close(&[Value::Number(9_999_999.0)]), Value::Bool(false)));
|
|
481
|
+
}
|
|
482
|
+
}
|