wstp-node 0.6.1 → 0.6.3
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/README.md +1 -1
- package/build/Release/wstp.node +0 -0
- package/index.d.ts +9 -0
- package/package.json +1 -1
- package/src/addon.cc +113 -13
- package/test.js +194 -37
package/README.md
CHANGED
|
@@ -98,7 +98,7 @@ The script automatically locates the WSTP SDK inside the default Wolfram install
|
|
|
98
98
|
node test.js
|
|
99
99
|
```
|
|
100
100
|
|
|
101
|
-
Expected last line: `All
|
|
101
|
+
Expected last line: `All 61 tests passed.`
|
|
102
102
|
|
|
103
103
|
A more comprehensive suite (both modes + In/Out + comparison) lives in `tmp/tests_all.js`:
|
|
104
104
|
|
package/build/Release/wstp.node
CHANGED
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -429,3 +429,12 @@ export class WstpReader {
|
|
|
429
429
|
* or `null` / `undefined` to clear a previously-set handler.
|
|
430
430
|
*/
|
|
431
431
|
export function setDiagHandler(fn: ((msg: string) => void) | null | undefined): void;
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Addon version string — mirrors the `version` field in `package.json`.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* const { version } = require('./build/Release/wstp.node');
|
|
438
|
+
* console.log(version); // "0.6.2"
|
|
439
|
+
*/
|
|
440
|
+
export declare const version: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wstp-node",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.3",
|
|
4
4
|
"description": "Native Node.js addon for Wolfram/Mathematica WSTP — kernel sessions with evaluation queue, streaming Print/messages, Dialog subsessions, and side-channel WstpReader",
|
|
5
5
|
"main": "build/Release/wstp.node",
|
|
6
6
|
"types": "index.d.ts",
|
package/src/addon.cc
CHANGED
|
@@ -882,13 +882,18 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
882
882
|
// Drain any stale packets (e.g. late BEGINDLGPKT from a concurrent
|
|
883
883
|
// interrupt that arrived just as this cell's RETURNPKT was sent).
|
|
884
884
|
// This prevents Pattern D: stale BEGINDLGPKT corrupting next eval.
|
|
885
|
-
|
|
886
|
-
// In interactive mode the kernel
|
|
887
|
-
// trailing INPUTNAMEPKT (
|
|
888
|
-
//
|
|
889
|
-
//
|
|
890
|
-
|
|
891
|
-
//
|
|
885
|
+
//
|
|
886
|
+
// IMPORTANT: In interactive mode (EnterExpressionPacket), the kernel
|
|
887
|
+
// follows RETURNEXPRPKT with a trailing INPUTNAMEPKT ("In[n+1]:=").
|
|
888
|
+
// drainStalePackets must NOT run here because it would consume that
|
|
889
|
+
// INPUTNAMEPKT and discard it, causing the outer loop to block forever
|
|
890
|
+
// waiting for a packet that was already eaten. Drain is deferred to
|
|
891
|
+
// the INPUTNAMEPKT handler below.
|
|
892
|
+
if (!opts || !opts->interactive) {
|
|
893
|
+
if (!r.aborted) drainStalePackets(lp, opts);
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
// Interactive mode: fall through to consume trailing INPUTNAMEPKT.
|
|
892
897
|
}
|
|
893
898
|
else if (pkt == INPUTNAMEPKT) {
|
|
894
899
|
const char* s = nullptr; WSGetString(lp, &s);
|
|
@@ -906,7 +911,9 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
906
911
|
break;
|
|
907
912
|
}
|
|
908
913
|
// Trailing INPUTNAMEPKT after RETURNEXPRPKT — consume and exit.
|
|
909
|
-
//
|
|
914
|
+
// Now safe to drain stale packets (Pattern D prevention) —
|
|
915
|
+
// the trailing INPUTNAMEPKT has already been consumed above.
|
|
916
|
+
if (!r.aborted) drainStalePackets(lp, opts);
|
|
910
917
|
break;
|
|
911
918
|
}
|
|
912
919
|
r.cellIndex = parseCellIndex(nameStr);
|
|
@@ -1159,6 +1166,60 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
1159
1166
|
continue;
|
|
1160
1167
|
}
|
|
1161
1168
|
|
|
1169
|
+
// ---- Safety fallback: no onDialogBegin callback registered -----
|
|
1170
|
+
// Legacy dialog loop (below) requires JS to call dialogEval()/
|
|
1171
|
+
// exitDialog() in response to the onDialogBegin callback. If no
|
|
1172
|
+
// callback is registered (e.g. stale ScheduledTask Dialog[] call
|
|
1173
|
+
// arriving during a non-Dynamic cell), nobody will ever call those
|
|
1174
|
+
// functions and the eval hangs permanently. Auto-close the dialog
|
|
1175
|
+
// using the same inline path as rejectDialog.
|
|
1176
|
+
if (!opts || !opts->hasOnDialogBegin) {
|
|
1177
|
+
DiagLog("[Eval] BEGINDLGPKT: no onDialogBegin callback — auto-closing "
|
|
1178
|
+
"(dynAutoMode=false, hasOnDialogBegin=false)");
|
|
1179
|
+
// Pre-drain INPUTNAMEPKT — Dialog[] from ScheduledTask sends
|
|
1180
|
+
// INPUTNAMEPKT before accepting EnterTextPacket.
|
|
1181
|
+
{
|
|
1182
|
+
auto preDl = std::chrono::steady_clock::now() +
|
|
1183
|
+
std::chrono::milliseconds(500);
|
|
1184
|
+
while (std::chrono::steady_clock::now() < preDl) {
|
|
1185
|
+
if (!WSReady(lp)) {
|
|
1186
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
1187
|
+
continue;
|
|
1188
|
+
}
|
|
1189
|
+
int ipkt = WSNextPacket(lp);
|
|
1190
|
+
DiagLog("[Eval] BEGINDLGPKT safety: pre-drain pkt=" + std::to_string(ipkt));
|
|
1191
|
+
WSNewPacket(lp);
|
|
1192
|
+
if (ipkt == INPUTNAMEPKT) break;
|
|
1193
|
+
if (ipkt == 0 || ipkt == ILLEGALPKT) { WSClearError(lp); break; }
|
|
1194
|
+
}
|
|
1195
|
+
}
|
|
1196
|
+
const char* closeExpr = "Return[$Failed]";
|
|
1197
|
+
WSPutFunction(lp, "EnterTextPacket", 1);
|
|
1198
|
+
WSPutUTF8String(lp,
|
|
1199
|
+
reinterpret_cast<const unsigned char*>(closeExpr),
|
|
1200
|
+
static_cast<int>(std::strlen(closeExpr)));
|
|
1201
|
+
WSEndPacket(lp);
|
|
1202
|
+
WSFlush(lp);
|
|
1203
|
+
DiagLog("[Eval] BEGINDLGPKT safety: sent Return[$Failed], draining until ENDDLGPKT");
|
|
1204
|
+
{
|
|
1205
|
+
auto dlgDeadline = std::chrono::steady_clock::now() +
|
|
1206
|
+
std::chrono::milliseconds(2000);
|
|
1207
|
+
while (std::chrono::steady_clock::now() < dlgDeadline) {
|
|
1208
|
+
if (!WSReady(lp)) {
|
|
1209
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
1210
|
+
continue;
|
|
1211
|
+
}
|
|
1212
|
+
int rp = WSNextPacket(lp);
|
|
1213
|
+
DiagLog("[Eval] BEGINDLGPKT safety: drain pkt=" + std::to_string(rp));
|
|
1214
|
+
WSNewPacket(lp);
|
|
1215
|
+
if (rp == ENDDLGPKT) break;
|
|
1216
|
+
if (rp == 0 || rp == ILLEGALPKT) { WSClearError(lp); break; }
|
|
1217
|
+
}
|
|
1218
|
+
}
|
|
1219
|
+
// Continue outer drain loop — original RETURNPKT is still coming.
|
|
1220
|
+
continue;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1162
1223
|
if (opts && opts->dialogOpen)
|
|
1163
1224
|
opts->dialogOpen->store(true);
|
|
1164
1225
|
if (opts && opts->hasOnDialogBegin)
|
|
@@ -1297,9 +1358,23 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
1297
1358
|
// Legacy dialog path: when dynAutoMode is false and the eval has
|
|
1298
1359
|
// dialog callbacks, respond 'i' (inspect) so the kernel enters
|
|
1299
1360
|
// inspect mode and the Internal`AddHandler fires Dialog[].
|
|
1300
|
-
//
|
|
1361
|
+
// For type-1 (interrupt menu) without dialog callbacks, respond
|
|
1362
|
+
// 'a' (abort) rather than 'c' (continue). On ARM64/Wolfram 3,
|
|
1363
|
+
// 'c' can leave the kernel in a stuck state when the interrupt
|
|
1364
|
+
// handler (Internal`AddHandler["Interrupt", Function[.., Dialog[]]])
|
|
1365
|
+
// is installed — Dialog[] interferes with the continuation and
|
|
1366
|
+
// the kernel never sends RETURNPKT. Aborting cleanly produces
|
|
1367
|
+
// $Aborted and keeps the session alive.
|
|
1368
|
+
// For non-interrupt menus (type != 1), 'c' is still safe.
|
|
1301
1369
|
bool wantInspect = opts && !opts->dynAutoMode && opts->hasOnDialogBegin;
|
|
1302
|
-
const char* resp
|
|
1370
|
+
const char* resp;
|
|
1371
|
+
if (wantInspect) {
|
|
1372
|
+
resp = "i";
|
|
1373
|
+
} else if (menuType_ == 1) {
|
|
1374
|
+
resp = "a";
|
|
1375
|
+
} else {
|
|
1376
|
+
resp = "c";
|
|
1377
|
+
}
|
|
1303
1378
|
DiagLog("[Eval] MENUPKT type=" + std::to_string(menuType_) + " — responding '" + resp + "'");
|
|
1304
1379
|
WSPutString(lp, resp);
|
|
1305
1380
|
WSEndPacket(lp);
|
|
@@ -1413,6 +1488,21 @@ public:
|
|
|
1413
1488
|
|
|
1414
1489
|
// ---- thread-pool thread: send packet; block until response ----
|
|
1415
1490
|
void Execute() override {
|
|
1491
|
+
// ---- Pre-eval drain: consume stale packets on the link --------
|
|
1492
|
+
// If an interrupt was sent just as the previous eval completed,
|
|
1493
|
+
// the kernel may have opened a Dialog[] (via the interrupt handler)
|
|
1494
|
+
// while idle. The resulting BEGINDLGPKT sits unread on the link.
|
|
1495
|
+
// Without draining it first, our EvaluatePacket is processed inside
|
|
1496
|
+
// the stale Dialog context and its RETURNPKT is consumed during the
|
|
1497
|
+
// BEGINDLGPKT handler's drain — leaving the outer DrainToEvalResult
|
|
1498
|
+
// loop waiting forever for a RETURNPKT that was already eaten.
|
|
1499
|
+
// Only check if data is already buffered (WSReady) to avoid adding
|
|
1500
|
+
// latency in the normal case (no stale packets).
|
|
1501
|
+
if (WSReady(lp_)) {
|
|
1502
|
+
DiagLog("[Eval] pre-eval: stale data on link — draining");
|
|
1503
|
+
drainStalePackets(lp_, nullptr);
|
|
1504
|
+
}
|
|
1505
|
+
|
|
1416
1506
|
// ---- Phase 2: ScheduledTask[Dialog[], interval] management ----
|
|
1417
1507
|
// Install a kernel-side ScheduledTask that calls Dialog[] periodically.
|
|
1418
1508
|
// Only install when:
|
|
@@ -2348,8 +2438,8 @@ public:
|
|
|
2348
2438
|
|
|
2349
2439
|
// -----------------------------------------------------------------------
|
|
2350
2440
|
// setDynAutoMode(auto) → void
|
|
2351
|
-
// true = C++-internal inline dialog path
|
|
2352
|
-
// false = legacy JS dialogEval/exitDialog path (for debugger)
|
|
2441
|
+
// true = C++-internal inline dialog path
|
|
2442
|
+
// false = legacy JS dialogEval/exitDialog path (default; for debugger)
|
|
2353
2443
|
// -----------------------------------------------------------------------
|
|
2354
2444
|
Napi::Value SetDynAutoMode(const Napi::CallbackInfo& info) {
|
|
2355
2445
|
Napi::Env env = info.Env();
|
|
@@ -2358,7 +2448,15 @@ public:
|
|
|
2358
2448
|
.ThrowAsJavaScriptException();
|
|
2359
2449
|
return env.Undefined();
|
|
2360
2450
|
}
|
|
2361
|
-
|
|
2451
|
+
bool newMode = info[0].As<Napi::Boolean>().Value();
|
|
2452
|
+
bool oldMode = dynAutoMode_.exchange(newMode);
|
|
2453
|
+
// When transitioning true→false (Dynamic cleanup), stop the timer
|
|
2454
|
+
// thread. Prevents stale ScheduledTask Dialog[] calls from reaching
|
|
2455
|
+
// the BEGINDLGPKT handler after cleanup. The safety fallback in
|
|
2456
|
+
// BEGINDLGPKT handles any Dialog[] that fires before this takes effect.
|
|
2457
|
+
if (oldMode && !newMode) {
|
|
2458
|
+
dynIntervalMs_.store(0);
|
|
2459
|
+
}
|
|
2362
2460
|
return env.Undefined();
|
|
2363
2461
|
}
|
|
2364
2462
|
|
|
@@ -3021,6 +3119,8 @@ Napi::Object InitModule(Napi::Env env, Napi::Object exports) {
|
|
|
3021
3119
|
WstpReader::Init(env, exports);
|
|
3022
3120
|
exports.Set("setDiagHandler",
|
|
3023
3121
|
Napi::Function::New(env, SetDiagHandler, "setDiagHandler"));
|
|
3122
|
+
// version — mirrors package.json "version"; read-only string constant.
|
|
3123
|
+
exports.Set("version", Napi::String::New(env, "0.6.3"));
|
|
3024
3124
|
return exports;
|
|
3025
3125
|
}
|
|
3026
3126
|
|
package/test.js
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
// ── Test suite for wstp-backend v0.6.
|
|
3
|
+
// ── Test suite for wstp-backend v0.6.2 ────────────────────────────────────
|
|
4
4
|
// Covers: evaluation queue, streaming callbacks, sub() priority, abort
|
|
5
5
|
// behaviour, WstpReader side-channel, edge cases, Dialog[] subsession
|
|
6
6
|
// (dialogEval, exitDialog, isDialogOpen, onDialogBegin/End/Print),
|
|
7
7
|
// subWhenIdle() (background queue, timeout, close rejection), kernelPid,
|
|
8
8
|
// Dynamic eval API (registerDynamic, getDynamicResults, setDynamicInterval,
|
|
9
9
|
// setDynAutoMode, dynamicActive, rejectDialog, abort deduplication).
|
|
10
|
+
// 0.6.2: BEGINDLGPKT safety fallback (Bug 1A), setDynAutoMode cleanup (Bug 1B).
|
|
10
11
|
|
|
11
12
|
const { WstpSession, WstpReader, setDiagHandler } = require('./build/Release/wstp.node');
|
|
12
13
|
|
|
@@ -508,11 +509,12 @@ async function main() {
|
|
|
508
509
|
});
|
|
509
510
|
|
|
510
511
|
// ── 17. Unhandled dialog does not corrupt the link ────────────────────
|
|
511
|
-
// Call Dialog[] from a plain evaluate()
|
|
512
|
-
//
|
|
513
|
-
//
|
|
512
|
+
// Call Dialog[] from a plain evaluate() using only isDialogOpen polling
|
|
513
|
+
// (no onDialogBegin handler). onDialogBegin: () => {} is still required
|
|
514
|
+
// to opt into the legacy dialog loop — without it Fix 1A auto-closes the
|
|
515
|
+
// dialog before JS can interact with it.
|
|
514
516
|
await run('17. unhandled dialog does not corrupt link', async () => {
|
|
515
|
-
const evalPromise = session.evaluate('Dialog[]; 42');
|
|
517
|
+
const evalPromise = session.evaluate('Dialog[]; 42', { onDialogBegin: () => {} });
|
|
516
518
|
|
|
517
519
|
await pollUntil(() => session.isDialogOpen);
|
|
518
520
|
assert(session.isDialogOpen, 'dialog should have opened');
|
|
@@ -529,7 +531,7 @@ async function main() {
|
|
|
529
531
|
// exitDialog('21') sends EnterTextPacket["Return[21]"]. The value 21
|
|
530
532
|
// becomes the value of Dialog[] in the outer expression, so x$$*2 == 42.
|
|
531
533
|
await run('19. exitDialog() with a return value', async () => {
|
|
532
|
-
const p = session.evaluate('x$$ = Dialog[]; x$$ * 2');
|
|
534
|
+
const p = session.evaluate('x$$ = Dialog[]; x$$ * 2', { onDialogBegin: () => {} });
|
|
533
535
|
await pollUntil(() => session.isDialogOpen);
|
|
534
536
|
await session.exitDialog('21');
|
|
535
537
|
const r = await p;
|
|
@@ -542,7 +544,7 @@ async function main() {
|
|
|
542
544
|
// ── 20. dialogEval() sees outer variable state ────────────────────────
|
|
543
545
|
// Variables set before Dialog[] are in scope inside the subsession.
|
|
544
546
|
await run('20. dialogEval sees outer variable state', async () => {
|
|
545
|
-
const p = session.evaluate('myVar$$ = 123; Dialog[]; myVar$$');
|
|
547
|
+
const p = session.evaluate('myVar$$ = 123; Dialog[]; myVar$$', { onDialogBegin: () => {} });
|
|
546
548
|
await pollUntil(() => session.isDialogOpen);
|
|
547
549
|
const v = await session.dialogEval('myVar$$');
|
|
548
550
|
assert(
|
|
@@ -556,7 +558,7 @@ async function main() {
|
|
|
556
558
|
// ── 21. dialogEval() can mutate kernel state ──────────────────────────
|
|
557
559
|
// Variables set inside the dialog persist after the dialog closes.
|
|
558
560
|
await run('21. dialogEval can mutate kernel state', async () => {
|
|
559
|
-
const p = session.evaluate('Dialog[]; mutated$$');
|
|
561
|
+
const p = session.evaluate('Dialog[]; mutated$$', { onDialogBegin: () => {} });
|
|
560
562
|
await pollUntil(() => session.isDialogOpen);
|
|
561
563
|
await session.dialogEval('mutated$$ = 777');
|
|
562
564
|
await session.exitDialog();
|
|
@@ -571,7 +573,7 @@ async function main() {
|
|
|
571
573
|
// ── 22. Multiple dialogEval() calls are serviced FIFO ─────────────────
|
|
572
574
|
// Three concurrent dialogEval() calls queue up and resolve in order.
|
|
573
575
|
await run('22. multiple dialogEval calls are serviced FIFO', async () => {
|
|
574
|
-
const p = session.evaluate('Dialog[]');
|
|
576
|
+
const p = session.evaluate('Dialog[]', { onDialogBegin: () => {} });
|
|
575
577
|
await pollUntil(() => session.isDialogOpen);
|
|
576
578
|
const [a, b, c] = await Promise.all([
|
|
577
579
|
session.dialogEval('1'),
|
|
@@ -590,7 +592,7 @@ async function main() {
|
|
|
590
592
|
// false → true (on Dialog[]) → false (after exitDialog).
|
|
591
593
|
await run('23. isDialogOpen transitions correctly', async () => {
|
|
592
594
|
assert(!session.isDialogOpen, 'initially false');
|
|
593
|
-
const p = session.evaluate('Dialog[]');
|
|
595
|
+
const p = session.evaluate('Dialog[]', { onDialogBegin: () => {} });
|
|
594
596
|
await pollUntil(() => session.isDialogOpen);
|
|
595
597
|
assert(session.isDialogOpen, 'true while dialog open');
|
|
596
598
|
await session.exitDialog();
|
|
@@ -604,6 +606,7 @@ async function main() {
|
|
|
604
606
|
let dlgPrintResolve;
|
|
605
607
|
const dlgPrintDelivered = new Promise(r => { dlgPrintResolve = r; });
|
|
606
608
|
const p = session.evaluate('Dialog[]', {
|
|
609
|
+
onDialogBegin: () => {},
|
|
607
610
|
onDialogPrint: (line) => { dialogLines.push(line); dlgPrintResolve(); },
|
|
608
611
|
});
|
|
609
612
|
await pollUntil(() => session.isDialogOpen);
|
|
@@ -659,7 +662,7 @@ async function main() {
|
|
|
659
662
|
// A plain evaluate() queued while the dialog inner loop is running must
|
|
660
663
|
// wait and then be dispatched normally after ENDDLGPKT.
|
|
661
664
|
await run('27. evaluate() queued during dialog runs after dialog closes', async () => {
|
|
662
|
-
const p1 = session.evaluate('Dialog[]');
|
|
665
|
+
const p1 = session.evaluate('Dialog[]', { onDialogBegin: () => {} });
|
|
663
666
|
await pollUntil(() => session.isDialogOpen);
|
|
664
667
|
// Queue a normal eval WHILE the dialog is open — it must wait.
|
|
665
668
|
const p2 = session.evaluate('"queued-during-dialog"');
|
|
@@ -688,7 +691,7 @@ async function main() {
|
|
|
688
691
|
const sub = session.createSubsession();
|
|
689
692
|
try {
|
|
690
693
|
// Open a Dialog[] on the subsession.
|
|
691
|
-
const pEval = sub.evaluate('Dialog[]');
|
|
694
|
+
const pEval = sub.evaluate('Dialog[]', { onDialogBegin: () => {} });
|
|
692
695
|
await pollUntil(() => sub.isDialogOpen);
|
|
693
696
|
assert(sub.isDialogOpen, 'dialog should be open');
|
|
694
697
|
|
|
@@ -728,14 +731,14 @@ async function main() {
|
|
|
728
731
|
// 2500ms because Pause[] ignores WSInterruptMessage during a sleep.
|
|
729
732
|
// This test documents the fundamental limitation: Dynamic cannot read a
|
|
730
733
|
// live variable while Pause[N] is running.
|
|
731
|
-
await run('P1: Pause[
|
|
734
|
+
await run('P1: Pause[4] ignores interrupt within 1500ms', async () => {
|
|
732
735
|
const s = mkSession();
|
|
733
736
|
try {
|
|
734
737
|
await installHandler(s);
|
|
735
738
|
|
|
736
739
|
let evalDone = false;
|
|
737
740
|
const mainProm = s.evaluate(
|
|
738
|
-
'pP1 = 0; Pause[
|
|
741
|
+
'pP1 = 0; Pause[4]; pP1 = 1; "p1-done"'
|
|
739
742
|
).then(() => { evalDone = true; });
|
|
740
743
|
|
|
741
744
|
await sleep(300);
|
|
@@ -744,12 +747,12 @@ async function main() {
|
|
|
744
747
|
assert(sent === true, 'interrupt() should return true mid-eval');
|
|
745
748
|
|
|
746
749
|
const t0 = Date.now();
|
|
747
|
-
while (!s.isDialogOpen && Date.now() - t0 <
|
|
750
|
+
while (!s.isDialogOpen && Date.now() - t0 < 1500) await sleep(25);
|
|
748
751
|
|
|
749
752
|
const dlgOpened = s.isDialogOpen;
|
|
750
|
-
assert(!dlgOpened, 'Pause[
|
|
753
|
+
assert(!dlgOpened, 'Pause[4] should NOT open a dialog within 1500ms');
|
|
751
754
|
|
|
752
|
-
// After the
|
|
755
|
+
// After the 1500ms window the interrupt may still be queued — the kernel
|
|
753
756
|
// will fire Dialog[] once Pause[] releases. Abort to unstick the eval
|
|
754
757
|
// rather than waiting for it to return on its own (which could take forever
|
|
755
758
|
// if Dialog[] opens and nobody services it).
|
|
@@ -758,7 +761,7 @@ async function main() {
|
|
|
758
761
|
} finally {
|
|
759
762
|
s.close();
|
|
760
763
|
}
|
|
761
|
-
},
|
|
764
|
+
}, 12_000);
|
|
762
765
|
|
|
763
766
|
// ── P2: Pause[0.3] loop — interrupt opens Dialog and dialogEval works ──
|
|
764
767
|
// Expected: interrupt during a short Pause[] loop opens a Dialog[],
|
|
@@ -770,7 +773,7 @@ async function main() {
|
|
|
770
773
|
|
|
771
774
|
let evalDone = false;
|
|
772
775
|
const mainProm = s.evaluate(
|
|
773
|
-
'Do[nP2 = k; Pause[0.
|
|
776
|
+
'Do[nP2 = k; Pause[0.1], {k, 1, 15}]; "p2-done"',
|
|
774
777
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
775
778
|
).then(() => { evalDone = true; });
|
|
776
779
|
|
|
@@ -778,19 +781,19 @@ async function main() {
|
|
|
778
781
|
|
|
779
782
|
s.interrupt();
|
|
780
783
|
try { await pollUntil(() => s.isDialogOpen, 3000); }
|
|
781
|
-
catch (_) { throw new Error('Dialog never opened — interrupt not working with Pause[0.
|
|
784
|
+
catch (_) { throw new Error('Dialog never opened — interrupt not working with Pause[0.1]'); }
|
|
782
785
|
|
|
783
786
|
const val = await withTimeout(s.dialogEval('nP2'), 5000, 'dialogEval nP2');
|
|
784
787
|
assert(val && typeof val.value === 'number' && val.value >= 1,
|
|
785
788
|
`expected nP2 >= 1, got ${JSON.stringify(val)}`);
|
|
786
789
|
|
|
787
790
|
await s.exitDialog();
|
|
788
|
-
await withTimeout(mainProm,
|
|
791
|
+
await withTimeout(mainProm, 8_000, 'P2 main eval');
|
|
789
792
|
} finally {
|
|
790
793
|
try { s.abort(); } catch (_) {}
|
|
791
794
|
s.close();
|
|
792
795
|
}
|
|
793
|
-
},
|
|
796
|
+
}, 15_000);
|
|
794
797
|
|
|
795
798
|
// ── P3: dialogEval timeout — kernel state after (diagnostic) ───────────
|
|
796
799
|
// Simulates the extension failure: dialogEval times out without exitDialog.
|
|
@@ -803,7 +806,7 @@ async function main() {
|
|
|
803
806
|
|
|
804
807
|
let evalDone = false;
|
|
805
808
|
const mainProm = s.evaluate(
|
|
806
|
-
'Do[nP3 = k; Pause[0.
|
|
809
|
+
'Do[nP3 = k; Pause[0.1], {k, 1, 15}]; "p3-done"',
|
|
807
810
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
808
811
|
).then(() => { evalDone = true; });
|
|
809
812
|
|
|
@@ -838,7 +841,7 @@ async function main() {
|
|
|
838
841
|
await s.exitDialog().catch(() => {});
|
|
839
842
|
}
|
|
840
843
|
|
|
841
|
-
if (!evalDone) { try { await withTimeout(mainProm,
|
|
844
|
+
if (!evalDone) { try { await withTimeout(mainProm, 6_000, 'P3 loop'); } catch (_) {} }
|
|
842
845
|
|
|
843
846
|
// Diagnostic: document observed outcome but do not hard-fail on dlg2
|
|
844
847
|
if (!exitOk) {
|
|
@@ -850,7 +853,7 @@ async function main() {
|
|
|
850
853
|
try { s.abort(); } catch (_) {}
|
|
851
854
|
s.close();
|
|
852
855
|
}
|
|
853
|
-
},
|
|
856
|
+
}, 20_000);
|
|
854
857
|
|
|
855
858
|
// ── P4: abort() after stuck dialog — session stays alive ────────────────
|
|
856
859
|
// Note: abort() sends WSAbortMessage which resets the Wolfram kernel's
|
|
@@ -864,7 +867,7 @@ async function main() {
|
|
|
864
867
|
await installHandler(s);
|
|
865
868
|
|
|
866
869
|
const mainProm = s.evaluate(
|
|
867
|
-
'Do[nP4=k; Pause[0.
|
|
870
|
+
'Do[nP4=k; Pause[0.1], {k,1,15}]; "p4-done"',
|
|
868
871
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
869
872
|
).catch(() => {});
|
|
870
873
|
|
|
@@ -896,7 +899,7 @@ async function main() {
|
|
|
896
899
|
try { s.abort(); } catch (_) {}
|
|
897
900
|
s.close();
|
|
898
901
|
}
|
|
899
|
-
},
|
|
902
|
+
}, 15_000);
|
|
900
903
|
|
|
901
904
|
// ── P5: closeAllDialogs()+abort() recovery → reinstall handler → new dialog works
|
|
902
905
|
// closeAllDialogs() is designed to be paired with abort(). It rejects all
|
|
@@ -909,7 +912,7 @@ async function main() {
|
|
|
909
912
|
await installHandler(s);
|
|
910
913
|
|
|
911
914
|
const mainProm = s.evaluate(
|
|
912
|
-
'Do[nP5 = k; Pause[0.
|
|
915
|
+
'Do[nP5 = k; Pause[0.1], {k, 1, 15}]; "p5-done"',
|
|
913
916
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
914
917
|
).catch(() => {});
|
|
915
918
|
|
|
@@ -935,7 +938,7 @@ async function main() {
|
|
|
935
938
|
|
|
936
939
|
let dlg2 = false;
|
|
937
940
|
const mainProm2 = s.evaluate(
|
|
938
|
-
'Do[nP5b = k; Pause[0.
|
|
941
|
+
'Do[nP5b = k; Pause[0.1], {k, 1, 15}]; "p5b-done"',
|
|
939
942
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
940
943
|
).catch(() => {});
|
|
941
944
|
|
|
@@ -957,7 +960,7 @@ async function main() {
|
|
|
957
960
|
try { s.abort(); } catch (_) {}
|
|
958
961
|
s.close();
|
|
959
962
|
}
|
|
960
|
-
},
|
|
963
|
+
}, 25_000);
|
|
961
964
|
|
|
962
965
|
// ── P6: Simulate Dynamic + Pause[5] full scenario (diagnostic) ─────────
|
|
963
966
|
// Reproduces: n=RandomInteger[100]; Pause[5] with interrupt every 2.5s.
|
|
@@ -970,18 +973,18 @@ async function main() {
|
|
|
970
973
|
|
|
971
974
|
let evalDone = false;
|
|
972
975
|
const mainProm = s.evaluate(
|
|
973
|
-
'n = RandomInteger[100]; Pause[
|
|
976
|
+
'n = RandomInteger[100]; Pause[3]; "p6-done"',
|
|
974
977
|
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
975
978
|
).then(() => { evalDone = true; });
|
|
976
979
|
|
|
977
980
|
await sleep(200);
|
|
978
981
|
|
|
979
|
-
const INTERRUPT_WAIT_MS =
|
|
982
|
+
const INTERRUPT_WAIT_MS = 1500;
|
|
980
983
|
const DIALOG_EVAL_TIMEOUT_MS = 8000;
|
|
981
984
|
let dialogReadSucceeded = false;
|
|
982
985
|
let pauseIgnoredInterrupt = false;
|
|
983
986
|
|
|
984
|
-
for (let cycle = 1; cycle <=
|
|
987
|
+
for (let cycle = 1; cycle <= 2 && !evalDone; cycle++) {
|
|
985
988
|
const t0 = Date.now();
|
|
986
989
|
s.interrupt();
|
|
987
990
|
while (!s.isDialogOpen && Date.now() - t0 < INTERRUPT_WAIT_MS) await sleep(25);
|
|
@@ -1009,11 +1012,11 @@ async function main() {
|
|
|
1009
1012
|
}
|
|
1010
1013
|
}
|
|
1011
1014
|
|
|
1012
|
-
try { await withTimeout(mainProm,
|
|
1015
|
+
try { await withTimeout(mainProm, 7_000, 'P6 main eval'); } catch (_) {}
|
|
1013
1016
|
|
|
1014
1017
|
// Diagnostic: always passes — log observed outcome
|
|
1015
1018
|
if (pauseIgnoredInterrupt && !dialogReadSucceeded) {
|
|
1016
|
-
console.log(' P6 note: Pause[
|
|
1019
|
+
console.log(' P6 note: Pause[3] blocked all interrupts — expected behaviour');
|
|
1017
1020
|
} else if (dialogReadSucceeded) {
|
|
1018
1021
|
console.log(' P6 note: at least one read succeeded despite Pause');
|
|
1019
1022
|
}
|
|
@@ -1021,7 +1024,7 @@ async function main() {
|
|
|
1021
1024
|
try { s.abort(); } catch (_) {}
|
|
1022
1025
|
s.close();
|
|
1023
1026
|
}
|
|
1024
|
-
},
|
|
1027
|
+
}, 20_000);
|
|
1025
1028
|
|
|
1026
1029
|
// ── 25. abort() while dialog is open ──────────────────────────────────
|
|
1027
1030
|
// Must run AFTER all other dialog tests — abort() sends WSAbortMessage
|
|
@@ -1029,7 +1032,7 @@ async function main() {
|
|
|
1029
1032
|
// evaluations. Tests 26 and 27 need a clean session, so test 25 runs last
|
|
1030
1033
|
// (just before test 18 which also corrupts the link via WSInterruptMessage).
|
|
1031
1034
|
await run('25. abort while dialog is open', async () => {
|
|
1032
|
-
const p = session.evaluate('Dialog[]');
|
|
1035
|
+
const p = session.evaluate('Dialog[]', { onDialogBegin: () => {} });
|
|
1033
1036
|
await pollUntil(() => session.isDialogOpen);
|
|
1034
1037
|
session.abort();
|
|
1035
1038
|
const r = await p;
|
|
@@ -1474,6 +1477,159 @@ async function main() {
|
|
|
1474
1477
|
}
|
|
1475
1478
|
});
|
|
1476
1479
|
|
|
1480
|
+
// ── 53. Bug 1A: stale ScheduledTask + non-Dynamic cell does not hang ───
|
|
1481
|
+
// Simulates the subsession.js teardown: Dynamic cell runs → cleanup calls
|
|
1482
|
+
// setDynAutoMode(false) but the kernel-side ScheduledTask may still fire
|
|
1483
|
+
// one more Dialog[]. The next plain cell (no onDialogBegin) must not hang.
|
|
1484
|
+
await run('53. stale ScheduledTask Dialog[] after setDynAutoMode(false) — no hang (Bug 1A)', async () => {
|
|
1485
|
+
const s = mkSession();
|
|
1486
|
+
try {
|
|
1487
|
+
// Step 1: register a Dynamic expr and start the interval.
|
|
1488
|
+
s.registerDynamic('dyn0', 'ToString[AbsoluteTime[], InputForm]');
|
|
1489
|
+
s.setDynamicInterval(300);
|
|
1490
|
+
|
|
1491
|
+
// Step 2: long enough eval for ScheduledTask to fire at least once.
|
|
1492
|
+
const r1 = await withTimeout(
|
|
1493
|
+
s.evaluate('Do[Pause[0.1], {20}]; "cell1"'),
|
|
1494
|
+
12000, '53 cell1'
|
|
1495
|
+
);
|
|
1496
|
+
assert(!r1.aborted && r1.result.value === 'cell1',
|
|
1497
|
+
`cell1 result: ${JSON.stringify(r1.result)}`);
|
|
1498
|
+
|
|
1499
|
+
// Step 3: simulate subsession.js cleanup.
|
|
1500
|
+
// Fix 1B: setDynAutoMode(false) now also sets dynIntervalMs_=0.
|
|
1501
|
+
s.unregisterDynamic('dyn0');
|
|
1502
|
+
s.setDynAutoMode(false);
|
|
1503
|
+
|
|
1504
|
+
// Step 4: plain cell with no onDialogBegin — must not hang.
|
|
1505
|
+
const r2 = await withTimeout(
|
|
1506
|
+
s.evaluate('n = 1'),
|
|
1507
|
+
10000, '53 cell2 — would hang without Fix 1A'
|
|
1508
|
+
);
|
|
1509
|
+
assert(!r2.aborted, 'cell2 must not be aborted');
|
|
1510
|
+
|
|
1511
|
+
// Step 5: follow-up eval also works.
|
|
1512
|
+
const r3 = await withTimeout(s.evaluate('1 + 1'), 6000, '53 cell3');
|
|
1513
|
+
assert(r3.result.value === 2, `cell3 expected 2, got ${r3.result.value}`);
|
|
1514
|
+
} finally {
|
|
1515
|
+
s.close();
|
|
1516
|
+
}
|
|
1517
|
+
}, 45000);
|
|
1518
|
+
|
|
1519
|
+
// ── 54. Bug 1A: BEGINDLGPKT with dynAutoMode=false, no JS callback ────
|
|
1520
|
+
// With dynAutoMode=false and no onDialogBegin registered, any BEGINDLGPKT
|
|
1521
|
+
// must be auto-closed by the safety fallback rather than entering the
|
|
1522
|
+
// legacy loop where nobody will ever call exitDialog().
|
|
1523
|
+
await run('54. BEGINDLGPKT safety fallback — auto-close when no onDialogBegin (Bug 1A)', async () => {
|
|
1524
|
+
const s = mkSession();
|
|
1525
|
+
try {
|
|
1526
|
+
s.setDynAutoMode(false);
|
|
1527
|
+
// Eval runs for ~2s; no onDialogBegin registered. Any BEGINDLGPKT
|
|
1528
|
+
// that arrives (from a stale ScheduledTask or concurrent interrupt)
|
|
1529
|
+
// must be auto-closed by the safety fallback.
|
|
1530
|
+
const r = await withTimeout(
|
|
1531
|
+
s.evaluate('Do[Pause[0.2], {10}]; "done"'),
|
|
1532
|
+
15000, '54 eval — would hang without Fix 1A'
|
|
1533
|
+
);
|
|
1534
|
+
assert(!r.aborted, 'eval must not be aborted');
|
|
1535
|
+
assert(r.result.value === 'done', `expected "done", got "${r.result.value}"`);
|
|
1536
|
+
|
|
1537
|
+
// Follow-up eval must work — no leftover packets.
|
|
1538
|
+
const r2 = await withTimeout(s.evaluate('2 + 2'), 6000, '54 follow-up');
|
|
1539
|
+
assert(r2.result.value === 4, `follow-up expected 4, got ${r2.result.value}`);
|
|
1540
|
+
} finally {
|
|
1541
|
+
s.close();
|
|
1542
|
+
}
|
|
1543
|
+
}, 30000);
|
|
1544
|
+
|
|
1545
|
+
// ── 55. Interactive mode with non-Null result (no trailing semicolon) ──
|
|
1546
|
+
// In interactive mode (EnterExpressionPacket), a non-Null result produces:
|
|
1547
|
+
// RETURNEXPRPKT → INPUTNAMEPKT.
|
|
1548
|
+
// Bug: drainStalePackets() consumed the trailing INPUTNAMEPKT, causing the
|
|
1549
|
+
// outer DrainToEvalResult loop to hang forever waiting for a packet already
|
|
1550
|
+
// consumed. This test verifies that interactive evals with visible results
|
|
1551
|
+
// (no trailing semicolon) complete without hanging.
|
|
1552
|
+
await run('55. interactive eval with non-Null result (drainStalePackets fix)', async () => {
|
|
1553
|
+
const s = mkSession();
|
|
1554
|
+
try {
|
|
1555
|
+
// Simple assignment without semicolon — returns non-Null.
|
|
1556
|
+
const r1 = await withTimeout(
|
|
1557
|
+
s.evaluate('n = 1', { interactive: true }),
|
|
1558
|
+
10000, '55 n=1 interactive — would hang without drainStalePackets fix'
|
|
1559
|
+
);
|
|
1560
|
+
assert(!r1.aborted, 'n=1 must not be aborted');
|
|
1561
|
+
assert(r1.result.value === 1, `n=1 expected 1, got ${r1.result.value}`);
|
|
1562
|
+
|
|
1563
|
+
// Follow-up: another non-Null interactive eval.
|
|
1564
|
+
const r2 = await withTimeout(
|
|
1565
|
+
s.evaluate('n + 41', { interactive: true }),
|
|
1566
|
+
10000, '55 n+41 interactive'
|
|
1567
|
+
);
|
|
1568
|
+
assert(!r2.aborted, 'n+41 must not be aborted');
|
|
1569
|
+
assert(r2.result.value === 42, `n+41 expected 42, got ${r2.result.value}`);
|
|
1570
|
+
|
|
1571
|
+
// Follow-up with semicolon (Null result) — should also work.
|
|
1572
|
+
const r3 = await withTimeout(
|
|
1573
|
+
s.evaluate('m = 99;', { interactive: true }),
|
|
1574
|
+
10000, '55 m=99; interactive'
|
|
1575
|
+
);
|
|
1576
|
+
assert(!r3.aborted, 'm=99; must not be aborted');
|
|
1577
|
+
|
|
1578
|
+
// Follow-up: non-interactive eval still works after interactive ones.
|
|
1579
|
+
const r4 = await withTimeout(
|
|
1580
|
+
s.evaluate('m + 1'),
|
|
1581
|
+
10000, '55 m+1 non-interactive follow-up'
|
|
1582
|
+
);
|
|
1583
|
+
assert(r4.result.value === 100, `m+1 expected 100, got ${r4.result.value}`);
|
|
1584
|
+
} finally {
|
|
1585
|
+
s.close();
|
|
1586
|
+
}
|
|
1587
|
+
}, 45000);
|
|
1588
|
+
|
|
1589
|
+
// ── 56. Stale interrupt aborts eval cleanly (no hang) ──────────────────
|
|
1590
|
+
// When live-watch sends an interrupt just as a cell completes, the kernel
|
|
1591
|
+
// may fire the queued interrupt during the NEXT evaluation. Without
|
|
1592
|
+
// dialog callbacks, the C++ MENUPKT handler responds 'a' (abort) so the
|
|
1593
|
+
// eval returns $Aborted rather than hanging. This is safe: the caller
|
|
1594
|
+
// can retry. A follow-up eval (without stale interrupt) must succeed.
|
|
1595
|
+
await run('56. stale interrupt aborts eval — no hang', async () => {
|
|
1596
|
+
const s = mkSession();
|
|
1597
|
+
try {
|
|
1598
|
+
await installHandler(s);
|
|
1599
|
+
|
|
1600
|
+
// Warm up
|
|
1601
|
+
const r0 = await withTimeout(s.evaluate('1 + 1'), 5000, '56 warmup');
|
|
1602
|
+
assert(r0.result.value === 2);
|
|
1603
|
+
|
|
1604
|
+
// Send interrupt to idle kernel — may fire during next eval
|
|
1605
|
+
s.interrupt();
|
|
1606
|
+
await sleep(500); // give kernel time to queue interrupt
|
|
1607
|
+
|
|
1608
|
+
// Evaluate — stale interrupt fires → MENUPKT → 'a' → $Aborted
|
|
1609
|
+
const r1 = await withTimeout(
|
|
1610
|
+
s.evaluate('42'),
|
|
1611
|
+
10000, '56 eval — would hang without abort-on-MENUPKT fix'
|
|
1612
|
+
);
|
|
1613
|
+
// The eval may return $Aborted (interrupt fired) or 42 (interrupt
|
|
1614
|
+
// was ignored by idle kernel). Either is acceptable — the critical
|
|
1615
|
+
// thing is that it does NOT hang.
|
|
1616
|
+
if (r1.aborted) {
|
|
1617
|
+
assert(r1.result.value === '$Aborted',
|
|
1618
|
+
`expected $Aborted, got ${JSON.stringify(r1.result)}`);
|
|
1619
|
+
} else {
|
|
1620
|
+
assert(r1.result.value === 42,
|
|
1621
|
+
`expected 42, got ${JSON.stringify(r1.result)}`);
|
|
1622
|
+
}
|
|
1623
|
+
|
|
1624
|
+
// Follow-up eval must work regardless
|
|
1625
|
+
const r2 = await withTimeout(s.evaluate('2 + 2'), 5000, '56 follow-up');
|
|
1626
|
+
assert(!r2.aborted, '56 follow-up should not be aborted');
|
|
1627
|
+
assert(r2.result.value === 4);
|
|
1628
|
+
} finally {
|
|
1629
|
+
s.close();
|
|
1630
|
+
}
|
|
1631
|
+
}, 30000);
|
|
1632
|
+
|
|
1477
1633
|
// ── Teardown ──────────────────────────────────────────────────────────
|
|
1478
1634
|
session.close();
|
|
1479
1635
|
assert(!session.isOpen, 'main session not closed');
|
|
@@ -1487,6 +1643,7 @@ async function main() {
|
|
|
1487
1643
|
process.exit(0);
|
|
1488
1644
|
} else {
|
|
1489
1645
|
console.log(`${passed} passed, ${failed} failed${skipped ? `, ${skipped} skipped` : ''}.`);
|
|
1646
|
+
process.exit(1);
|
|
1490
1647
|
}
|
|
1491
1648
|
}
|
|
1492
1649
|
|