@tishlang/tish-format 3.10.4 → 3.10.11
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/cargo_native_registry.rs +4 -0
- package/crates/tish/src/main.rs +65 -25
- package/crates/tish/tests/integration_test.rs +3 -0
- 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 +65 -25
- package/crates/tish_bytecode/tests/captured_param_slot_binding.rs +131 -0
- package/crates/tish_bytecode/tests/fundecl_slot_binding.rs +115 -0
- package/crates/tish_bytecode/tests/param_defaults_slot_binding.rs +86 -0
- package/crates/tish_core/src/shape.rs +55 -2
- package/crates/tish_core/src/value.rs +229 -13
- 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_pg/src/lib.rs +59 -8
- package/crates/tish_pg/src/statement_registry.rs +250 -0
- package/crates/tish_runtime/src/http.rs +19 -0
- package/crates/tish_runtime/src/http_fetch.rs +159 -65
- 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/promise.rs +521 -90
- package/crates/tish_runtime/src/promise_io.rs +29 -6
- 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 +8 -5
- package/crates/tish_vm/tests/captured_param_slot_mode.rs +210 -0
- package/crates/tish_vm/tests/captured_param_vm_slots_off.rs +64 -0
- package/crates/tish_vm/tests/fundecl_slot_no_frame_cycle.rs +152 -0
- package/crates/tish_vm/tests/param_defaults_slot_mode.rs +126 -0
- package/crates/tish_vm/tests/param_defaults_vm_slots_off.rs +44 -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
|
@@ -15,20 +15,43 @@ pub struct StringResultPromise {
|
|
|
15
15
|
pub(crate) rx: Mutex<Option<oneshot::Receiver<Result<String, String>>>>,
|
|
16
16
|
}
|
|
17
17
|
|
|
18
|
+
impl StringResultPromise {
|
|
19
|
+
fn convert(
|
|
20
|
+
r: Result<Result<String, String>, oneshot::error::RecvError>,
|
|
21
|
+
) -> std::result::Result<Value, Value> {
|
|
22
|
+
match r {
|
|
23
|
+
Ok(Ok(s)) => Ok(Value::String(s.into())),
|
|
24
|
+
Ok(Err(e)) => Err(error_value(e)),
|
|
25
|
+
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
18
30
|
impl TishPromise for StringResultPromise {
|
|
19
31
|
fn block_until_settled(&self) -> std::result::Result<Value, Value> {
|
|
20
32
|
let rx = self.rx.lock().unwrap().take();
|
|
21
33
|
if let Some(rx) = rx {
|
|
22
|
-
|
|
23
|
-
match result {
|
|
24
|
-
Ok(Ok(s)) => Ok(Value::String(s.into())),
|
|
25
|
-
Ok(Err(e)) => Err(error_value(e)),
|
|
26
|
-
Err(_) => Err(Value::String("Promise dropped".into())),
|
|
27
|
-
}
|
|
34
|
+
Self::convert(crate::http::block_on_http(rx))
|
|
28
35
|
} else {
|
|
29
36
|
Err(Value::String("Promise already consumed".into()))
|
|
30
37
|
}
|
|
31
38
|
}
|
|
39
|
+
|
|
40
|
+
/// Await the oneshot on the shared subscriber runtime instead of parking a
|
|
41
|
+
/// blocking thread (plus a per-call tokio runtime) per raced input (issue #702).
|
|
42
|
+
fn subscribe(
|
|
43
|
+
self: Arc<Self>,
|
|
44
|
+
on_settled: Box<dyn FnOnce(std::result::Result<Value, Value>) + Send>,
|
|
45
|
+
) {
|
|
46
|
+
match self.rx.lock().unwrap().take() {
|
|
47
|
+
Some(rx) => {
|
|
48
|
+
crate::http::subscriber_runtime().spawn(async move {
|
|
49
|
+
on_settled(Self::convert(rx.await));
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
None => on_settled(Err(Value::String("Promise already consumed".into()))),
|
|
53
|
+
}
|
|
54
|
+
}
|
|
32
55
|
}
|
|
33
56
|
|
|
34
57
|
pub fn string_result_promise(rx: oneshot::Receiver<Result<String, String>>) -> Value {
|
|
@@ -15,37 +15,100 @@
|
|
|
15
15
|
//! - `kill(id) -> bool`
|
|
16
16
|
//! - `pid(id) -> number | null`
|
|
17
17
|
//!
|
|
18
|
-
//! A per-session reader thread fills a byte buffer that `read` drains at a UTF-8
|
|
19
|
-
//! (
|
|
18
|
+
//! A per-session reader thread fills a bounded byte buffer that `read` drains at a UTF-8
|
|
19
|
+
//! boundary (see `stream_buf.rs` for the cap/backpressure and invalid-byte semantics). A global
|
|
20
20
|
//! `Mutex<HashMap<id, PtySession>>` registry mirrors `ws.rs`'s `CONNS`. Errors surface as
|
|
21
21
|
//! `null`/`false` rather than panicking.
|
|
22
|
+
//!
|
|
23
|
+
//! Session lifecycle mirrors `process_spawn.rs`: an entry lives until `kill(id)` or until it is
|
|
24
|
+
//! reaped — child exited AND the buffer fully drained — by an opportunistic sweep on each
|
|
25
|
+
//! `spawn` (plus a check when `read` returns `null` at EOF). The pid is tombstoned (bounded) so
|
|
26
|
+
//! a late `pid(id)` still answers, and `kill(id)` on a reaped id still reports `true` once.
|
|
22
27
|
|
|
23
|
-
use std::collections::HashMap;
|
|
24
|
-
use std::io::
|
|
28
|
+
use std::collections::{BTreeMap, HashMap};
|
|
29
|
+
use std::io::Write;
|
|
25
30
|
use std::sync::atomic::{AtomicU64, Ordering};
|
|
26
|
-
use std::sync::
|
|
27
|
-
use std::time::Duration;
|
|
31
|
+
use std::sync::Mutex;
|
|
28
32
|
|
|
29
33
|
use lazy_static::lazy_static;
|
|
30
34
|
use portable_pty::{native_pty_system, Child, CommandBuilder, MasterPty, PtySize};
|
|
31
35
|
use tishlang_core::Value;
|
|
32
36
|
|
|
33
|
-
|
|
34
|
-
struct PtyBuf {
|
|
35
|
-
data: Vec<u8>,
|
|
36
|
-
eof: bool,
|
|
37
|
-
}
|
|
37
|
+
use crate::stream_buf::{drain_stream, is_drained, new_buf, retire_buf, spawn_reader, SharedBuf};
|
|
38
38
|
|
|
39
39
|
struct PtySession {
|
|
40
40
|
child: Mutex<Box<dyn Child + Send + Sync>>,
|
|
41
41
|
master: Mutex<Box<dyn MasterPty + Send>>,
|
|
42
42
|
writer: Mutex<Box<dyn Write + Send>>,
|
|
43
|
-
buf:
|
|
43
|
+
buf: SharedBuf,
|
|
44
44
|
pid: Option<u32>,
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
lazy_static! {
|
|
48
48
|
static ref SESSIONS: Mutex<HashMap<u64, PtySession>> = Mutex::new(HashMap::new());
|
|
49
|
+
/// Pids of reaped sessions, so a late `pid(id)` still answers (bounded: ids are
|
|
50
|
+
/// monotonic, so evicting the smallest key evicts the oldest record).
|
|
51
|
+
static ref TOMBSTONES: Mutex<BTreeMap<u64, Option<u32>>> = Mutex::new(BTreeMap::new());
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const MAX_TOMBSTONES: usize = 1024;
|
|
55
|
+
|
|
56
|
+
fn remember_tombstone(id: u64, pid: Option<u32>) {
|
|
57
|
+
if let Ok(mut m) = TOMBSTONES.lock() {
|
|
58
|
+
m.insert(id, pid);
|
|
59
|
+
while m.len() > MAX_TOMBSTONES {
|
|
60
|
+
m.pop_first();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// Reap every session whose child has exited AND whose buffer is fully drained
|
|
66
|
+
/// (`try_wait` also reaps the OS zombie).
|
|
67
|
+
fn sweep_sessions() {
|
|
68
|
+
let mut reaped: Vec<(u64, Option<u32>)> = Vec::new();
|
|
69
|
+
{
|
|
70
|
+
let mut g = match SESSIONS.lock() {
|
|
71
|
+
Ok(g) => g,
|
|
72
|
+
Err(_) => return,
|
|
73
|
+
};
|
|
74
|
+
g.retain(|id, s| {
|
|
75
|
+
let exited = match s.child.lock() {
|
|
76
|
+
Ok(mut c) => c.try_wait().ok().flatten().is_some(),
|
|
77
|
+
Err(_) => false,
|
|
78
|
+
};
|
|
79
|
+
if exited && is_drained(&s.buf) {
|
|
80
|
+
reaped.push((*id, s.pid));
|
|
81
|
+
false
|
|
82
|
+
} else {
|
|
83
|
+
true
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
for (id, pid) in reaped {
|
|
88
|
+
remember_tombstone(id, pid);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/// Reap one session if (and only if) its child has exited and its buffer is drained.
|
|
93
|
+
fn try_reap(id: u64) {
|
|
94
|
+
let pid = {
|
|
95
|
+
let mut g = match SESSIONS.lock() {
|
|
96
|
+
Ok(g) => g,
|
|
97
|
+
Err(_) => return,
|
|
98
|
+
};
|
|
99
|
+
let Some(s) = g.get(&id) else { return };
|
|
100
|
+
let exited = match s.child.lock() {
|
|
101
|
+
Ok(mut c) => c.try_wait().ok().flatten().is_some(),
|
|
102
|
+
Err(_) => false,
|
|
103
|
+
};
|
|
104
|
+
if !(exited && is_drained(&s.buf)) {
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
let pid = s.pid;
|
|
108
|
+
g.remove(&id);
|
|
109
|
+
pid
|
|
110
|
+
};
|
|
111
|
+
remember_tombstone(id, pid);
|
|
49
112
|
}
|
|
50
113
|
|
|
51
114
|
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
|
@@ -92,6 +155,10 @@ fn obj_num(o: &Value, key: &str) -> Option<f64> {
|
|
|
92
155
|
/// `spawn({ program?, cwd?, cols?, rows?, env? })` → a stable id for read/write/resize/kill,
|
|
93
156
|
/// or `null` on failure.
|
|
94
157
|
pub fn pty_spawn(args: &[Value]) -> Value {
|
|
158
|
+
// Opportunistic reap of exited-and-drained sessions (a shell the user `exit`ed and whose
|
|
159
|
+
// output was fully read), so un-killed self-exiting ptys cannot accumulate without bound.
|
|
160
|
+
sweep_sessions();
|
|
161
|
+
|
|
95
162
|
let null = Value::Null;
|
|
96
163
|
let opts = args.first().unwrap_or(&null);
|
|
97
164
|
|
|
@@ -151,50 +218,10 @@ pub fn pty_spawn(args: &[Value]) -> Value {
|
|
|
151
218
|
};
|
|
152
219
|
let pid = child.process_id();
|
|
153
220
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}),
|
|
159
|
-
Condvar::new(),
|
|
160
|
-
));
|
|
161
|
-
|
|
162
|
-
// Reader thread: block on the master, append bytes, wake any waiting `read`. On EOF/error,
|
|
163
|
-
// mark eof so `read` can return null once the buffer drains.
|
|
164
|
-
{
|
|
165
|
-
let buf = buf.clone();
|
|
166
|
-
let mut reader = reader;
|
|
167
|
-
std::thread::spawn(move || {
|
|
168
|
-
let mut tmp = [0u8; 8192];
|
|
169
|
-
loop {
|
|
170
|
-
match reader.read(&mut tmp) {
|
|
171
|
-
Ok(0) => {
|
|
172
|
-
let (lock, cv) = &*buf;
|
|
173
|
-
if let Ok(mut b) = lock.lock() {
|
|
174
|
-
b.eof = true;
|
|
175
|
-
}
|
|
176
|
-
cv.notify_all();
|
|
177
|
-
break;
|
|
178
|
-
}
|
|
179
|
-
Ok(n) => {
|
|
180
|
-
let (lock, cv) = &*buf;
|
|
181
|
-
if let Ok(mut b) = lock.lock() {
|
|
182
|
-
b.data.extend_from_slice(&tmp[..n]);
|
|
183
|
-
}
|
|
184
|
-
cv.notify_all();
|
|
185
|
-
}
|
|
186
|
-
Err(_) => {
|
|
187
|
-
let (lock, cv) = &*buf;
|
|
188
|
-
if let Ok(mut b) = lock.lock() {
|
|
189
|
-
b.eof = true;
|
|
190
|
-
}
|
|
191
|
-
cv.notify_all();
|
|
192
|
-
break;
|
|
193
|
-
}
|
|
194
|
-
}
|
|
195
|
-
}
|
|
196
|
-
});
|
|
197
|
-
}
|
|
221
|
+
// Reader thread: block on the master, append bytes (bounded, see stream_buf.rs), wake any
|
|
222
|
+
// waiting `read`. On EOF/error, mark eof so `read` can return null once the buffer drains.
|
|
223
|
+
let buf = new_buf();
|
|
224
|
+
spawn_reader(reader, buf.clone());
|
|
198
225
|
|
|
199
226
|
let id = NEXT_ID.fetch_add(1, Ordering::SeqCst);
|
|
200
227
|
let session = PtySession {
|
|
@@ -214,8 +241,8 @@ pub fn pty_spawn(args: &[Value]) -> Value {
|
|
|
214
241
|
}
|
|
215
242
|
|
|
216
243
|
/// `read(id, timeoutMs?)` → available output as a string (possibly `""` if none arrived within the
|
|
217
|
-
/// timeout), or `null` at EOF / for an unknown id. Drains only
|
|
218
|
-
/// incomplete trailing multibyte
|
|
244
|
+
/// timeout), or `null` at EOF / for an unknown id. Drains only complete UTF-8, holding an
|
|
245
|
+
/// incomplete trailing multibyte sequence for the next call (invalid bytes become U+FFFD).
|
|
219
246
|
pub fn pty_read(args: &[Value]) -> Value {
|
|
220
247
|
let id = match arg_u64(args, 0) {
|
|
221
248
|
Some(x) => x,
|
|
@@ -237,46 +264,13 @@ pub fn pty_read(args: &[Value]) -> Value {
|
|
|
237
264
|
}
|
|
238
265
|
};
|
|
239
266
|
|
|
240
|
-
let (
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
if b.data.is_empty() && !b.eof && timeout_ms > 0 {
|
|
246
|
-
let res = cv.wait_timeout_while(b, Duration::from_millis(timeout_ms), |b| {
|
|
247
|
-
b.data.is_empty() && !b.eof
|
|
248
|
-
});
|
|
249
|
-
b = match res {
|
|
250
|
-
Ok((g, _)) => g,
|
|
251
|
-
Err(e) => e.into_inner().0,
|
|
252
|
-
};
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
if b.data.is_empty() {
|
|
256
|
-
if b.eof {
|
|
257
|
-
return Value::Null;
|
|
258
|
-
}
|
|
259
|
-
return Value::String("".into());
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// Drain up to the last complete UTF-8 code point; keep the incomplete tail for next time.
|
|
263
|
-
let valid = match std::str::from_utf8(&b.data) {
|
|
264
|
-
Ok(_) => b.data.len(),
|
|
265
|
-
Err(e) => e.valid_up_to(),
|
|
266
|
-
};
|
|
267
|
-
if valid == 0 {
|
|
268
|
-
// A leading incomplete multibyte sequence. If the stream is done, flush it lossily so
|
|
269
|
-
// nothing is stranded; otherwise wait for the rest.
|
|
270
|
-
if b.eof {
|
|
271
|
-
let s = String::from_utf8_lossy(&b.data).into_owned();
|
|
272
|
-
b.data.clear();
|
|
273
|
-
return Value::String(s.into());
|
|
274
|
-
}
|
|
275
|
-
return Value::String("".into());
|
|
267
|
+
let out = drain_stream(&buf, timeout_ms);
|
|
268
|
+
if matches!(out, Value::Null) {
|
|
269
|
+
// EOF fully drained: if the child has also exited, the session is fully consumed —
|
|
270
|
+
// reap it (a pid tombstone keeps `pid(id)` answering).
|
|
271
|
+
try_reap(id);
|
|
276
272
|
}
|
|
277
|
-
|
|
278
|
-
let s = String::from_utf8(out).unwrap_or_default();
|
|
279
|
-
Value::String(s.into())
|
|
273
|
+
out
|
|
280
274
|
}
|
|
281
275
|
|
|
282
276
|
/// `write(id, data)` → feed input bytes to the PTY. Returns whether the write succeeded.
|
|
@@ -345,7 +339,9 @@ pub fn pty_resize(args: &[Value]) -> Value {
|
|
|
345
339
|
}
|
|
346
340
|
}
|
|
347
341
|
|
|
348
|
-
/// `kill(id)` → terminate the child and drop the session. Returns whether the id was live
|
|
342
|
+
/// `kill(id)` → terminate the child and drop the session. Returns whether the id was live
|
|
343
|
+
/// (including a reaped id, whose child already exited — reported `true` once, like before
|
|
344
|
+
/// reaping existed).
|
|
349
345
|
pub fn pty_kill(args: &[Value]) -> Value {
|
|
350
346
|
let id = match arg_u64(args, 0) {
|
|
351
347
|
Some(x) => x,
|
|
@@ -364,25 +360,46 @@ pub fn pty_kill(args: &[Value]) -> Value {
|
|
|
364
360
|
let _ = c.kill();
|
|
365
361
|
let _ = c.wait();
|
|
366
362
|
}
|
|
363
|
+
// Wake a reader parked at the buffer cap so its thread exits (a read-blocked
|
|
364
|
+
// reader exits on its own once the killed child's pty closes).
|
|
365
|
+
retire_buf(&s.buf);
|
|
367
366
|
Value::Bool(true)
|
|
368
367
|
}
|
|
369
|
-
None =>
|
|
368
|
+
None => {
|
|
369
|
+
// A reaped session was live from the caller's perspective; consume its tombstone.
|
|
370
|
+
match TOMBSTONES.lock() {
|
|
371
|
+
Ok(mut t) => Value::Bool(t.remove(&id).is_some()),
|
|
372
|
+
Err(_) => Value::Bool(false),
|
|
373
|
+
}
|
|
374
|
+
}
|
|
370
375
|
}
|
|
371
376
|
}
|
|
372
377
|
|
|
373
|
-
/// `pid(id)` → the child process id, or `null` for an unknown id / no pid.
|
|
378
|
+
/// `pid(id)` → the child process id, or `null` for an unknown id / no pid. Still answers after
|
|
379
|
+
/// the session was reaped (the pid is tombstoned).
|
|
374
380
|
pub fn pty_pid(args: &[Value]) -> Value {
|
|
375
381
|
let id = match arg_u64(args, 0) {
|
|
376
382
|
Some(x) => x,
|
|
377
383
|
None => return Value::Null,
|
|
378
384
|
};
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
Some(
|
|
385
|
-
|
|
385
|
+
{
|
|
386
|
+
let g = match SESSIONS.lock() {
|
|
387
|
+
Ok(g) => g,
|
|
388
|
+
Err(_) => return Value::Null,
|
|
389
|
+
};
|
|
390
|
+
if let Some(s) = g.get(&id) {
|
|
391
|
+
return match s.pid {
|
|
392
|
+
Some(p) => Value::Number(p as f64),
|
|
393
|
+
None => Value::Null,
|
|
394
|
+
};
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
match TOMBSTONES.lock() {
|
|
398
|
+
Ok(t) => match t.get(&id).copied().flatten() {
|
|
399
|
+
Some(p) => Value::Number(p as f64),
|
|
400
|
+
None => Value::Null,
|
|
401
|
+
},
|
|
402
|
+
Err(_) => Value::Null,
|
|
386
403
|
}
|
|
387
404
|
}
|
|
388
405
|
|
|
@@ -433,6 +450,43 @@ mod tests {
|
|
|
433
450
|
assert!(matches!(pty_kill(&[Value::Number(id)]), Value::Bool(true)));
|
|
434
451
|
}
|
|
435
452
|
|
|
453
|
+
#[test]
|
|
454
|
+
fn self_exited_session_is_reaped_after_drain_with_pid_tombstoned() {
|
|
455
|
+
// `true` exits immediately on its own — the un-killed lifecycle that used to strand
|
|
456
|
+
// the session (2 fds + an unreaped child) forever.
|
|
457
|
+
let mut m = tishlang_core::ObjectMap::default();
|
|
458
|
+
m.insert(
|
|
459
|
+
std::sync::Arc::from("program"),
|
|
460
|
+
Value::String("true".into()),
|
|
461
|
+
);
|
|
462
|
+
let id = match pty_spawn(&[Value::object(m)]) {
|
|
463
|
+
Value::Number(n) => n,
|
|
464
|
+
other => panic!("spawn failed: {:?}", other),
|
|
465
|
+
};
|
|
466
|
+
// Drain to EOF; the null read reaps the exited session.
|
|
467
|
+
let mut saw_eof = false;
|
|
468
|
+
for _ in 0..200 {
|
|
469
|
+
if matches!(
|
|
470
|
+
pty_read(&[Value::Number(id), Value::Number(50.0)]),
|
|
471
|
+
Value::Null
|
|
472
|
+
) {
|
|
473
|
+
saw_eof = true;
|
|
474
|
+
break;
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
assert!(saw_eof, "pty never reached EOF");
|
|
478
|
+
assert!(
|
|
479
|
+
!SESSIONS.lock().unwrap().contains_key(&(id as u64)),
|
|
480
|
+
"session not reaped after exit + full drain"
|
|
481
|
+
);
|
|
482
|
+
// The pid still answers from the tombstone; kill reports the id as (once) live;
|
|
483
|
+
// reads keep returning null (the read-to-EOF-then-read-again contract).
|
|
484
|
+
assert!(matches!(pty_read(&[Value::Number(id)]), Value::Null));
|
|
485
|
+
assert!(matches!(pty_pid(&[Value::Number(id)]), Value::Number(_)));
|
|
486
|
+
assert!(matches!(pty_kill(&[Value::Number(id)]), Value::Bool(true)));
|
|
487
|
+
assert!(matches!(pty_kill(&[Value::Number(id)]), Value::Bool(false)));
|
|
488
|
+
}
|
|
489
|
+
|
|
436
490
|
#[test]
|
|
437
491
|
fn unknown_id_surfaces_null_or_false() {
|
|
438
492
|
let bad = 9_999_999.0;
|