wstp-node 0.4.6 → 0.6.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/README.md +32 -2
- package/build/Release/wstp.node +0 -0
- package/index.d.ts +134 -0
- package/package.json +1 -1
- package/src/addon.cc +906 -172
- package/test.js +492 -30
package/src/addon.cc
CHANGED
|
@@ -91,6 +91,21 @@ struct DialogRequest {
|
|
|
91
91
|
Napi::ThreadSafeFunction resolve; // NonBlockingCall'd with the WExpr result
|
|
92
92
|
};
|
|
93
93
|
|
|
94
|
+
// ---------------------------------------------------------------------------
|
|
95
|
+
// Dynamic evaluation registration and result types. Defined at file scope so
|
|
96
|
+
// both EvalOptions and WstpSession can use them without circular dependencies.
|
|
97
|
+
// ---------------------------------------------------------------------------
|
|
98
|
+
struct DynRegistration {
|
|
99
|
+
std::string id;
|
|
100
|
+
std::string expr;
|
|
101
|
+
};
|
|
102
|
+
struct DynResult {
|
|
103
|
+
std::string id;
|
|
104
|
+
std::string value;
|
|
105
|
+
double timestamp = 0.0; // ms since epoch (set when stored)
|
|
106
|
+
std::string error; // non-empty if evaluation failed
|
|
107
|
+
};
|
|
108
|
+
|
|
94
109
|
struct EvalOptions {
|
|
95
110
|
Napi::ThreadSafeFunction onPrint; // fires once per Print[] line
|
|
96
111
|
Napi::ThreadSafeFunction onMessage; // fires once per kernel message
|
|
@@ -105,6 +120,21 @@ struct EvalOptions {
|
|
|
105
120
|
// When true, DrainToEvalResult expects EnterExpressionPacket protocol:
|
|
106
121
|
// INPUTNAMEPKT → OUTPUTNAMEPKT → RETURNEXPRPKT (or INPUTNAMEPKT for Null).
|
|
107
122
|
bool interactive = false;
|
|
123
|
+
// When true, any BEGINDLGPKT received during the drain is immediately
|
|
124
|
+
// auto-closed (Return[$Failed] sent) without informing the JS layer.
|
|
125
|
+
// Use for non-interactive VsCodeRender/handler-install evals to prevent
|
|
126
|
+
// a concurrent interrupt from hanging the evaluation forever (Pattern C).
|
|
127
|
+
bool rejectDialog = false;
|
|
128
|
+
// Phase 2 Dynamic eval: pointers wired up by Evaluate() so DrainToEvalResult
|
|
129
|
+
// can inline-evaluate registered Dynamic expressions inside BEGINDLGPKT.
|
|
130
|
+
// When dynAutoMode is false, legacy JS-callback dialog path is used instead.
|
|
131
|
+
std::mutex* dynMutex = nullptr;
|
|
132
|
+
std::vector<DynRegistration>* dynRegistry = nullptr; // non-owning
|
|
133
|
+
std::vector<DynResult>* dynResults = nullptr; // non-owning
|
|
134
|
+
std::chrono::steady_clock::time_point* dynLastEval = nullptr;
|
|
135
|
+
bool dynAutoMode = true; // mirrors dynAutoMode_ at time of queue dispatch
|
|
136
|
+
int dynIntervalMs = 0; // mirrors dynIntervalMs_ at time of queue dispatch
|
|
137
|
+
int* dynTaskInstalledInterval = nullptr; // non-owning; tracks installed ScheduledTask interval
|
|
108
138
|
CompleteCtx* ctx = nullptr; // non-owning; set when TSFNs are in use
|
|
109
139
|
|
|
110
140
|
// Pointers to session's dialog queue — set by WstpSession::Evaluate() so the
|
|
@@ -262,6 +292,210 @@ static void drainDialogAbortResponse(WSLINK lp) {
|
|
|
262
292
|
}
|
|
263
293
|
}
|
|
264
294
|
|
|
295
|
+
// ---------------------------------------------------------------------------
|
|
296
|
+
// drainStalePackets — after RETURNPKT, check for stale packets that arrived
|
|
297
|
+
// in the 50ms window after the kernel sent the main result.
|
|
298
|
+
//
|
|
299
|
+
// Scenario: an interrupt was sent just as the cell completed; the kernel may
|
|
300
|
+
// have queued a BEGINDLGPKT that arrives after RETURNPKT. If left unread it
|
|
301
|
+
// corrupts the NEXT evaluation (Pattern D).
|
|
302
|
+
//
|
|
303
|
+
// If opts->rejectDialog is true the dialog is also closed via our inline
|
|
304
|
+
// path (same as the main BEGINDLGPKT handler). If false, we still close
|
|
305
|
+
// stale dialogs silently to keep the link clean — the JS side never knew
|
|
306
|
+
// about this dialog so there's nobody to call exitDialog().
|
|
307
|
+
// ---------------------------------------------------------------------------
|
|
308
|
+
static void drainStalePackets(WSLINK lp, EvalOptions* opts) {
|
|
309
|
+
// When dynAutoMode is active, use a short timeout (50ms) so we do NOT
|
|
310
|
+
// accidentally consume a ScheduledTask Dialog[] that the main eval's
|
|
311
|
+
// dynAutoMode handler should handle. The stale packets we are looking
|
|
312
|
+
// for (from PREVIOUS evals) are already sitting on the link and arrive
|
|
313
|
+
// instantly, so 50ms is plenty. The longer 200ms timeout is only needed
|
|
314
|
+
// for non-dynAutoMode scenarios.
|
|
315
|
+
int timeoutMs = (opts && opts->dynAutoMode) ? 50 : 200;
|
|
316
|
+
auto deadline = std::chrono::steady_clock::now() +
|
|
317
|
+
std::chrono::milliseconds(timeoutMs);
|
|
318
|
+
while (std::chrono::steady_clock::now() < deadline) {
|
|
319
|
+
if (!WSReady(lp)) {
|
|
320
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
321
|
+
continue;
|
|
322
|
+
}
|
|
323
|
+
int pkt = WSNextPacket(lp);
|
|
324
|
+
if (pkt == BEGINDLGPKT) {
|
|
325
|
+
// Stale dialog — consume level int then auto-close.
|
|
326
|
+
wsint64 lvl = 0;
|
|
327
|
+
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &lvl);
|
|
328
|
+
WSNewPacket(lp);
|
|
329
|
+
DiagLog("[Eval] drainStalePackets: stale BEGINDLGPKT level=" + std::to_string(lvl) + " — auto-closing");
|
|
330
|
+
// Pre-drain INPUTNAMEPKT — Dialog[] sends INPUTNAMEPKT before
|
|
331
|
+
// accepting EnterTextPacket.
|
|
332
|
+
{
|
|
333
|
+
auto preDl = std::chrono::steady_clock::now() +
|
|
334
|
+
std::chrono::milliseconds(500);
|
|
335
|
+
while (std::chrono::steady_clock::now() < preDl) {
|
|
336
|
+
if (!WSReady(lp)) {
|
|
337
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
338
|
+
continue;
|
|
339
|
+
}
|
|
340
|
+
int ipkt = WSNextPacket(lp);
|
|
341
|
+
DiagLog("[Eval] drainStale: pre-drain pkt=" + std::to_string(ipkt));
|
|
342
|
+
WSNewPacket(lp);
|
|
343
|
+
if (ipkt == INPUTNAMEPKT) break;
|
|
344
|
+
if (ipkt == 0 || ipkt == ILLEGALPKT) { WSClearError(lp); break; }
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
const char* closeExpr = "Return[$Failed]";
|
|
348
|
+
WSPutFunction(lp, "EnterTextPacket", 1);
|
|
349
|
+
WSPutUTF8String(lp,
|
|
350
|
+
reinterpret_cast<const unsigned char*>(closeExpr),
|
|
351
|
+
static_cast<int>(std::strlen(closeExpr)));
|
|
352
|
+
WSEndPacket(lp);
|
|
353
|
+
WSFlush(lp);
|
|
354
|
+
DiagLog("[Eval] drainStale: sent Return[$Failed], draining...");
|
|
355
|
+
// Drain until ENDDLGPKT.
|
|
356
|
+
auto dlgDeadline = std::chrono::steady_clock::now() +
|
|
357
|
+
std::chrono::milliseconds(2000);
|
|
358
|
+
while (std::chrono::steady_clock::now() < dlgDeadline) {
|
|
359
|
+
if (!WSReady(lp)) {
|
|
360
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
361
|
+
continue;
|
|
362
|
+
}
|
|
363
|
+
int rp = WSNextPacket(lp);
|
|
364
|
+
DiagLog("[Eval] drainStale: drain pkt=" + std::to_string(rp));
|
|
365
|
+
WSNewPacket(lp);
|
|
366
|
+
if (rp == ENDDLGPKT) break;
|
|
367
|
+
if (rp == 0 || rp == ILLEGALPKT) { WSClearError(lp); break; }
|
|
368
|
+
}
|
|
369
|
+
} else if (pkt == MENUPKT) {
|
|
370
|
+
// Stale interrupt menu — dismiss with empty response.
|
|
371
|
+
wsint64 menuType = 0; WSGetInteger64(lp, &menuType);
|
|
372
|
+
const char* menuPrompt = nullptr; WSGetString(lp, &menuPrompt);
|
|
373
|
+
if (menuPrompt) WSReleaseString(lp, menuPrompt);
|
|
374
|
+
WSNewPacket(lp);
|
|
375
|
+
DiagLog("[Eval] drainStalePackets: stale MENUPKT type=" + std::to_string(menuType) + " — dismissing");
|
|
376
|
+
WSPutString(lp, "");
|
|
377
|
+
WSEndPacket(lp); WSFlush(lp);
|
|
378
|
+
} else {
|
|
379
|
+
WSNewPacket(lp); // discard any other stale packet
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
// ---------------------------------------------------------------------------
|
|
385
|
+
// drainUntilEndDialog — reads packets until ENDDLGPKT (dialog closed).
|
|
386
|
+
// Used by the C++-internal Dynamic eval path to finish a dialog level cleanly.
|
|
387
|
+
// Returns true on success, false on timeout or link error.
|
|
388
|
+
//
|
|
389
|
+
// If capturedOuterResult is non-null and a RETURNPKT/RETURNEXPRPKT arrives
|
|
390
|
+
// that looks like the outer eval's result (rather than Return[$Failed]'s
|
|
391
|
+
// response), it is saved there for the caller to use.
|
|
392
|
+
// ---------------------------------------------------------------------------
|
|
393
|
+
static bool drainUntilEndDialog(WSLINK lp, int timeoutMs,
|
|
394
|
+
WExpr* capturedOuterResult = nullptr) {
|
|
395
|
+
auto deadline = std::chrono::steady_clock::now() +
|
|
396
|
+
std::chrono::milliseconds(timeoutMs);
|
|
397
|
+
while (std::chrono::steady_clock::now() < deadline) {
|
|
398
|
+
if (!WSReady(lp)) {
|
|
399
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
400
|
+
continue;
|
|
401
|
+
}
|
|
402
|
+
int pkt = WSNextPacket(lp);
|
|
403
|
+
DiagLog("[drainEndDlg] pkt=" + std::to_string(pkt));
|
|
404
|
+
if (pkt == ENDDLGPKT) {
|
|
405
|
+
WSNewPacket(lp);
|
|
406
|
+
return true;
|
|
407
|
+
}
|
|
408
|
+
if (pkt == RETURNPKT || pkt == RETURNEXPRPKT) {
|
|
409
|
+
// Capture the outer eval's RETURNPKT if requested and not yet set.
|
|
410
|
+
if (capturedOuterResult &&
|
|
411
|
+
capturedOuterResult->kind == WExpr::WError &&
|
|
412
|
+
capturedOuterResult->strVal.empty()) {
|
|
413
|
+
*capturedOuterResult = ReadExprRaw(lp);
|
|
414
|
+
DiagLog("[drainEndDlg] captured outer result (pkt=" +
|
|
415
|
+
std::to_string(pkt) + ")");
|
|
416
|
+
}
|
|
417
|
+
WSNewPacket(lp);
|
|
418
|
+
continue;
|
|
419
|
+
}
|
|
420
|
+
if (pkt == 0 || pkt == ILLEGALPKT) {
|
|
421
|
+
WSClearError(lp);
|
|
422
|
+
return false;
|
|
423
|
+
}
|
|
424
|
+
WSNewPacket(lp); // discard TEXTPKT, MESSAGEPKT, INPUTNAMEPKT, etc.
|
|
425
|
+
}
|
|
426
|
+
return false; // timeout
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// ---------------------------------------------------------------------------
|
|
430
|
+
// readDynResultWithTimeout — reads one result from an open Dialog level after
|
|
431
|
+
// the caller has already sent the expression (EnterTextPacket).
|
|
432
|
+
// On success sets dr.value (string form) and returns true.
|
|
433
|
+
// On failure sets dr.error and returns false.
|
|
434
|
+
//
|
|
435
|
+
// If capturedOuterResult is non-null and a RETURNPKT/RETURNEXPRPKT arrives
|
|
436
|
+
// (which is the outer eval's result that got evaluated inside the Dialog[]
|
|
437
|
+
// context), the value is saved there and the function continues waiting for
|
|
438
|
+
// the RETURNTEXTPKT that EnterTextPacket produces.
|
|
439
|
+
// ---------------------------------------------------------------------------
|
|
440
|
+
static bool readDynResultWithTimeout(WSLINK lp, DynResult& dr, int timeoutMs,
|
|
441
|
+
WExpr* capturedOuterResult = nullptr) {
|
|
442
|
+
auto deadline = std::chrono::steady_clock::now() +
|
|
443
|
+
std::chrono::milliseconds(timeoutMs);
|
|
444
|
+
while (std::chrono::steady_clock::now() < deadline) {
|
|
445
|
+
if (!WSReady(lp)) {
|
|
446
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
449
|
+
int pkt = WSNextPacket(lp);
|
|
450
|
+
DiagLog("[DynRead] pkt=" + std::to_string(pkt));
|
|
451
|
+
if (pkt == RETURNPKT || pkt == RETURNEXPRPKT || pkt == RETURNTEXTPKT) {
|
|
452
|
+
WExpr val = ReadExprRaw(lp);
|
|
453
|
+
WSNewPacket(lp);
|
|
454
|
+
DiagLog("[DynRead] accepted pkt=" + std::to_string(pkt) +
|
|
455
|
+
" kind=" + std::to_string(val.kind) +
|
|
456
|
+
" val=" + (val.kind == WExpr::String ? val.strVal :
|
|
457
|
+
val.kind == WExpr::Integer ? std::to_string(val.intVal) :
|
|
458
|
+
val.kind == WExpr::Symbol ? val.strVal :
|
|
459
|
+
val.kind == WExpr::Real ? std::to_string(val.realVal) :
|
|
460
|
+
val.head.empty() ? "?" : val.head));
|
|
461
|
+
// RETURNPKT/RETURNEXPRPKT inside a dialog means the outer eval's
|
|
462
|
+
// EvaluatePacket was processed inside this Dialog[] context (race
|
|
463
|
+
// condition when ScheduledTask fires between evals). Save the
|
|
464
|
+
// value for the caller and keep waiting for our RETURNTEXTPKT.
|
|
465
|
+
if (pkt != RETURNTEXTPKT && capturedOuterResult) {
|
|
466
|
+
DiagLog("[DynRead] captured outer result (pkt=" + std::to_string(pkt) + "), continuing");
|
|
467
|
+
*capturedOuterResult = std::move(val);
|
|
468
|
+
continue;
|
|
469
|
+
}
|
|
470
|
+
if (val.kind == WExpr::WError) {
|
|
471
|
+
dr.error = val.strVal;
|
|
472
|
+
return false;
|
|
473
|
+
}
|
|
474
|
+
switch (val.kind) {
|
|
475
|
+
case WExpr::String: dr.value = val.strVal; break;
|
|
476
|
+
case WExpr::Integer: dr.value = std::to_string(val.intVal); break;
|
|
477
|
+
case WExpr::Real: dr.value = std::to_string(val.realVal); break;
|
|
478
|
+
case WExpr::Symbol: dr.value = val.strVal; break;
|
|
479
|
+
default: dr.value = val.head.empty() ? "?" : val.head; break;
|
|
480
|
+
}
|
|
481
|
+
return true;
|
|
482
|
+
}
|
|
483
|
+
if (pkt == TEXTPKT || pkt == MESSAGEPKT ||
|
|
484
|
+
pkt == OUTPUTNAMEPKT || pkt == INPUTNAMEPKT) {
|
|
485
|
+
WSNewPacket(lp);
|
|
486
|
+
continue;
|
|
487
|
+
}
|
|
488
|
+
if (pkt == 0 || pkt == ILLEGALPKT) {
|
|
489
|
+
WSClearError(lp);
|
|
490
|
+
dr.error = "WSTP link error during Dynamic eval";
|
|
491
|
+
return false;
|
|
492
|
+
}
|
|
493
|
+
WSNewPacket(lp);
|
|
494
|
+
}
|
|
495
|
+
dr.error = "timeout";
|
|
496
|
+
return false;
|
|
497
|
+
}
|
|
498
|
+
|
|
265
499
|
// ---------------------------------------------------------------------------
|
|
266
500
|
// DrainToEvalResult — consume all packets for one cell, capturing Print[]
|
|
267
501
|
// output and messages. Blocks until RETURNPKT. Thread-pool thread only.
|
|
@@ -645,6 +879,10 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
645
879
|
if (r.result.kind == WExpr::Symbol && stripCtx(r.result.strVal) == "$Aborted")
|
|
646
880
|
r.aborted = true;
|
|
647
881
|
gotResult = true;
|
|
882
|
+
// Drain any stale packets (e.g. late BEGINDLGPKT from a concurrent
|
|
883
|
+
// interrupt that arrived just as this cell's RETURNPKT was sent).
|
|
884
|
+
// This prevents Pattern D: stale BEGINDLGPKT corrupting next eval.
|
|
885
|
+
if (!r.aborted) drainStalePackets(lp, opts);
|
|
648
886
|
// In interactive mode the kernel always follows RETURNEXPRPKT with a
|
|
649
887
|
// trailing INPUTNAMEPKT (the next "In[n+1]:=" prompt). We must consume
|
|
650
888
|
// that packet before returning so the wire is clean for the next eval.
|
|
@@ -710,12 +948,217 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
710
948
|
else if (pkt == BEGINDLGPKT) {
|
|
711
949
|
// ----------------------------------------------------------------
|
|
712
950
|
// Dialog subsession opened by the kernel.
|
|
713
|
-
// Read the dialog level integer, set dialogOpen_ flag, fire callback.
|
|
714
951
|
// ----------------------------------------------------------------
|
|
715
952
|
wsint64 level = 0;
|
|
716
953
|
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &level);
|
|
717
954
|
WSNewPacket(lp);
|
|
718
955
|
|
|
956
|
+
// ---- rejectDialog: auto-close without informing JS layer --------
|
|
957
|
+
// Non-interactive evals (VsCodeRender, handler install, sub() exprs)
|
|
958
|
+
// must never block waiting for JS dialogEval() that never comes.
|
|
959
|
+
// Send Return[$Failed] immediately and continue waiting for RETURNPKT.
|
|
960
|
+
if (opts && opts->rejectDialog) {
|
|
961
|
+
DiagLog("[Eval] rejectDialog: auto-closing BEGINDLGPKT level=" + std::to_string(level));
|
|
962
|
+
// Pre-drain INPUTNAMEPKT — Dialog[] from ScheduledTask sends
|
|
963
|
+
// INPUTNAMEPKT before accepting EnterTextPacket.
|
|
964
|
+
{
|
|
965
|
+
auto preDl = std::chrono::steady_clock::now() +
|
|
966
|
+
std::chrono::milliseconds(500);
|
|
967
|
+
while (std::chrono::steady_clock::now() < preDl) {
|
|
968
|
+
if (!WSReady(lp)) {
|
|
969
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
970
|
+
continue;
|
|
971
|
+
}
|
|
972
|
+
int ipkt = WSNextPacket(lp);
|
|
973
|
+
DiagLog("[Eval] rejectDialog: pre-drain pkt=" + std::to_string(ipkt));
|
|
974
|
+
WSNewPacket(lp);
|
|
975
|
+
if (ipkt == INPUTNAMEPKT) break;
|
|
976
|
+
if (ipkt == 0 || ipkt == ILLEGALPKT) { WSClearError(lp); break; }
|
|
977
|
+
}
|
|
978
|
+
}
|
|
979
|
+
const char* closeExpr = "Return[$Failed]";
|
|
980
|
+
WSPutFunction(lp, "EnterTextPacket", 1);
|
|
981
|
+
WSPutUTF8String(lp,
|
|
982
|
+
reinterpret_cast<const unsigned char*>(closeExpr),
|
|
983
|
+
static_cast<int>(std::strlen(closeExpr)));
|
|
984
|
+
WSEndPacket(lp);
|
|
985
|
+
WSFlush(lp);
|
|
986
|
+
DiagLog("[Eval] rejectDialog: sent Return[$Failed], draining until ENDDLGPKT");
|
|
987
|
+
// Drain until ENDDLGPKT — kernel will close the dialog level.
|
|
988
|
+
{
|
|
989
|
+
auto dlgDeadline = std::chrono::steady_clock::now() +
|
|
990
|
+
std::chrono::milliseconds(2000);
|
|
991
|
+
while (std::chrono::steady_clock::now() < dlgDeadline) {
|
|
992
|
+
if (!WSReady(lp)) {
|
|
993
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
994
|
+
continue;
|
|
995
|
+
}
|
|
996
|
+
int rp = WSNextPacket(lp);
|
|
997
|
+
DiagLog("[Eval] rejectDialog: drain pkt=" + std::to_string(rp));
|
|
998
|
+
WSNewPacket(lp);
|
|
999
|
+
if (rp == ENDDLGPKT) break;
|
|
1000
|
+
if (rp == 0 || rp == ILLEGALPKT) { WSClearError(lp); break; }
|
|
1001
|
+
}
|
|
1002
|
+
}
|
|
1003
|
+
// Continue outer drain loop — the original RETURNPKT is still coming.
|
|
1004
|
+
continue;
|
|
1005
|
+
}
|
|
1006
|
+
|
|
1007
|
+
// ---- dynAutoMode: C++-internal Dynamic evaluation ---------------
|
|
1008
|
+
// When dynAutoMode is true, all registered Dynamic expressions are
|
|
1009
|
+
// evaluated inline inside the dialog — no JS roundtrip needed.
|
|
1010
|
+
// The dialog is then closed unconditionally with Return[$Failed].
|
|
1011
|
+
if (!opts || opts->dynAutoMode) {
|
|
1012
|
+
// Check if aborted before entering evaluation.
|
|
1013
|
+
if (opts && opts->abortFlag && opts->abortFlag->load()) {
|
|
1014
|
+
if (opts && opts->dialogOpen) opts->dialogOpen->store(false);
|
|
1015
|
+
r.result = WExpr::mkSymbol("System`$Aborted");
|
|
1016
|
+
r.aborted = true;
|
|
1017
|
+
drainDialogAbortResponse(lp);
|
|
1018
|
+
return r;
|
|
1019
|
+
}
|
|
1020
|
+
|
|
1021
|
+
// If the outer eval's EvaluatePacket was processed inside
|
|
1022
|
+
// this Dialog[] context (between-eval ScheduledTask fire),
|
|
1023
|
+
// its RETURNPKT will appear before our RETURNTEXTPKT.
|
|
1024
|
+
// capturedOuterResult captures that value so we can return
|
|
1025
|
+
// it as the cell result.
|
|
1026
|
+
WExpr capturedOuterResult;
|
|
1027
|
+
|
|
1028
|
+
if (opts && opts->dynMutex && opts->dynRegistry && opts->dynResults) {
|
|
1029
|
+
std::lock_guard<std::mutex> lk(*opts->dynMutex);
|
|
1030
|
+
|
|
1031
|
+
// Drain any initial packets (INPUTNAMEPKT, etc.) before
|
|
1032
|
+
// sending expressions. Dialog[] from ScheduledTask may
|
|
1033
|
+
// send INPUTNAMEPKT or TEXTPKT before accepting input.
|
|
1034
|
+
{
|
|
1035
|
+
auto drainDl = std::chrono::steady_clock::now() +
|
|
1036
|
+
std::chrono::milliseconds(500);
|
|
1037
|
+
while (std::chrono::steady_clock::now() < drainDl) {
|
|
1038
|
+
if (!WSReady(lp)) {
|
|
1039
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
1040
|
+
continue;
|
|
1041
|
+
}
|
|
1042
|
+
int ipkt = WSNextPacket(lp);
|
|
1043
|
+
DiagLog("[Eval] dynAutoMode(BEGINDLG): pre-drain pkt=" + std::to_string(ipkt));
|
|
1044
|
+
if (ipkt == INPUTNAMEPKT) {
|
|
1045
|
+
WSNewPacket(lp);
|
|
1046
|
+
break; // ready for input
|
|
1047
|
+
}
|
|
1048
|
+
if (ipkt == 0 || ipkt == ILLEGALPKT) {
|
|
1049
|
+
WSClearError(lp);
|
|
1050
|
+
break;
|
|
1051
|
+
}
|
|
1052
|
+
WSNewPacket(lp); // consume TEXTPKT, MESSAGEPKT, etc.
|
|
1053
|
+
}
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1056
|
+
// Current time in ms since epoch for timestamps.
|
|
1057
|
+
auto nowMs = static_cast<double>(
|
|
1058
|
+
std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
1059
|
+
std::chrono::system_clock::now().time_since_epoch())
|
|
1060
|
+
.count());
|
|
1061
|
+
|
|
1062
|
+
for (const auto& reg : *opts->dynRegistry) {
|
|
1063
|
+
// Send expression via EnterTextPacket so the kernel
|
|
1064
|
+
// evaluates it in OutputForm text mode (string result).
|
|
1065
|
+
DiagLog("[Eval] dynAutoMode(BEGINDLG): sending expr id=" + reg.id + " expr='" + reg.expr + "'");
|
|
1066
|
+
bool sentDyn =
|
|
1067
|
+
WSPutFunction(lp, "EnterTextPacket", 1) &&
|
|
1068
|
+
WSPutUTF8String(lp,
|
|
1069
|
+
reinterpret_cast<const unsigned char*>(reg.expr.c_str()),
|
|
1070
|
+
static_cast<int>(reg.expr.size())) &&
|
|
1071
|
+
WSEndPacket(lp) &&
|
|
1072
|
+
WSFlush(lp);
|
|
1073
|
+
|
|
1074
|
+
DynResult dr;
|
|
1075
|
+
dr.id = reg.id;
|
|
1076
|
+
dr.timestamp = nowMs;
|
|
1077
|
+
if (!sentDyn) {
|
|
1078
|
+
dr.error = "failed to send Dynamic expression";
|
|
1079
|
+
} else {
|
|
1080
|
+
readDynResultWithTimeout(lp, dr, 2000, &capturedOuterResult);
|
|
1081
|
+
}
|
|
1082
|
+
opts->dynResults->push_back(std::move(dr));
|
|
1083
|
+
|
|
1084
|
+
// If the outer result was captured, stop processing
|
|
1085
|
+
// further dynamic expressions — the eval is done.
|
|
1086
|
+
if (capturedOuterResult.kind != WExpr::WError ||
|
|
1087
|
+
!capturedOuterResult.strVal.empty()) break;
|
|
1088
|
+
|
|
1089
|
+
// Abort check between expressions.
|
|
1090
|
+
if (opts->abortFlag && opts->abortFlag->load()) break;
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
if (opts->dynLastEval)
|
|
1094
|
+
*opts->dynLastEval = std::chrono::steady_clock::now();
|
|
1095
|
+
|
|
1096
|
+
// Check if the outer eval's RETURNPKT was captured inside
|
|
1097
|
+
// this dialog. If so, close the dialog and return the
|
|
1098
|
+
// captured result directly — the outer eval is already done.
|
|
1099
|
+
bool outerCaptured = capturedOuterResult.kind != WExpr::WError ||
|
|
1100
|
+
!capturedOuterResult.strVal.empty();
|
|
1101
|
+
if (outerCaptured) {
|
|
1102
|
+
DiagLog("[Eval] dynAutoMode: outer RETURNPKT captured inside dialog — returning directly");
|
|
1103
|
+
// Close the dialog.
|
|
1104
|
+
{
|
|
1105
|
+
const char* closeExpr = "Return[$Failed]";
|
|
1106
|
+
WSPutFunction(lp, "EnterTextPacket", 1);
|
|
1107
|
+
WSPutUTF8String(lp,
|
|
1108
|
+
reinterpret_cast<const unsigned char*>(closeExpr),
|
|
1109
|
+
static_cast<int>(std::strlen(closeExpr)));
|
|
1110
|
+
WSEndPacket(lp);
|
|
1111
|
+
WSFlush(lp);
|
|
1112
|
+
}
|
|
1113
|
+
drainUntilEndDialog(lp, 3000);
|
|
1114
|
+
r.result = std::move(capturedOuterResult);
|
|
1115
|
+
if (r.result.kind == WExpr::Symbol &&
|
|
1116
|
+
stripCtx(r.result.strVal) == "$Aborted")
|
|
1117
|
+
r.aborted = true;
|
|
1118
|
+
// Drain any remaining stale packets.
|
|
1119
|
+
drainStalePackets(lp, opts);
|
|
1120
|
+
return r;
|
|
1121
|
+
}
|
|
1122
|
+
}
|
|
1123
|
+
|
|
1124
|
+
// Close the dialog: send Return[$Failed] then drain ENDDLGPKT.
|
|
1125
|
+
{
|
|
1126
|
+
const char* closeExpr = "Return[$Failed]";
|
|
1127
|
+
WSPutFunction(lp, "EnterTextPacket", 1);
|
|
1128
|
+
WSPutUTF8String(lp,
|
|
1129
|
+
reinterpret_cast<const unsigned char*>(closeExpr),
|
|
1130
|
+
static_cast<int>(std::strlen(closeExpr)));
|
|
1131
|
+
WSEndPacket(lp);
|
|
1132
|
+
WSFlush(lp);
|
|
1133
|
+
}
|
|
1134
|
+
bool exitOk = drainUntilEndDialog(lp, 3000, &capturedOuterResult);
|
|
1135
|
+
if (!exitOk) {
|
|
1136
|
+
DiagLog("[Eval] dynAutoMode: drainUntilEndDialog timed out; aborting");
|
|
1137
|
+
r.aborted = true;
|
|
1138
|
+
r.result = WExpr::mkSymbol("System`$Aborted");
|
|
1139
|
+
drainDialogAbortResponse(lp);
|
|
1140
|
+
return r;
|
|
1141
|
+
}
|
|
1142
|
+
// Check if the outer eval's RETURNPKT was captured during the
|
|
1143
|
+
// drain (e.g. empty registry but between-eval Dialog[] race).
|
|
1144
|
+
{
|
|
1145
|
+
bool outerCapturedInDrain =
|
|
1146
|
+
capturedOuterResult.kind != WExpr::WError ||
|
|
1147
|
+
!capturedOuterResult.strVal.empty();
|
|
1148
|
+
if (outerCapturedInDrain) {
|
|
1149
|
+
DiagLog("[Eval] dynAutoMode: outer RETURNPKT captured during drain — returning directly");
|
|
1150
|
+
r.result = std::move(capturedOuterResult);
|
|
1151
|
+
if (r.result.kind == WExpr::Symbol &&
|
|
1152
|
+
stripCtx(r.result.strVal) == "$Aborted")
|
|
1153
|
+
r.aborted = true;
|
|
1154
|
+
drainStalePackets(lp, opts);
|
|
1155
|
+
return r;
|
|
1156
|
+
}
|
|
1157
|
+
}
|
|
1158
|
+
// Dialog closed — continue outer loop waiting for the original RETURNPKT.
|
|
1159
|
+
continue;
|
|
1160
|
+
}
|
|
1161
|
+
|
|
719
1162
|
if (opts && opts->dialogOpen)
|
|
720
1163
|
opts->dialogOpen->store(true);
|
|
721
1164
|
if (opts && opts->hasOnDialogBegin)
|
|
@@ -844,166 +1287,23 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
844
1287
|
}
|
|
845
1288
|
else if (pkt == MENUPKT) {
|
|
846
1289
|
// ----------------------------------------------------------------
|
|
847
|
-
// MENUPKT (6) — interrupt menu
|
|
848
|
-
//
|
|
849
|
-
// Protocol (from JLink InterruptDialog.java):
|
|
850
|
-
// 1. WSNextPacket(lp) → MENUPKT
|
|
851
|
-
// 2. WSGetInteger(lp, &type) — menu type (1=interrupt, 3=LinkRead)
|
|
852
|
-
// 3. WSGetString(lp, &prompt) — prompt string
|
|
853
|
-
// 4. WSNewPacket(lp)
|
|
854
|
-
// 5. WSPutString(lp, "i") — respond with bare string
|
|
855
|
-
// Options: a=abort, c=continue, i=inspect/dialog
|
|
856
|
-
// 6. WSEndPacket; WSFlush
|
|
857
|
-
//
|
|
858
|
-
// After "i", the kernel enters interactive inspect mode and sends
|
|
859
|
-
// MENUPKT(inspect) as the dialog prompt → handled in menuDlgDone.
|
|
1290
|
+
// MENUPKT (6) — interrupt menu
|
|
860
1291
|
// ----------------------------------------------------------------
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
865
|
-
WSNewPacket(lp);
|
|
866
|
-
DiagLog("[Eval] MENUPKT type=" + std::to_string(menuType_) + " — responding 'i' (inspect)");
|
|
1292
|
+
wsint64 menuType_ = 0; WSGetInteger64(lp, &menuType_);
|
|
1293
|
+
const char* menuPrompt_ = nullptr; WSGetString(lp, &menuPrompt_);
|
|
1294
|
+
if (menuPrompt_) WSReleaseString(lp, menuPrompt_);
|
|
1295
|
+
WSNewPacket(lp);
|
|
867
1296
|
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
879
|
-
r.result = WExpr::mkSymbol("System`$Aborted");
|
|
880
|
-
r.aborted = true;
|
|
881
|
-
drainDialogAbortResponse(lp); // consume pending abort response
|
|
882
|
-
return r;
|
|
883
|
-
}
|
|
884
|
-
if (opts && opts->dialogPending && opts->dialogPending->load()) {
|
|
885
|
-
if (!serviceDialogRequest(/*menuDlgProto=*/true)) {
|
|
886
|
-
menuDlgDone = true;
|
|
887
|
-
continue;
|
|
888
|
-
}
|
|
889
|
-
}
|
|
890
|
-
if (!WSReady(lp)) {
|
|
891
|
-
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
892
|
-
continue;
|
|
893
|
-
}
|
|
894
|
-
int dpkt = WSNextPacket(lp);
|
|
895
|
-
DiagLog("[MenuDlg] dpkt=" + std::to_string(dpkt));
|
|
896
|
-
if (dpkt == ENDDLGPKT) {
|
|
897
|
-
wsint64 el = 0;
|
|
898
|
-
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &el);
|
|
899
|
-
WSNewPacket(lp);
|
|
900
|
-
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
901
|
-
if (opts->hasOnDialogEnd)
|
|
902
|
-
opts->onDialogEnd.NonBlockingCall(
|
|
903
|
-
[el](Napi::Env e, Napi::Function cb){
|
|
904
|
-
cb.Call({Napi::Number::New(e, static_cast<double>(el))}); });
|
|
905
|
-
menuDlgDone = true;
|
|
906
|
-
} else if (dpkt == BEGINDLGPKT) {
|
|
907
|
-
// BEGINDLGPKT: dialog subsession started.
|
|
908
|
-
// This arrives after 'i' response to MENUPKT, before INPUTNAMEPKT.
|
|
909
|
-
// The dialog level integer follows.
|
|
910
|
-
wsint64 beginLevel = 0;
|
|
911
|
-
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &beginLevel);
|
|
912
|
-
WSNewPacket(lp);
|
|
913
|
-
DiagLog("[MenuDlg] BEGINDLGPKT level=" + std::to_string(beginLevel) + " — dialog is open");
|
|
914
|
-
// isDialogOpen will be set when INPUTNAMEPKT arrives (see below).
|
|
915
|
-
// Just consume and wait for INPUTNAMEPKT.
|
|
916
|
-
} else if (dpkt == RETURNPKT) {
|
|
917
|
-
WExpr inner = ReadExprRaw(lp); WSNewPacket(lp);
|
|
918
|
-
if (inner.kind == WExpr::Symbol &&
|
|
919
|
-
stripCtx(inner.strVal) == "$Aborted") {
|
|
920
|
-
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
921
|
-
r.result = inner; r.aborted = true; return r;
|
|
922
|
-
}
|
|
923
|
-
} else if (dpkt == MENUPKT) {
|
|
924
|
-
// In the menuDlgDone loop, MENUPKTs serve two purposes:
|
|
925
|
-
// 1. FIRST MENUPKT (isDialogOpen still false):
|
|
926
|
-
// The dialog subsession has just opened.
|
|
927
|
-
// Set isDialogOpen = true, fire onDialogBegin, consume & wait.
|
|
928
|
-
// 2. LATER MENUPKTs (isDialogOpen true):
|
|
929
|
-
// Either the interrupt menu again (if interrupt handler also opened
|
|
930
|
-
// Dialog[], these menus pile up) — respond "c" to dismiss it.
|
|
931
|
-
// Or if exitDialog sent 'c' and cleared dialogOpen → we're done.
|
|
932
|
-
bool isOpen = opts->dialogOpen && opts->dialogOpen->load();
|
|
933
|
-
if (!isOpen) {
|
|
934
|
-
// First MENUPKT in menuDlgDone: the inspect/dialog-mode prompt.
|
|
935
|
-
// The kernel has entered interactive inspection mode.
|
|
936
|
-
// Read the type and prompt following JLink MENUPKT protocol.
|
|
937
|
-
wsint64 menuTypeInsp = 0; WSGetInteger64(lp, &menuTypeInsp);
|
|
938
|
-
const char* menuPromptInsp = nullptr; WSGetString(lp, &menuPromptInsp);
|
|
939
|
-
if (menuPromptInsp) WSReleaseString(lp, menuPromptInsp);
|
|
940
|
-
WSNewPacket(lp);
|
|
941
|
-
DiagLog("[MenuDlg] inspect MENUPKT type=" + std::to_string(menuTypeInsp) + " — isDialogOpen=true");
|
|
942
|
-
if (opts->dialogOpen) opts->dialogOpen->store(true);
|
|
943
|
-
if (opts->hasOnDialogBegin)
|
|
944
|
-
opts->onDialogBegin.NonBlockingCall(
|
|
945
|
-
[](Napi::Env e, Napi::Function cb){
|
|
946
|
-
cb.Call({Napi::Number::New(e, 1.0)}); });
|
|
947
|
-
} else if (opts->dialogOpen && !opts->dialogOpen->load()) {
|
|
948
|
-
// exitDialog already closed the dialog (cleared by serviceDialogRequest)
|
|
949
|
-
WSNewPacket(lp);
|
|
950
|
-
menuDlgDone = true;
|
|
951
|
-
} else {
|
|
952
|
-
// Subsequent MENUPKT while dialog is open — this is likely the
|
|
953
|
-
// interrupt-level menu that appeared AFTER the dialog-open MENUPKT.
|
|
954
|
-
// Dismiss it with bare string 'c' (continue) per JLink MENUPKT protocol.
|
|
955
|
-
DiagLog("[MenuDlg] subsequent MENUPKT while dialog open — responding 'c' (continue)");
|
|
956
|
-
wsint64 menuTypeDis = 0; WSGetInteger64(lp, &menuTypeDis);
|
|
957
|
-
const char* menuPromptDis = nullptr; WSGetString(lp, &menuPromptDis);
|
|
958
|
-
if (menuPromptDis) WSReleaseString(lp, menuPromptDis);
|
|
959
|
-
WSNewPacket(lp);
|
|
960
|
-
WSPutString(lp, "c");
|
|
961
|
-
WSEndPacket(lp);
|
|
962
|
-
WSFlush(lp);
|
|
963
|
-
}
|
|
964
|
-
} else if (dpkt == INPUTNAMEPKT || dpkt == OUTPUTNAMEPKT) {
|
|
965
|
-
const char* nm = nullptr; WSGetString(lp, &nm);
|
|
966
|
-
if (nm) {
|
|
967
|
-
std::string nml = nm; WSReleaseString(lp, nm);
|
|
968
|
-
DiagLog("[MenuDlg] pkt=" + std::to_string(dpkt) + " name='" + nml + "'");
|
|
969
|
-
// INPUTNAMEPKT in menuDlgDone may mean the dialog/inspect
|
|
970
|
-
// subsession is prompting for input (alternative to MENUPKT).
|
|
971
|
-
if (dpkt == INPUTNAMEPKT && opts && opts->dialogOpen &&
|
|
972
|
-
!opts->dialogOpen->load()) {
|
|
973
|
-
DiagLog("[MenuDlg] INPUTNAMEPKT while !dialogOpen — dialog opened via INPUTNAMEPKT");
|
|
974
|
-
opts->dialogOpen->store(true);
|
|
975
|
-
if (opts->hasOnDialogBegin)
|
|
976
|
-
opts->onDialogBegin.NonBlockingCall(
|
|
977
|
-
[](Napi::Env e, Napi::Function cb){
|
|
978
|
-
cb.Call({Napi::Number::New(e, 1.0)}); });
|
|
979
|
-
}
|
|
980
|
-
}
|
|
981
|
-
WSNewPacket(lp);
|
|
982
|
-
} else if (dpkt == TEXTPKT) {
|
|
983
|
-
const char* ts = nullptr; WSGetString(lp, &ts);
|
|
984
|
-
if (ts) {
|
|
985
|
-
std::string tl = rtrimNL(ts); WSReleaseString(lp, ts);
|
|
986
|
-
DiagLog("[MenuDlg] TEXTPKT text='" + tl.substr(0, 80) + "'");
|
|
987
|
-
// Filter out interrupt menu options text (informational only).
|
|
988
|
-
bool isMenuOptions = (tl.find("Your options are") != std::string::npos);
|
|
989
|
-
if (!isMenuOptions && opts && opts->hasOnDialogPrint)
|
|
990
|
-
opts->onDialogPrint.NonBlockingCall(
|
|
991
|
-
[tl](Napi::Env e, Napi::Function cb){
|
|
992
|
-
cb.Call({Napi::String::New(e, tl)}); });
|
|
993
|
-
}
|
|
994
|
-
WSNewPacket(lp);
|
|
995
|
-
} else if (dpkt == MESSAGEPKT) {
|
|
996
|
-
handleMessage(true);
|
|
997
|
-
} else if (dpkt == 0 || dpkt == ILLEGALPKT) {
|
|
998
|
-
const char* em = WSErrorMessage(lp); WSClearError(lp);
|
|
999
|
-
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
1000
|
-
r.result = WExpr::mkError(em ? em : "WSTP link error in dialog");
|
|
1001
|
-
return r;
|
|
1002
|
-
} else {
|
|
1003
|
-
WSNewPacket(lp);
|
|
1004
|
-
}
|
|
1005
|
-
}
|
|
1006
|
-
// Dialog closed — outer loop continues to collect main eval result.
|
|
1297
|
+
// Legacy dialog path: when dynAutoMode is false and the eval has
|
|
1298
|
+
// dialog callbacks, respond 'i' (inspect) so the kernel enters
|
|
1299
|
+
// inspect mode and the Internal`AddHandler fires Dialog[].
|
|
1300
|
+
// Otherwise respond 'c' (continue) to dismiss the menu.
|
|
1301
|
+
bool wantInspect = opts && !opts->dynAutoMode && opts->hasOnDialogBegin;
|
|
1302
|
+
const char* resp = wantInspect ? "i" : "c";
|
|
1303
|
+
DiagLog("[Eval] MENUPKT type=" + std::to_string(menuType_) + " — responding '" + resp + "'");
|
|
1304
|
+
WSPutString(lp, resp);
|
|
1305
|
+
WSEndPacket(lp);
|
|
1306
|
+
WSFlush(lp);
|
|
1007
1307
|
}
|
|
1008
1308
|
else {
|
|
1009
1309
|
DiagLog("[Eval] unknown pkt=" + std::to_string(pkt) + ", discarding");
|
|
@@ -1113,6 +1413,61 @@ public:
|
|
|
1113
1413
|
|
|
1114
1414
|
// ---- thread-pool thread: send packet; block until response ----
|
|
1115
1415
|
void Execute() override {
|
|
1416
|
+
// ---- Phase 2: ScheduledTask[Dialog[], interval] management ----
|
|
1417
|
+
// Install a kernel-side ScheduledTask that calls Dialog[] periodically.
|
|
1418
|
+
// Only install when:
|
|
1419
|
+
// (a) dynAutoMode is active
|
|
1420
|
+
// (b) there are registered Dynamic expressions
|
|
1421
|
+
// (c) interval > 0
|
|
1422
|
+
// (d) the currently installed task has a DIFFERENT interval (or none)
|
|
1423
|
+
//
|
|
1424
|
+
// The task expression includes a stop-flag check so the old task can
|
|
1425
|
+
// be suppressed during the install eval (which uses rejectDialog=true).
|
|
1426
|
+
//
|
|
1427
|
+
// When there are no registrations (or interval=0), we do NOT send any
|
|
1428
|
+
// stop/remove eval. The old task keeps running but the dynAutoMode
|
|
1429
|
+
// handler handles empty-registry Dialog[]s by simply closing them.
|
|
1430
|
+
if (opts_.dynAutoMode && opts_.dynIntervalMs > 0) {
|
|
1431
|
+
bool hasRegs = false;
|
|
1432
|
+
if (opts_.dynMutex && opts_.dynRegistry) {
|
|
1433
|
+
std::lock_guard<std::mutex> lk(*opts_.dynMutex);
|
|
1434
|
+
hasRegs = !opts_.dynRegistry->empty();
|
|
1435
|
+
}
|
|
1436
|
+
|
|
1437
|
+
int installedInterval = opts_.dynTaskInstalledInterval
|
|
1438
|
+
? *opts_.dynTaskInstalledInterval : 0;
|
|
1439
|
+
bool needInstall = hasRegs && (installedInterval != opts_.dynIntervalMs);
|
|
1440
|
+
|
|
1441
|
+
if (needInstall) {
|
|
1442
|
+
double intervalSec = opts_.dynIntervalMs / 1000.0;
|
|
1443
|
+
std::string taskExpr =
|
|
1444
|
+
"Quiet[$wstpDynTaskStop = True;"
|
|
1445
|
+
" If[ValueQ[$wstpDynTask], RemoveScheduledTask[$wstpDynTask]];"
|
|
1446
|
+
" $wstpDynTaskStop =.;"
|
|
1447
|
+
" $wstpDynTask = RunScheduledTask["
|
|
1448
|
+
"If[!TrueQ[$wstpDynTaskStop], Dialog[]], " +
|
|
1449
|
+
std::to_string(intervalSec) + "]]";
|
|
1450
|
+
DiagLog("[Eval] dynAutoMode: installing ScheduledTask interval=" +
|
|
1451
|
+
std::to_string(intervalSec) + "s");
|
|
1452
|
+
EvalOptions taskOpts;
|
|
1453
|
+
taskOpts.rejectDialog = true;
|
|
1454
|
+
bool sentT =
|
|
1455
|
+
WSPutFunction(lp_, "EvaluatePacket", 1) &&
|
|
1456
|
+
WSPutFunction(lp_, "ToExpression", 1) &&
|
|
1457
|
+
WSPutUTF8String(lp_,
|
|
1458
|
+
reinterpret_cast<const unsigned char*>(taskExpr.c_str()),
|
|
1459
|
+
static_cast<int>(taskExpr.size())) &&
|
|
1460
|
+
WSEndPacket(lp_) &&
|
|
1461
|
+
WSFlush(lp_);
|
|
1462
|
+
if (sentT) {
|
|
1463
|
+
DrainToEvalResult(lp_, &taskOpts);
|
|
1464
|
+
// Track the installed interval so we don't reinstall next time.
|
|
1465
|
+
if (opts_.dynTaskInstalledInterval)
|
|
1466
|
+
*opts_.dynTaskInstalledInterval = opts_.dynIntervalMs;
|
|
1467
|
+
}
|
|
1468
|
+
}
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1116
1471
|
bool sent;
|
|
1117
1472
|
if (!interactive_) {
|
|
1118
1473
|
// EvaluatePacket + ToExpression: non-interactive, does NOT populate In[n]/Out[n].
|
|
@@ -1137,6 +1492,24 @@ public:
|
|
|
1137
1492
|
} else {
|
|
1138
1493
|
opts_.interactive = interactive_;
|
|
1139
1494
|
result_ = DrainToEvalResult(lp_, &opts_);
|
|
1495
|
+
// DrainToEvalResult may leave dialogOpen_=true (dynAutoMode suppresses
|
|
1496
|
+
// the timer during the exit protocol). Clear it now that we're done.
|
|
1497
|
+
if (opts_.dialogOpen) opts_.dialogOpen->store(false);
|
|
1498
|
+
|
|
1499
|
+
// ---- Cleanup: ScheduledTask that fires Dialog[] ----
|
|
1500
|
+
// The task is left running after the main eval completes.
|
|
1501
|
+
// Between evaluations, Dialog[]s from the task accumulate on the
|
|
1502
|
+
// link as stale BEGINDLGPKT packets. These are drained at the
|
|
1503
|
+
// start of the next eval by drainStalePackets(). The install
|
|
1504
|
+
// code at the top of Execute() also removes any old task before
|
|
1505
|
+
// installing a new one. On session close(), the kernel is
|
|
1506
|
+
// killed, which stops the task automatically.
|
|
1507
|
+
//
|
|
1508
|
+
// We CANNOT send a cleanup EvaluatePacket here because the
|
|
1509
|
+
// ScheduledTask fires Dialog[] preemptively during the cleanup
|
|
1510
|
+
// eval, and the RETURNPKT from the cleanup can get interleaved
|
|
1511
|
+
// with dialog packets, causing the drain to hang.
|
|
1512
|
+
|
|
1140
1513
|
workerReadingLink_.store(false, std::memory_order_release); // lp_ no longer in use
|
|
1141
1514
|
if (!interactive_) {
|
|
1142
1515
|
// EvaluatePacket mode: kernel never sends INPUTNAMEPKT/OUTPUTNAMEPKT,
|
|
@@ -1241,6 +1614,7 @@ public:
|
|
|
1241
1614
|
Napi::Function func = DefineClass(env, "WstpSession", {
|
|
1242
1615
|
InstanceMethod<&WstpSession::Evaluate> ("evaluate"),
|
|
1243
1616
|
InstanceMethod<&WstpSession::Sub> ("sub"),
|
|
1617
|
+
InstanceMethod<&WstpSession::SubWhenIdle> ("subWhenIdle"),
|
|
1244
1618
|
InstanceMethod<&WstpSession::DialogEval> ("dialogEval"),
|
|
1245
1619
|
InstanceMethod<&WstpSession::ExitDialog> ("exitDialog"),
|
|
1246
1620
|
InstanceMethod<&WstpSession::Interrupt> ("interrupt"),
|
|
@@ -1248,9 +1622,18 @@ public:
|
|
|
1248
1622
|
InstanceMethod<&WstpSession::CloseAllDialogs> ("closeAllDialogs"),
|
|
1249
1623
|
InstanceMethod<&WstpSession::CreateSubsession>("createSubsession"),
|
|
1250
1624
|
InstanceMethod<&WstpSession::Close> ("close"),
|
|
1625
|
+
// Dynamic evaluation API (Phase 2)
|
|
1626
|
+
InstanceMethod<&WstpSession::RegisterDynamic> ("registerDynamic"),
|
|
1627
|
+
InstanceMethod<&WstpSession::UnregisterDynamic> ("unregisterDynamic"),
|
|
1628
|
+
InstanceMethod<&WstpSession::ClearDynamicRegistry>("clearDynamicRegistry"),
|
|
1629
|
+
InstanceMethod<&WstpSession::GetDynamicResults> ("getDynamicResults"),
|
|
1630
|
+
InstanceMethod<&WstpSession::SetDynamicInterval> ("setDynamicInterval"),
|
|
1631
|
+
InstanceMethod<&WstpSession::SetDynAutoMode> ("setDynAutoMode"),
|
|
1251
1632
|
InstanceAccessor<&WstpSession::IsOpen> ("isOpen"),
|
|
1252
1633
|
InstanceAccessor<&WstpSession::IsDialogOpen> ("isDialogOpen"),
|
|
1253
1634
|
InstanceAccessor<&WstpSession::IsReady> ("isReady"),
|
|
1635
|
+
InstanceAccessor<&WstpSession::KernelPid> ("kernelPid"),
|
|
1636
|
+
InstanceAccessor<&WstpSession::DynamicActive> ("dynamicActive"),
|
|
1254
1637
|
});
|
|
1255
1638
|
|
|
1256
1639
|
Napi::FunctionReference* ctor = new Napi::FunctionReference();
|
|
@@ -1308,7 +1691,10 @@ public:
|
|
|
1308
1691
|
DiagLog("[Session] restart attempt " + std::to_string(attempt) +
|
|
1309
1692
|
" — $Output routing broken on previous kernel");
|
|
1310
1693
|
WSClose(lp_); lp_ = nullptr;
|
|
1311
|
-
|
|
1694
|
+
// Kill the stale kernel before relaunching; use the same guard
|
|
1695
|
+
// as CleanUp() to avoid double-kill on already-dead process.
|
|
1696
|
+
if (kernelPid_ > 0 && !kernelKilled_) { kernelKilled_ = true; kill(kernelPid_, SIGTERM); }
|
|
1697
|
+
kernelKilled_ = false; // reset for the fresh kernel about to launch
|
|
1312
1698
|
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
1313
1699
|
}
|
|
1314
1700
|
|
|
@@ -1387,6 +1773,10 @@ public:
|
|
|
1387
1773
|
// Per-call interactive override: opts.interactive = true/false
|
|
1388
1774
|
if (optsObj.Has("interactive") && optsObj.Get("interactive").IsBoolean())
|
|
1389
1775
|
interactiveOverride = optsObj.Get("interactive").As<Napi::Boolean>().Value() ? 1 : 0;
|
|
1776
|
+
// Per-call rejectDialog: auto-close any BEGINDLGPKT without JS roundtrip.
|
|
1777
|
+
if (optsObj.Has("rejectDialog") && optsObj.Get("rejectDialog").IsBoolean() &&
|
|
1778
|
+
optsObj.Get("rejectDialog").As<Napi::Boolean>().Value())
|
|
1779
|
+
opts.rejectDialog = true;
|
|
1390
1780
|
|
|
1391
1781
|
// CompleteCtx: count = numTsfns + 1 (the extra slot is for OnOK/OnError).
|
|
1392
1782
|
// This ensures the Promise resolves ONLY after every TSFN has been
|
|
@@ -1416,6 +1806,15 @@ public:
|
|
|
1416
1806
|
opts.dialogPending = &dialogPending_;
|
|
1417
1807
|
opts.dialogOpen = &dialogOpen_;
|
|
1418
1808
|
opts.abortFlag = &abortFlag_;
|
|
1809
|
+
// Wire up Dynamic eval pointers so DrainToEvalResult can evaluate
|
|
1810
|
+
// registered Dynamic expressions inline when BEGINDLGPKT arrives.
|
|
1811
|
+
opts.dynMutex = &dynMutex_;
|
|
1812
|
+
opts.dynRegistry = &dynRegistry_;
|
|
1813
|
+
opts.dynResults = &dynResults_;
|
|
1814
|
+
opts.dynLastEval = &dynLastEval_;
|
|
1815
|
+
opts.dynAutoMode = dynAutoMode_.load();
|
|
1816
|
+
opts.dynIntervalMs = dynIntervalMs_.load();
|
|
1817
|
+
opts.dynTaskInstalledInterval = &dynTaskInstalledInterval_;
|
|
1419
1818
|
|
|
1420
1819
|
{
|
|
1421
1820
|
std::lock_guard<std::mutex> lk(queueMutex_);
|
|
@@ -1435,6 +1834,16 @@ public:
|
|
|
1435
1834
|
Napi::Promise::Deferred deferred;
|
|
1436
1835
|
};
|
|
1437
1836
|
|
|
1837
|
+
// Queue entry — one pending subWhenIdle() call (lowest priority).
|
|
1838
|
+
// Executed only when both subIdleQueue_ and queue_ are empty.
|
|
1839
|
+
struct QueuedWhenIdle {
|
|
1840
|
+
std::string expr;
|
|
1841
|
+
Napi::Promise::Deferred deferred;
|
|
1842
|
+
// Absolute deadline; time_point::max() = no timeout.
|
|
1843
|
+
std::chrono::steady_clock::time_point deadline =
|
|
1844
|
+
std::chrono::steady_clock::time_point::max();
|
|
1845
|
+
};
|
|
1846
|
+
|
|
1438
1847
|
// Sub-idle evals are preferred over normal evals so sub()-when-idle gets
|
|
1439
1848
|
// a quick result without waiting for a queued evaluate().
|
|
1440
1849
|
// -----------------------------------------------------------------------
|
|
@@ -1455,6 +1864,26 @@ public:
|
|
|
1455
1864
|
}
|
|
1456
1865
|
|
|
1457
1866
|
if (queue_.empty()) {
|
|
1867
|
+
// No normal evaluate() items — check lowest-priority when-idle queue.
|
|
1868
|
+
// Drain any timeout-expired entries first, then run the next live one.
|
|
1869
|
+
while (!whenIdleQueue_.empty()) {
|
|
1870
|
+
auto& front = whenIdleQueue_.front();
|
|
1871
|
+
bool expired =
|
|
1872
|
+
(front.deadline != std::chrono::steady_clock::time_point::max() &&
|
|
1873
|
+
std::chrono::steady_clock::now() >= front.deadline);
|
|
1874
|
+
if (expired) {
|
|
1875
|
+
Napi::Env wenv = front.deferred.Env();
|
|
1876
|
+
front.deferred.Reject(
|
|
1877
|
+
Napi::Error::New(wenv, "subWhenIdle: timeout").Value());
|
|
1878
|
+
whenIdleQueue_.pop();
|
|
1879
|
+
continue;
|
|
1880
|
+
}
|
|
1881
|
+
auto wiItem = std::move(whenIdleQueue_.front());
|
|
1882
|
+
whenIdleQueue_.pop();
|
|
1883
|
+
lk.unlock();
|
|
1884
|
+
StartWhenIdleWorker(std::move(wiItem));
|
|
1885
|
+
return;
|
|
1886
|
+
}
|
|
1458
1887
|
busy_.store(false);
|
|
1459
1888
|
return;
|
|
1460
1889
|
}
|
|
@@ -1537,6 +1966,63 @@ public:
|
|
|
1537
1966
|
[this]() { busy_.store(false); MaybeStartNext(); }))->Queue();
|
|
1538
1967
|
}
|
|
1539
1968
|
|
|
1969
|
+
// Launch a lightweight EvaluateWorker for one subWhenIdle() entry.
|
|
1970
|
+
// Identical to StartSubIdleWorker — reuses the same SubIdleWorker inner
|
|
1971
|
+
// class pattern; separated here so it can receive QueuedWhenIdle.
|
|
1972
|
+
void StartWhenIdleWorker(QueuedWhenIdle item) {
|
|
1973
|
+
struct WhenIdleWorker : public Napi::AsyncWorker {
|
|
1974
|
+
WhenIdleWorker(Napi::Promise::Deferred d, WSLINK lp, std::string expr,
|
|
1975
|
+
std::atomic<bool>& workerReadingLink,
|
|
1976
|
+
std::function<void()> done)
|
|
1977
|
+
: Napi::AsyncWorker(d.Env()),
|
|
1978
|
+
deferred_(std::move(d)), lp_(lp), expr_(std::move(expr)),
|
|
1979
|
+
workerReadingLink_(workerReadingLink), done_(std::move(done)) {}
|
|
1980
|
+
|
|
1981
|
+
void Execute() override {
|
|
1982
|
+
if (!WSPutFunction(lp_, "EvaluatePacket", 1) ||
|
|
1983
|
+
!WSPutFunction(lp_, "ToExpression", 1) ||
|
|
1984
|
+
!WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) ||
|
|
1985
|
+
!WSEndPacket(lp_) ||
|
|
1986
|
+
!WSFlush(lp_)) {
|
|
1987
|
+
workerReadingLink_.store(false, std::memory_order_release);
|
|
1988
|
+
SetError("subWhenIdle: failed to send EvaluatePacket");
|
|
1989
|
+
return;
|
|
1990
|
+
}
|
|
1991
|
+
result_ = DrainToEvalResult(lp_);
|
|
1992
|
+
workerReadingLink_.store(false, std::memory_order_release);
|
|
1993
|
+
}
|
|
1994
|
+
void OnOK() override {
|
|
1995
|
+
Napi::Env env = Env();
|
|
1996
|
+
if (result_.result.kind == WExpr::WError) {
|
|
1997
|
+
deferred_.Reject(Napi::Error::New(env, result_.result.strVal).Value());
|
|
1998
|
+
} else {
|
|
1999
|
+
Napi::Value v = WExprToNapi(env, result_.result);
|
|
2000
|
+
if (env.IsExceptionPending())
|
|
2001
|
+
deferred_.Reject(env.GetAndClearPendingException().Value());
|
|
2002
|
+
else
|
|
2003
|
+
deferred_.Resolve(v);
|
|
2004
|
+
}
|
|
2005
|
+
done_();
|
|
2006
|
+
}
|
|
2007
|
+
void OnError(const Napi::Error& e) override {
|
|
2008
|
+
deferred_.Reject(e.Value());
|
|
2009
|
+
done_();
|
|
2010
|
+
}
|
|
2011
|
+
private:
|
|
2012
|
+
Napi::Promise::Deferred deferred_;
|
|
2013
|
+
WSLINK lp_;
|
|
2014
|
+
std::string expr_;
|
|
2015
|
+
std::atomic<bool>& workerReadingLink_;
|
|
2016
|
+
std::function<void()> done_;
|
|
2017
|
+
EvalResult result_;
|
|
2018
|
+
};
|
|
2019
|
+
|
|
2020
|
+
workerReadingLink_.store(true, std::memory_order_release);
|
|
2021
|
+
(new WhenIdleWorker(std::move(item.deferred), lp_, std::move(item.expr),
|
|
2022
|
+
workerReadingLink_,
|
|
2023
|
+
[this]() { busy_.store(false); MaybeStartNext(); }))->Queue();
|
|
2024
|
+
}
|
|
2025
|
+
|
|
1540
2026
|
// -----------------------------------------------------------------------
|
|
1541
2027
|
// sub(expr) → Promise<WExpr>
|
|
1542
2028
|
//
|
|
@@ -1568,6 +2054,53 @@ public:
|
|
|
1568
2054
|
return promise;
|
|
1569
2055
|
}
|
|
1570
2056
|
|
|
2057
|
+
// -----------------------------------------------------------------------
|
|
2058
|
+
// subWhenIdle(expr, opts?) → Promise<WExpr>
|
|
2059
|
+
//
|
|
2060
|
+
// Queues a lightweight evaluation at the LOWEST priority: it runs only
|
|
2061
|
+
// when both subIdleQueue_ and queue_ are empty (kernel truly idle).
|
|
2062
|
+
// Ideal for background queries that must not compete with cell evaluations.
|
|
2063
|
+
//
|
|
2064
|
+
// opts may contain:
|
|
2065
|
+
// timeout?: number — milliseconds to wait before the Promise rejects
|
|
2066
|
+
// -----------------------------------------------------------------------
|
|
2067
|
+
Napi::Value SubWhenIdle(const Napi::CallbackInfo& info) {
|
|
2068
|
+
Napi::Env env = info.Env();
|
|
2069
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
2070
|
+
auto promise = deferred.Promise();
|
|
2071
|
+
|
|
2072
|
+
if (!open_) {
|
|
2073
|
+
deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
|
|
2074
|
+
return promise;
|
|
2075
|
+
}
|
|
2076
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
2077
|
+
deferred.Reject(Napi::TypeError::New(env,
|
|
2078
|
+
"subWhenIdle(expr: string, opts?: {timeout?: number})").Value());
|
|
2079
|
+
return promise;
|
|
2080
|
+
}
|
|
2081
|
+
std::string expr = info[0].As<Napi::String>().Utf8Value();
|
|
2082
|
+
|
|
2083
|
+
// Parse optional timeout from opts object.
|
|
2084
|
+
auto deadline = std::chrono::steady_clock::time_point::max();
|
|
2085
|
+
if (info.Length() >= 2 && info[1].IsObject()) {
|
|
2086
|
+
auto optsObj = info[1].As<Napi::Object>();
|
|
2087
|
+
if (optsObj.Has("timeout") && optsObj.Get("timeout").IsNumber()) {
|
|
2088
|
+
double ms = optsObj.Get("timeout").As<Napi::Number>().DoubleValue();
|
|
2089
|
+
if (ms > 0)
|
|
2090
|
+
deadline = std::chrono::steady_clock::now() +
|
|
2091
|
+
std::chrono::milliseconds(static_cast<int64_t>(ms));
|
|
2092
|
+
}
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
{
|
|
2096
|
+
std::lock_guard<std::mutex> lk(queueMutex_);
|
|
2097
|
+
whenIdleQueue_.push(QueuedWhenIdle{
|
|
2098
|
+
std::move(expr), std::move(deferred), deadline });
|
|
2099
|
+
}
|
|
2100
|
+
MaybeStartNext();
|
|
2101
|
+
return promise;
|
|
2102
|
+
}
|
|
2103
|
+
|
|
1571
2104
|
// -----------------------------------------------------------------------
|
|
1572
2105
|
// exitDialog(retVal?) → Promise<void>
|
|
1573
2106
|
//
|
|
@@ -1713,16 +2246,132 @@ public:
|
|
|
1713
2246
|
}
|
|
1714
2247
|
|
|
1715
2248
|
// -----------------------------------------------------------------------
|
|
1716
|
-
//
|
|
1717
|
-
//
|
|
1718
|
-
//
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1723
|
-
|
|
1724
|
-
|
|
2249
|
+
// registerDynamic(id, expr) → void
|
|
2250
|
+
// Register or replace a Dynamic expression for periodic evaluation.
|
|
2251
|
+
// -----------------------------------------------------------------------
|
|
2252
|
+
Napi::Value RegisterDynamic(const Napi::CallbackInfo& info) {
|
|
2253
|
+
Napi::Env env = info.Env();
|
|
2254
|
+
if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
|
|
2255
|
+
Napi::TypeError::New(env, "registerDynamic(id: string, expr: string)")
|
|
2256
|
+
.ThrowAsJavaScriptException();
|
|
2257
|
+
return env.Undefined();
|
|
2258
|
+
}
|
|
2259
|
+
std::string id = info[0].As<Napi::String>().Utf8Value();
|
|
2260
|
+
std::string expr = info[1].As<Napi::String>().Utf8Value();
|
|
2261
|
+
{
|
|
2262
|
+
std::lock_guard<std::mutex> lk(dynMutex_);
|
|
2263
|
+
for (auto& reg : dynRegistry_) {
|
|
2264
|
+
if (reg.id == id) { reg.expr = expr; return env.Undefined(); }
|
|
2265
|
+
}
|
|
2266
|
+
dynRegistry_.push_back({id, expr});
|
|
2267
|
+
}
|
|
2268
|
+
return env.Undefined();
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
// -----------------------------------------------------------------------
|
|
2272
|
+
// unregisterDynamic(id) → void
|
|
2273
|
+
// -----------------------------------------------------------------------
|
|
2274
|
+
Napi::Value UnregisterDynamic(const Napi::CallbackInfo& info) {
|
|
2275
|
+
Napi::Env env = info.Env();
|
|
2276
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
2277
|
+
Napi::TypeError::New(env, "unregisterDynamic(id: string)")
|
|
2278
|
+
.ThrowAsJavaScriptException();
|
|
2279
|
+
return env.Undefined();
|
|
2280
|
+
}
|
|
2281
|
+
std::string id = info[0].As<Napi::String>().Utf8Value();
|
|
2282
|
+
{
|
|
2283
|
+
std::lock_guard<std::mutex> lk(dynMutex_);
|
|
2284
|
+
dynRegistry_.erase(
|
|
2285
|
+
std::remove_if(dynRegistry_.begin(), dynRegistry_.end(),
|
|
2286
|
+
[&id](const DynRegistration& r){ return r.id == id; }),
|
|
2287
|
+
dynRegistry_.end());
|
|
2288
|
+
}
|
|
2289
|
+
return env.Undefined();
|
|
2290
|
+
}
|
|
2291
|
+
|
|
2292
|
+
// -----------------------------------------------------------------------
|
|
2293
|
+
// clearDynamicRegistry() → void
|
|
2294
|
+
// -----------------------------------------------------------------------
|
|
2295
|
+
Napi::Value ClearDynamicRegistry(const Napi::CallbackInfo& info) {
|
|
2296
|
+
std::lock_guard<std::mutex> lk(dynMutex_);
|
|
2297
|
+
dynRegistry_.clear();
|
|
2298
|
+
dynResults_.clear();
|
|
2299
|
+
return info.Env().Undefined();
|
|
2300
|
+
}
|
|
2301
|
+
|
|
1725
2302
|
// -----------------------------------------------------------------------
|
|
2303
|
+
// getDynamicResults() → Record<string, DynResult>
|
|
2304
|
+
// Swaps and returns accumulated results; clears the internal buffer.
|
|
2305
|
+
// -----------------------------------------------------------------------
|
|
2306
|
+
Napi::Value GetDynamicResults(const Napi::CallbackInfo& info) {
|
|
2307
|
+
Napi::Env env = info.Env();
|
|
2308
|
+
std::vector<DynResult> snap;
|
|
2309
|
+
{
|
|
2310
|
+
std::lock_guard<std::mutex> lk(dynMutex_);
|
|
2311
|
+
snap.swap(dynResults_);
|
|
2312
|
+
}
|
|
2313
|
+
auto obj = Napi::Object::New(env);
|
|
2314
|
+
for (const auto& dr : snap) {
|
|
2315
|
+
auto entry = Napi::Object::New(env);
|
|
2316
|
+
entry.Set("value", Napi::String::New(env, dr.value));
|
|
2317
|
+
entry.Set("timestamp", Napi::Number::New(env, dr.timestamp));
|
|
2318
|
+
if (!dr.error.empty())
|
|
2319
|
+
entry.Set("error", Napi::String::New(env, dr.error));
|
|
2320
|
+
obj.Set(dr.id, entry);
|
|
2321
|
+
}
|
|
2322
|
+
return obj;
|
|
2323
|
+
}
|
|
2324
|
+
|
|
2325
|
+
// -----------------------------------------------------------------------
|
|
2326
|
+
// setDynamicInterval(ms) → void
|
|
2327
|
+
// Set the auto-interrupt interval. 0 = disabled.
|
|
2328
|
+
// Starting/stopping the timer thread is handled here.
|
|
2329
|
+
// -----------------------------------------------------------------------
|
|
2330
|
+
Napi::Value SetDynamicInterval(const Napi::CallbackInfo& info) {
|
|
2331
|
+
Napi::Env env = info.Env();
|
|
2332
|
+
if (info.Length() < 1 || !info[0].IsNumber()) {
|
|
2333
|
+
Napi::TypeError::New(env, "setDynamicInterval(ms: number)")
|
|
2334
|
+
.ThrowAsJavaScriptException();
|
|
2335
|
+
return env.Undefined();
|
|
2336
|
+
}
|
|
2337
|
+
int ms = static_cast<int>(info[0].As<Napi::Number>().Int32Value());
|
|
2338
|
+
if (ms < 0) ms = 0;
|
|
2339
|
+
int prev = dynIntervalMs_.exchange(ms);
|
|
2340
|
+
// Start timer thread if transitioning from 0 to non-zero.
|
|
2341
|
+
if (prev == 0 && ms > 0 && !dynTimerRunning_.load()) {
|
|
2342
|
+
StartDynTimer();
|
|
2343
|
+
}
|
|
2344
|
+
return env.Undefined();
|
|
2345
|
+
}
|
|
2346
|
+
|
|
2347
|
+
// -----------------------------------------------------------------------
|
|
2348
|
+
// setDynAutoMode(auto) → void
|
|
2349
|
+
// true = C++-internal inline dialog path (default)
|
|
2350
|
+
// false = legacy JS dialogEval/exitDialog path (for debugger)
|
|
2351
|
+
// -----------------------------------------------------------------------
|
|
2352
|
+
Napi::Value SetDynAutoMode(const Napi::CallbackInfo& info) {
|
|
2353
|
+
Napi::Env env = info.Env();
|
|
2354
|
+
if (info.Length() < 1 || !info[0].IsBoolean()) {
|
|
2355
|
+
Napi::TypeError::New(env, "setDynAutoMode(auto: boolean)")
|
|
2356
|
+
.ThrowAsJavaScriptException();
|
|
2357
|
+
return env.Undefined();
|
|
2358
|
+
}
|
|
2359
|
+
dynAutoMode_.store(info[0].As<Napi::Boolean>().Value());
|
|
2360
|
+
return env.Undefined();
|
|
2361
|
+
}
|
|
2362
|
+
|
|
2363
|
+
// -----------------------------------------------------------------------
|
|
2364
|
+
// dynamicActive (accessor) → boolean
|
|
2365
|
+
// True if registry non-empty and interval > 0.
|
|
2366
|
+
// -----------------------------------------------------------------------
|
|
2367
|
+
Napi::Value DynamicActive(const Napi::CallbackInfo& info) {
|
|
2368
|
+
std::lock_guard<std::mutex> lk(dynMutex_);
|
|
2369
|
+
bool active = !dynRegistry_.empty() && dynIntervalMs_.load() > 0;
|
|
2370
|
+
return Napi::Boolean::New(info.Env(), active);
|
|
2371
|
+
}
|
|
2372
|
+
|
|
2373
|
+
// -----------------------------------------------------------------------
|
|
2374
|
+
// abort() — interrupt the currently running evaluation.
|
|
1726
2375
|
Napi::Value Abort(const Napi::CallbackInfo& info) {
|
|
1727
2376
|
Napi::Env env = info.Env();
|
|
1728
2377
|
if (!open_) return Napi::Boolean::New(env, false);
|
|
@@ -1730,7 +2379,12 @@ public:
|
|
|
1730
2379
|
// Sending WSAbortMessage to an idle kernel causes it to emit a
|
|
1731
2380
|
// spurious RETURNPKT[$Aborted] that would corrupt the next evaluation.
|
|
1732
2381
|
if (!busy_.load()) return Napi::Boolean::New(env, false);
|
|
1733
|
-
abortFlag_
|
|
2382
|
+
// Deduplication: if abortFlag_ is already true, another abort() is already
|
|
2383
|
+
// in flight — sending a second WSAbortMessage causes stale response packets
|
|
2384
|
+
// that corrupt the next evaluation. Just return true (already aborting).
|
|
2385
|
+
bool expected = false;
|
|
2386
|
+
if (!abortFlag_.compare_exchange_strong(expected, true))
|
|
2387
|
+
return Napi::Boolean::New(env, true); // already aborting — no-op
|
|
1734
2388
|
// Flush any queued dialogEval/exitDialog requests so their promises
|
|
1735
2389
|
// reject immediately instead of hanging forever.
|
|
1736
2390
|
FlushDialogQueueWithError("abort");
|
|
@@ -1816,6 +2470,17 @@ public:
|
|
|
1816
2470
|
&& subIdleQueue_.empty());
|
|
1817
2471
|
}
|
|
1818
2472
|
|
|
2473
|
+
// -----------------------------------------------------------------------
|
|
2474
|
+
// kernelPid (read-only accessor)
|
|
2475
|
+
// Returns the OS process ID of the WolframKernel child process.
|
|
2476
|
+
// Returns 0 if the PID could not be fetched (non-fatal, rare fallback).
|
|
2477
|
+
// The PID can be used by the caller to monitor or force-terminate the
|
|
2478
|
+
// kernel process independently of the WSTP link (e.g. after a restart).
|
|
2479
|
+
// -----------------------------------------------------------------------
|
|
2480
|
+
Napi::Value KernelPid(const Napi::CallbackInfo& info) {
|
|
2481
|
+
return Napi::Number::New(info.Env(), static_cast<double>(kernelPid_));
|
|
2482
|
+
}
|
|
2483
|
+
|
|
1819
2484
|
private:
|
|
1820
2485
|
// Queue entry — one pending evaluate() call.
|
|
1821
2486
|
// interactiveOverride: -1 = use session default, 0 = force batch, 1 = force interactive
|
|
@@ -1851,7 +2516,57 @@ private:
|
|
|
1851
2516
|
}
|
|
1852
2517
|
}
|
|
1853
2518
|
|
|
2519
|
+
// -----------------------------------------------------------------------
|
|
2520
|
+
// StartDynTimer — launches the background timer thread that sends
|
|
2521
|
+
// WSInterruptMessage at the configured interval when the kernel is busy
|
|
2522
|
+
// and Dynamic expressions are registered.
|
|
2523
|
+
// Called on the JS main thread; harmless to call if already running.
|
|
2524
|
+
// -----------------------------------------------------------------------
|
|
2525
|
+
void StartDynTimer() {
|
|
2526
|
+
if (dynTimerRunning_.exchange(true)) return; // already running
|
|
2527
|
+
if (dynTimerThread_.joinable()) dynTimerThread_.join();
|
|
2528
|
+
dynTimerThread_ = std::thread([this]() {
|
|
2529
|
+
while (open_ && dynIntervalMs_.load() > 0) {
|
|
2530
|
+
int ms = dynIntervalMs_.load();
|
|
2531
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(ms > 0 ? ms : 100));
|
|
2532
|
+
if (!open_) break;
|
|
2533
|
+
if (!busy_.load()) continue; // kernel idle
|
|
2534
|
+
{
|
|
2535
|
+
std::lock_guard<std::mutex> lk(dynMutex_);
|
|
2536
|
+
if (dynRegistry_.empty()) continue; // nothing registered
|
|
2537
|
+
}
|
|
2538
|
+
if (dynIntervalMs_.load() == 0) break;
|
|
2539
|
+
// Check enough time has elapsed since last eval.
|
|
2540
|
+
auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
2541
|
+
std::chrono::steady_clock::now() - dynLastEval_).count();
|
|
2542
|
+
if (elapsed < dynIntervalMs_.load()) continue;
|
|
2543
|
+
|
|
2544
|
+
if (workerReadingLink_.load() && !dialogOpen_.load() && !dynAutoMode_.load()) {
|
|
2545
|
+
WSPutMessage(lp_, WSInterruptMessage);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
dynTimerRunning_.store(false);
|
|
2549
|
+
});
|
|
2550
|
+
dynTimerThread_.detach();
|
|
2551
|
+
}
|
|
2552
|
+
|
|
1854
2553
|
void CleanUp() {
|
|
2554
|
+
// Stop the Dynamic timer thread.
|
|
2555
|
+
dynIntervalMs_.store(0);
|
|
2556
|
+
dynTimerRunning_.store(false);
|
|
2557
|
+
|
|
2558
|
+
// Immediately reject all queued subWhenIdle() requests before the link
|
|
2559
|
+
// is torn down. These items have never been dispatched to a worker so
|
|
2560
|
+
// they won't receive an OnError callback — we must reject them here.
|
|
2561
|
+
{
|
|
2562
|
+
std::lock_guard<std::mutex> lk(queueMutex_);
|
|
2563
|
+
while (!whenIdleQueue_.empty()) {
|
|
2564
|
+
auto& wi = whenIdleQueue_.front();
|
|
2565
|
+
wi.deferred.Reject(
|
|
2566
|
+
Napi::Error::New(wi.deferred.Env(), "Session is closed").Value());
|
|
2567
|
+
whenIdleQueue_.pop();
|
|
2568
|
+
}
|
|
2569
|
+
}
|
|
1855
2570
|
// If a worker thread is currently reading from lp_, calling WSClose()
|
|
1856
2571
|
// on it from the JS main thread causes a concurrent-access crash
|
|
1857
2572
|
// (heap-use-after-free / SIGSEGV).
|
|
@@ -1877,7 +2592,11 @@ private:
|
|
|
1877
2592
|
// Kill the child kernel process so it doesn't become a zombie.
|
|
1878
2593
|
// WSClose() closes the link but does not terminate the WolframKernel
|
|
1879
2594
|
// child process — without this, each session leaks a kernel.
|
|
1880
|
-
|
|
2595
|
+
// kernelPid_ is intentionally NOT zeroed here so .kernelPid remains
|
|
2596
|
+
// readable after close() (useful for post-mortem logging / force-kill).
|
|
2597
|
+
// kernelKilled_ prevents a double-kill when the destructor later calls
|
|
2598
|
+
// CleanUp() a second time.
|
|
2599
|
+
if (kernelPid_ > 0 && !kernelKilled_) { kernelKilled_ = true; kill(kernelPid_, SIGTERM); }
|
|
1881
2600
|
}
|
|
1882
2601
|
|
|
1883
2602
|
|
|
@@ -1971,7 +2690,8 @@ private:
|
|
|
1971
2690
|
WSLINK lp_;
|
|
1972
2691
|
bool open_;
|
|
1973
2692
|
bool interactiveMode_ = false; // true → EnterTextPacket, populates In/Out
|
|
1974
|
-
pid_t kernelPid_ = 0; // child process —
|
|
2693
|
+
pid_t kernelPid_ = 0; // child process PID — preserved after close() for .kernelPid accessor
|
|
2694
|
+
bool kernelKilled_ = false; // guards against double-kill across close() + destructor
|
|
1975
2695
|
std::atomic<int64_t> nextLine_{1}; // 1-based In[n] counter for EvalResult.cellIndex
|
|
1976
2696
|
std::atomic<bool> abortFlag_{false};
|
|
1977
2697
|
std::atomic<bool> busy_{false};
|
|
@@ -1983,11 +2703,25 @@ private:
|
|
|
1983
2703
|
std::mutex queueMutex_;
|
|
1984
2704
|
std::queue<QueuedEval> queue_;
|
|
1985
2705
|
std::queue<QueuedSubIdle> subIdleQueue_; // sub() — runs before queue_ items
|
|
2706
|
+
std::queue<QueuedWhenIdle> whenIdleQueue_; // subWhenIdle() — lowest priority, runs when truly idle
|
|
1986
2707
|
// Dialog subsession state — written on main thread, consumed on thread pool
|
|
1987
2708
|
std::mutex dialogMutex_;
|
|
1988
2709
|
std::queue<DialogRequest> dialogQueue_; // dialogEval() requests
|
|
1989
2710
|
std::atomic<bool> dialogPending_{false};
|
|
1990
2711
|
std::atomic<bool> dialogOpen_{false};
|
|
2712
|
+
|
|
2713
|
+
// -----------------------------------------------------------------------
|
|
2714
|
+
// Dynamic evaluation state (Phase 2 — C++-internal Dynamic eval)
|
|
2715
|
+
// -----------------------------------------------------------------------
|
|
2716
|
+
std::mutex dynMutex_;
|
|
2717
|
+
std::vector<DynRegistration> dynRegistry_; // registered exprs
|
|
2718
|
+
std::vector<DynResult> dynResults_; // accumulated results (swapped on getDynamicResults)
|
|
2719
|
+
std::atomic<int> dynIntervalMs_{0}; // 0 = disabled
|
|
2720
|
+
std::atomic<bool> dynAutoMode_{true}; // true = inline C++ path; false = legacy JS path
|
|
2721
|
+
int dynTaskInstalledInterval_{0}; // interval of currently installed ScheduledTask (0 = none)
|
|
2722
|
+
std::chrono::steady_clock::time_point dynLastEval_{}; // time of last successful dialog eval
|
|
2723
|
+
std::thread dynTimerThread_;
|
|
2724
|
+
std::atomic<bool> dynTimerRunning_{false};
|
|
1991
2725
|
};
|
|
1992
2726
|
|
|
1993
2727
|
// ===========================================================================
|