wstp-node 0.6.3 → 0.7.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/src/addon.cc CHANGED
@@ -1,3096 +1,24 @@
1
1
  // =============================================================================
2
- // wstp-backend/src/addon.cc (v6dialog subsession support)
3
- //
4
- // Architecture:
5
- // Execute() runs on the libuv thread pool → does ALL blocking WSTP I/O,
6
- // stores result in plain C++ structs (WExpr / EvalResult, no NAPI).
7
- // OnOK() runs on the JS main thread → converts to Napi::Value.
8
- //
9
- // EvalResult captures everything the kernel sends for one cell:
10
- // cellIndex (1-based, tracked by WstpSession::nextLine_), outputName,
11
- // return value, Print[] lines, and messages.
12
- // This means the Node.js event loop is NEVER blocked, so abort() fires
13
- // correctly while Execute() is waiting inside WSNextPacket() on the thread pool.
2
+ // wstp-backend/src/addon.cc (v0.7.0thin entry point after refactor)
3
+ //
4
+ // This file is intentionally minimal. All implementation lives in the
5
+ // other source files listed in binding.gyp:
6
+ // diag.cc — diagnostic logging channel
7
+ // wstp_expr.cc — WSTP expression reading + Napi conversion
8
+ // drain.cc — packet-draining helpers + DrainToEvalResult
9
+ // evaluate_worker.cc EvaluateWorker async worker
10
+ // wstp_session.cc — WstpSession class (main kernel session)
11
+ // wstp_reader.cc — WstpReader + ReadNextWorker (connect-mode reader)
14
12
  // =============================================================================
15
13
 
16
- #include <napi.h>
17
- #include <wstp.h>
18
-
19
- #include <atomic>
20
- #include <chrono>
21
- #include <cstdint>
22
- #include <thread>
23
- #include <functional>
24
- #include <memory>
25
- #include <mutex>
26
- #include <queue>
27
- #include <string>
28
- #include <thread>
29
- #include <vector>
30
-
31
- #include <signal.h> // kill(), SIGTERM
32
- #include <sys/types.h>
33
-
34
- // ===========================================================================
35
- // Plain C++ expression tree — no NAPI, safe to build on any thread.
36
- // ===========================================================================
37
- struct WExpr {
38
- enum Kind { Integer, Real, String, Symbol, Function, WError } kind = WError;
39
-
40
- int64_t intVal = 0;
41
- double realVal = 0.0;
42
- std::string strVal; // string content, symbol name, or error msg
43
- std::string head; // function head symbol
44
- std::vector<WExpr> args; // function arguments
45
-
46
- static WExpr mkError(const std::string& msg) {
47
- WExpr e; e.kind = WError; e.strVal = msg; return e;
48
- }
49
- static WExpr mkSymbol(const std::string& name) {
50
- WExpr e; e.kind = Symbol; e.strVal = name; return e;
51
- }
52
- };
53
-
54
- // ---------------------------------------------------------------------------
55
- // EvalResult — everything the kernel sends for one cell (thread-pool safe).
56
- // ---------------------------------------------------------------------------
57
- struct EvalResult {
58
- int64_t cellIndex = 0; // 1-based counter, tracked by WstpSession::nextLine_
59
- std::string outputName; // "Out[n]=" when result is non-Null, "" otherwise
60
- WExpr result; // the ReturnPacket payload
61
- std::vector<std::string> print; // TextPacket lines in order
62
- std::vector<std::string> messages; // e.g. "Power::infy: Infinite expression..."
63
- bool aborted = false;
64
- };
65
-
66
- // ---------------------------------------------------------------------------
67
- // EvalOptions — optional streaming callbacks for one evaluate() call.
68
- // ThreadSafeFunctions (TSFNs) are safe to call from any thread, which is
69
- // what makes streaming from Execute() (thread pool) possible.
70
- // ---------------------------------------------------------------------------
71
-
72
- // Latch that fires fn() once after ALL parties have called done().
73
- // remaining is initialised to numTsfns + 1 (the +1 is for OnOK / OnError).
74
- // IMPORTANT: all calls happen on the main thread — no atomics needed.
75
- struct CompleteCtx {
76
- int remaining;
77
- std::function<void()> fn; // set by OnOK/OnError; called when remaining → 0
78
- void done() {
79
- if (--remaining == 0) { if (fn) fn(); delete this; }
80
- }
81
- };
82
-
83
- // ---------------------------------------------------------------------------
84
- // DialogRequest — one dialogEval() call queued for the thread pool.
85
- // Written on the main thread; consumed exclusively on the thread pool while
86
- // the dialog inner loop is running. The TSFN delivers the result back.
87
- // ---------------------------------------------------------------------------
88
- struct DialogRequest {
89
- std::string expr;
90
- bool useEnterText = false; // true → EnterTextPacket; false → EvaluatePacket
91
- Napi::ThreadSafeFunction resolve; // NonBlockingCall'd with the WExpr result
92
- };
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
-
109
- struct EvalOptions {
110
- Napi::ThreadSafeFunction onPrint; // fires once per Print[] line
111
- Napi::ThreadSafeFunction onMessage; // fires once per kernel message
112
- Napi::ThreadSafeFunction onDialogBegin; // fires with dialog level (int)
113
- Napi::ThreadSafeFunction onDialogPrint; // fires with dialog Print[] lines
114
- Napi::ThreadSafeFunction onDialogEnd; // fires with dialog level (int)
115
- bool hasOnPrint = false;
116
- bool hasOnMessage = false;
117
- bool hasOnDialogBegin = false;
118
- bool hasOnDialogPrint = false;
119
- bool hasOnDialogEnd = false;
120
- // When true, DrainToEvalResult expects EnterExpressionPacket protocol:
121
- // INPUTNAMEPKT → OUTPUTNAMEPKT → RETURNEXPRPKT (or INPUTNAMEPKT for Null).
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
138
- CompleteCtx* ctx = nullptr; // non-owning; set when TSFNs are in use
139
-
140
- // Pointers to session's dialog queue — set by WstpSession::Evaluate() so the
141
- // drain loop can service dialogEval() requests from the thread pool.
142
- // Non-owning; valid for the lifetime of the EvaluateWorker.
143
- std::mutex* dialogMutex = nullptr;
144
- std::queue<DialogRequest>* dialogQueue = nullptr;
145
- std::atomic<bool>* dialogPending = nullptr;
146
- std::atomic<bool>* dialogOpen = nullptr;
147
- // Session-level abort flag — set by abort() on the main thread; checked in
148
- // the dialog inner loop to break out proactively when abort() is called.
149
- std::atomic<bool>* abortFlag = nullptr;
150
- };
151
-
152
- // ===========================================================================
153
- // Module-level diagnostic channel.
154
- // setDiagHandler(fn) registers a JS callback; DiagLog(msg) fires it from any
155
- // C++ thread. Both the global flag and the TSFN are guarded by g_diagMutex.
156
- // The TSFN is Unref()'d so it does not prevent the Node.js event loop from
157
- // exiting normally.
158
- // ===========================================================================
159
- static std::mutex g_diagMutex;
160
- static Napi::ThreadSafeFunction g_diagTsfn;
161
- static bool g_diagActive = false;
162
-
163
- // If DEBUG_WSTP=1 is set in the environment at module-load time, every
164
- // DiagLog message is also written synchronously to stderr via fwrite.
165
- // Useful when no JS setDiagHandler is registered (e.g. bare node runs).
166
- static const bool g_debugToStderr = []() {
167
- const char* v = getenv("DEBUG_WSTP");
168
- return v && v[0] == '1';
169
- }();
170
-
171
- // Module-relative timestamp helper — milliseconds since module load.
172
- // Used to embed C++ dispatch times in log messages so JS-side handler
173
- // timestamps can be compared to measure TSFN delivery latency.
174
- static auto g_startTime = std::chrono::steady_clock::now();
175
- static long long diagMs() {
176
- return std::chrono::duration_cast<std::chrono::milliseconds>(
177
- std::chrono::steady_clock::now() - g_startTime).count();
178
- }
179
-
180
- static void DiagLog(const std::string& msg) {
181
- if (g_debugToStderr) {
182
- std::string out = "[wstp +" + std::to_string(diagMs()) + "ms] " + msg + "\n";
183
- fwrite(out.c_str(), 1, out.size(), stderr);
184
- }
185
- std::lock_guard<std::mutex> lk(g_diagMutex);
186
- if (!g_diagActive) return;
187
- // NonBlockingCall with lambda — copies msg by value into the captured closure.
188
- g_diagTsfn.NonBlockingCall(
189
- [msg](Napi::Env env, Napi::Function fn) {
190
- fn.Call({ Napi::String::New(env, msg) });
191
- });
192
- }
193
-
194
- // ---------------------------------------------------------------------------
195
- // ReadExprRaw — build a WExpr from one WSTP token/expression (any thread).
196
- // ---------------------------------------------------------------------------
197
- static WExpr ReadExprRaw(WSLINK lp, int depth = 0) {
198
- if (depth > 512) return WExpr::mkError("expression too deep");
199
-
200
- int type = WSGetType(lp);
201
-
202
- if (type == WSTKINT) {
203
- wsint64 i = 0;
204
- if (!WSGetInteger64(lp, &i))
205
- return WExpr::mkError("WSGetInteger64 failed");
206
- WExpr e; e.kind = WExpr::Integer; e.intVal = i;
207
- return e;
208
- }
209
- if (type == WSTKREAL) {
210
- double d = 0.0;
211
- if (!WSGetReal64(lp, &d))
212
- return WExpr::mkError("WSGetReal64 failed");
213
- WExpr e; e.kind = WExpr::Real; e.realVal = d;
214
- return e;
215
- }
216
- if (type == WSTKSTR) {
217
- const char* s = nullptr;
218
- if (!WSGetString(lp, &s))
219
- return WExpr::mkError("WSGetString failed");
220
- WExpr e; e.kind = WExpr::String; e.strVal = s;
221
- WSReleaseString(lp, s);
222
- return e;
223
- }
224
- if (type == WSTKSYM) {
225
- const char* s = nullptr;
226
- if (!WSGetSymbol(lp, &s))
227
- return WExpr::mkError("WSGetSymbol failed");
228
- WExpr e; e.kind = WExpr::Symbol; e.strVal = s;
229
- WSReleaseSymbol(lp, s);
230
- return e;
231
- }
232
- if (type == WSTKFUNC) {
233
- const char* head = nullptr;
234
- int argc = 0;
235
- if (!WSGetFunction(lp, &head, &argc))
236
- return WExpr::mkError("WSGetFunction failed");
237
- WExpr e;
238
- e.kind = WExpr::Function;
239
- e.head = head;
240
- WSReleaseSymbol(lp, head);
241
- e.args.reserve(argc);
242
- for (int i = 0; i < argc; ++i) {
243
- WExpr child = ReadExprRaw(lp, depth + 1);
244
- if (child.kind == WExpr::WError) return child;
245
- e.args.push_back(std::move(child));
246
- }
247
- return e;
248
- }
249
- return WExpr::mkError("unexpected token type: " + std::to_string(type));
250
- }
251
-
252
- // Forward declaration — WExprToNapi is defined after WstpSession.
253
- static Napi::Value WExprToNapi(Napi::Env env, const WExpr& e);
254
-
255
- // ---------------------------------------------------------------------------
256
- // drainDialogAbortResponse — drain the WSTP link after aborting out of a
257
- // dialog inner loop.
258
- //
259
- // When abort() fires proactively (abortFlag_ is true before the kernel has
260
- // sent its response), WSAbortMessage has already been posted but the kernel's
261
- // abort response — RETURNPKT[$Aborted] or ILLEGALPKT — has not yet been read.
262
- // Leaving it on the link corrupts the next evaluation: it becomes the first
263
- // packet seen by the next DrainToEvalResult call, which resolves immediately
264
- // with $Aborted and leaves the real response on the link — permanently
265
- // degrading the session and disabling subsequent interrupts.
266
- //
267
- // Reads and discards packets until RETURNPKT, RETURNEXPRPKT, ILLEGALPKT, or a
268
- // 10-second wall-clock deadline. Intermediate packets (ENDDLGPKT, TEXTPKT,
269
- // MESSAGEPKT, MENUPKT, etc.) are silently consumed.
270
- // ---------------------------------------------------------------------------
271
- static void drainDialogAbortResponse(WSLINK lp) {
272
- const auto deadline =
273
- std::chrono::steady_clock::now() + std::chrono::seconds(10);
274
- while (std::chrono::steady_clock::now() < deadline) {
275
- // Poll until data is available or the deadline passes.
276
- while (std::chrono::steady_clock::now() < deadline) {
277
- if (WSReady(lp)) break;
278
- std::this_thread::sleep_for(std::chrono::milliseconds(10));
279
- }
280
- if (!WSReady(lp)) return; // timed out — give up
281
- int pkt = WSNextPacket(lp);
282
- if (pkt == RETURNPKT || pkt == RETURNEXPRPKT) {
283
- WSNewPacket(lp);
284
- return; // outer-eval abort response consumed — link is clean
285
- }
286
- if (pkt == ILLEGALPKT || pkt == 0) {
287
- WSClearError(lp);
288
- WSNewPacket(lp);
289
- return; // link reset — clean
290
- }
291
- WSNewPacket(lp); // discard intermediate packet (ENDDLGPKT, MENUPKT, …)
292
- }
293
- }
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
-
499
- // ---------------------------------------------------------------------------
500
- // DrainToEvalResult — consume all packets for one cell, capturing Print[]
501
- // output and messages. Blocks until RETURNPKT. Thread-pool thread only.
502
- // opts may be nullptr (no streaming callbacks).
503
- // ---------------------------------------------------------------------------
504
- static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
505
- EvalResult r;
506
-
507
- // Parse "In[42]:=" → 42
508
- auto parseCellIndex = [](const std::string& s) -> int64_t {
509
- auto a = s.find('[');
510
- auto b = s.find(']');
511
- if (a == std::string::npos || b == std::string::npos) return 0;
512
- try { return std::stoll(s.substr(a + 1, b - a - 1)); }
513
- catch (...) { return 0; }
514
- };
515
-
516
- // Strip "Context`" prefix from fully-qualified symbol names.
517
- // WSTP sends e.g. "System`MessageName" or "System`Power"; we want the bare name.
518
- auto stripCtx = [](const std::string& s) -> std::string {
519
- auto p = s.rfind('`');
520
- return p != std::string::npos ? s.substr(p + 1) : s;
521
- };
522
-
523
- // Remove trailing newline(s) that Print[] appends to TextPacket content.
524
- // WSTP delivers actual '\n' bytes on some platforms and the 4-char literal
525
- // "\012" (backslash + '0' + '1' + '2') on others (Wolfram's octal escape).
526
- auto rtrimNL = [](std::string s) -> std::string {
527
- // Strip actual ASCII newlines / carriage returns
528
- while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
529
- s.pop_back();
530
- // Strip Wolfram's 4-char literal octal escape "\\012"
531
- while (s.size() >= 4 && s.compare(s.size() - 4, 4, "\\012") == 0)
532
- s.resize(s.size() - 4);
533
- return s;
534
- };
535
-
536
- // -----------------------------------------------------------------------
537
- // Helper: handle a MESSAGEPKT (already current) — shared by outer and
538
- // dialog inner loops. Reads the follow-up TEXTPKT, extracts the message
539
- // string, appends it to r.messages, and fires the onMessage TSFN.
540
- // -----------------------------------------------------------------------
541
- auto handleMessage = [&](bool forDialog) {
542
- WSNewPacket(lp); // discard message-name expression
543
- if (WSNextPacket(lp) == TEXTPKT) {
544
- const char* s = nullptr; WSGetString(lp, &s);
545
- if (s) {
546
- std::string text = s;
547
- WSReleaseString(lp, s);
548
- static const std::string NL = "\\012";
549
- std::string msg;
550
- // Extract the message starting from the Symbol::tag position.
551
- // Wolfram's TEXTPKT for MESSAGEPKT may contain multiple \012-separated
552
- // sections (e.g. the tag on one line, the body on subsequent lines).
553
- // Old code stopped at the first \012 after ::, which truncated long
554
- // messages like NIntegrate::ncvb to just their tag.
555
- // Fixed: take the whole text from the start of the Symbol name,
556
- // stripping only leading/trailing \012 groups, and replacing
557
- // internal \012 with spaces so the full message is one readable line.
558
- auto dc = text.find("::");
559
- size_t raw_start = 0;
560
- if (dc != std::string::npos) {
561
- auto nl_before = text.rfind(NL, dc);
562
- raw_start = (nl_before != std::string::npos) ? nl_before + 4 : 0;
563
- }
564
- // Strip trailing \012 sequences from the raw text
565
- std::string raw = text.substr(raw_start);
566
- while (raw.size() >= 4 && raw.compare(raw.size() - 4, 4, NL) == 0)
567
- raw.resize(raw.size() - 4);
568
- // Strip leading spaces
569
- size_t sp = 0;
570
- while (sp < raw.size() && raw[sp] == ' ') ++sp;
571
- raw = raw.substr(sp);
572
- // Replace all remaining \012 with a single space
573
- for (size_t i = 0; i < raw.size(); ) {
574
- if (raw.compare(i, 4, NL) == 0) { msg += ' '; i += 4; }
575
- else { msg += raw[i++]; }
576
- }
577
- if (!forDialog) {
578
- r.messages.push_back(msg);
579
- if (opts && opts->hasOnMessage)
580
- opts->onMessage.NonBlockingCall(
581
- [msg](Napi::Env e, Napi::Function cb){
582
- cb.Call({Napi::String::New(e, msg)}); });
583
- } else {
584
- // Dialog messages: still append to outer result so nothing is lost.
585
- r.messages.push_back(msg);
586
- if (opts && opts->hasOnMessage)
587
- opts->onMessage.NonBlockingCall(
588
- [msg](Napi::Env e, Napi::Function cb){
589
- cb.Call({Napi::String::New(e, msg)}); });
590
- }
591
- }
592
- }
593
- WSNewPacket(lp);
594
- };
595
-
596
- // -----------------------------------------------------------------------
597
- // Helper: service one pending DialogRequest from dialogQueue_.
598
- //
599
- // menuDlgProto = false (default, BEGINDLGPKT path):
600
- // Sends EvaluatePacket[ToExpression[...]], drains until RETURNPKT.
601
- //
602
- // menuDlgProto = true (MENUPKT-dialog path, interrupt-triggered Dialog[]):
603
- // Sends EnterExpressionPacket[ToExpression[...]], drains until RETURNEXPRPKT.
604
- // The kernel uses MENUPKT as the dialog-prompt between evaluations.
605
- //
606
- // Returns true → dialog still open.
607
- // Returns false → dialog closed (ENDDLGPKT or exitDialog via MENUPKT 'c').
608
- // -----------------------------------------------------------------------
609
- auto serviceDialogRequest = [&](bool menuDlgProto = false) -> bool {
610
- DialogRequest req;
611
- {
612
- std::lock_guard<std::mutex> lk(*opts->dialogMutex);
613
- if (opts->dialogQueue->empty()) return true;
614
- req = std::move(opts->dialogQueue->front());
615
- opts->dialogQueue->pop();
616
- if (opts->dialogQueue->empty())
617
- opts->dialogPending->store(false);
618
- }
619
- // Send the expression to the kernel's dialog REPL.
620
- // menuDlgProto / EnterExpressionPacket — interrupt-triggered Dialog[] context
621
- // EvaluatePacket — BEGINDLGPKT Dialog[] context
622
- // EnterTextPacket — exitDialog (Return[] in interactive ctx)
623
- bool sent;
624
- if (!req.useEnterText) {
625
- if (menuDlgProto) {
626
- // MENUPKT-dialog (interrupt-triggered): text-mode I/O.
627
- // The kernel expects EnterTextPacket and returns OutputForm via TEXTPKT.
628
- sent = WSPutFunction(lp, "EnterTextPacket", 1) &&
629
- WSPutUTF8String(lp, (const unsigned char *)req.expr.c_str(), (int)req.expr.size()) &&
630
- WSEndPacket (lp) &&
631
- WSFlush (lp);
632
- } else {
633
- // BEGINDLGPKT dialog: batch mode.
634
- sent = WSPutFunction(lp, "EvaluatePacket", 1) &&
635
- WSPutFunction(lp, "ToExpression", 1) &&
636
- WSPutUTF8String(lp, (const unsigned char *)req.expr.c_str(), (int)req.expr.size()) &&
637
- WSEndPacket (lp) &&
638
- WSFlush (lp);
639
- }
640
- } else {
641
- sent = WSPutFunction(lp, "EnterTextPacket", 1) &&
642
- WSPutUTF8String(lp, (const unsigned char *)req.expr.c_str(), (int)req.expr.size()) &&
643
- WSEndPacket (lp) &&
644
- WSFlush (lp);
645
- }
646
- if (!sent) {
647
- // Send failure: resolve with a WError.
648
- WExpr err = WExpr::mkError("dialogEval: failed to send to kernel");
649
- req.resolve.NonBlockingCall(
650
- [err](Napi::Env e, Napi::Function cb){
651
- Napi::Object o = Napi::Object::New(e);
652
- o.Set("type", Napi::String::New(e, "error"));
653
- o.Set("error", Napi::String::New(e, err.strVal));
654
- cb.Call({o});
655
- });
656
- req.resolve.Release();
657
- return true; // link might still work; let outer loop decide
658
- }
659
- // Read response packets until RETURNPKT/RETURNEXPRPKT or ENDDLGPKT.
660
- WExpr result;
661
- std::string lastDlgText; // accumulated OutputForm text for menuDlgProto dialogEval
662
- bool dialogEndedHere = false;
663
- bool menuDlgFirstSkipped = false; // whether the pre-result setup MENUPKT was already skipped
664
- DiagLog("[SDR] waiting for response, menuDlgProto=" + std::to_string(menuDlgProto)
665
- + " useEnterText=" + std::to_string(req.useEnterText));
666
- while (true) {
667
- int p2 = WSNextPacket(lp);
668
- DiagLog("[SDR] p2=" + std::to_string(p2));
669
- if (p2 == RETURNPKT) {
670
- result = ReadExprRaw(lp);
671
- WSNewPacket(lp);
672
- break;
673
- }
674
- if (p2 == RETURNTEXTPKT) {
675
- // ReturnTextPacket: the dialog/inspect-mode result as OutputForm text.
676
- // This is how 'i' (inspect) mode returns results in Wolfram 3.
677
- const char* s = nullptr; WSGetString(lp, &s);
678
- std::string txt = s ? rtrimNL(s) : ""; if (s) WSReleaseString(lp, s);
679
- WSNewPacket(lp);
680
- DiagLog("[SDR] RETURNTEXTPKT text='" + txt.substr(0, 60) + "'");
681
- if (txt.empty()) {
682
- result = WExpr::mkSymbol("System`Null");
683
- } else {
684
- // Try integer
685
- try {
686
- size_t pos = 0;
687
- long long iv = std::stoll(txt, &pos);
688
- if (pos == txt.size()) { result.kind = WExpr::Integer; result.intVal = iv; }
689
- else { throw std::exception(); }
690
- } catch (...) {
691
- // Try real
692
- try {
693
- size_t pos = 0;
694
- double rv = std::stod(txt, &pos);
695
- if (pos == txt.size()) { result.kind = WExpr::Real; result.realVal = rv; }
696
- else { throw std::exception(); }
697
- } catch (...) {
698
- result.kind = WExpr::String; result.strVal = txt;
699
- }
700
- }
701
- }
702
- break;
703
- }
704
- if (p2 == RETURNEXPRPKT) {
705
- // EnterExpressionPacket path (menuDlgProto): collect structured result.
706
- // The kernel will follow with INPUTNAMEPKT or MENUPKT (next prompt);
707
- // we break after collecting and let the outer loop consume that.
708
- result = ReadExprRaw(lp);
709
- WSNewPacket(lp);
710
- if (!menuDlgProto) break; // BEGINDLGPKT: also terminates
711
- // menuDlgProto: keep looping to consume OUTPUTNAMEPKT/INPUTNAMEPKT
712
- // and the trailing MENUPKT (which triggers the outer break below)
713
- continue;
714
- }
715
- if (p2 == ENDDLGPKT) {
716
- // The expression exited the dialog (e.g. Return[]).
717
- // Handle ENDDLGPKT here so the outer inner-loop doesn't see it.
718
- wsint64 endLevel = 0;
719
- if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &endLevel);
720
- WSNewPacket(lp);
721
- if (opts->dialogOpen) opts->dialogOpen->store(false);
722
- if (opts->hasOnDialogEnd)
723
- opts->onDialogEnd.NonBlockingCall(
724
- [endLevel](Napi::Env e, Napi::Function cb){
725
- cb.Call({Napi::Number::New(e, static_cast<double>(endLevel))}); });
726
- // Resolve with Null — the caller asked for Return[], gets Null back.
727
- result.kind = WExpr::Symbol;
728
- result.strVal = "Null";
729
- dialogEndedHere = true;
730
- break;
731
- }
732
- if (p2 == MESSAGEPKT) { handleMessage(true); continue; }
733
- if (p2 == TEXTPKT) {
734
- const char* s = nullptr; WSGetString(lp, &s);
735
- if (s) {
736
- std::string line = rtrimNL(s); WSReleaseString(lp, s);
737
- if (menuDlgProto && !req.useEnterText) {
738
- DiagLog("[SDR] TEXTPKT(menuDlg) text='" + line + "'");
739
- if (!line.empty()) {
740
- if (!lastDlgText.empty()) lastDlgText += "\n";
741
- lastDlgText += line;
742
- }
743
- } else {
744
- if (opts->hasOnDialogPrint)
745
- opts->onDialogPrint.NonBlockingCall(
746
- [line](Napi::Env e, Napi::Function cb){
747
- cb.Call({Napi::String::New(e, line)}); });
748
- }
749
- }
750
- WSNewPacket(lp); continue;
751
- }
752
- if (p2 == INPUTNAMEPKT || p2 == OUTPUTNAMEPKT) { WSNewPacket(lp); continue; }
753
- if (p2 == MENUPKT) {
754
- // MENUPKT in dialog context:
755
- // * menuDlgProto && exitDialog: respond 'c' to resume main eval.
756
- // * menuDlgProto && dialogEval: result arrived via TEXTPKT; parse it.
757
- // * BEGINDLGPKT path: result via RETURNPKT (should already have it).
758
- if (req.useEnterText) {
759
- // exitDialog: respond bare string 'c' (continue) per JLink MENUPKT protocol.
760
- wsint64 menuType2_ = 0; WSGetInteger64(lp, &menuType2_);
761
- const char* menuPrompt2_ = nullptr; WSGetString(lp, &menuPrompt2_);
762
- if (menuPrompt2_) WSReleaseString(lp, menuPrompt2_);
763
- WSNewPacket(lp);
764
- WSPutString(lp, "c");
765
- WSEndPacket(lp);
766
- WSFlush(lp);
767
- if (opts->dialogOpen) opts->dialogOpen->store(false);
768
- if (opts->hasOnDialogEnd)
769
- opts->onDialogEnd.NonBlockingCall(
770
- [](Napi::Env e, Napi::Function cb){
771
- cb.Call({Napi::Number::New(e, 0.0)}); });
772
- result.kind = WExpr::Symbol;
773
- result.strVal = "Null";
774
- dialogEndedHere = true;
775
- } else if (menuDlgProto) {
776
- // dialogEval in text-mode dialog: result arrives as TEXTPKT before MENUPKT.
777
- // However: the kernel may send a "setup" MENUPKT immediately after we
778
- // send our expression (buffered from the dialog-open sequence), before
779
- // sending the TEXTPKT result. Skip that one MENUPKT and keep waiting.
780
- WSNewPacket(lp);
781
- if (lastDlgText.empty() && !menuDlgFirstSkipped) {
782
- menuDlgFirstSkipped = true;
783
- DiagLog("[SDR] menuDlg: skipping pre-result MENUPKT, waiting for TEXTPKT");
784
- continue; // keep waiting for TEXTPKT result
785
- }
786
- // Second MENUPKT (or first if we already have text): end of result.
787
- DiagLog("[SDR] menuDlg: end-of-result MENUPKT, lastDlgText='" + lastDlgText + "'");
788
- if (lastDlgText.empty()) {
789
- result = WExpr::mkSymbol("System`Null");
790
- } else {
791
- // Try integer
792
- try {
793
- size_t pos = 0;
794
- long long iv = std::stoll(lastDlgText, &pos);
795
- if (pos == lastDlgText.size()) {
796
- result.kind = WExpr::Integer;
797
- result.intVal = iv;
798
- } else { throw std::exception(); }
799
- } catch (...) {
800
- // Try real
801
- try {
802
- size_t pos = 0;
803
- double rv = std::stod(lastDlgText, &pos);
804
- if (pos == lastDlgText.size()) {
805
- result.kind = WExpr::Real;
806
- result.realVal = rv;
807
- } else { throw std::exception(); }
808
- } catch (...) {
809
- result.kind = WExpr::String;
810
- result.strVal = lastDlgText;
811
- }
812
- }
813
- }
814
- } else {
815
- // BEGINDLGPKT path: result should have arrived via RETURNPKT.
816
- WSNewPacket(lp);
817
- if (result.kind == WExpr::WError)
818
- result = WExpr::mkSymbol("System`Null");
819
- }
820
- break;
821
- }
822
- if (p2 == 0 || p2 == ILLEGALPKT) {
823
- const char* m = WSErrorMessage(lp); WSClearError(lp);
824
- result = WExpr::mkError(m ? m : "WSTP error in dialogEval");
825
- break;
826
- }
827
- WSNewPacket(lp);
828
- }
829
- // Deliver result to the JS Promise via TSFN.
830
- WExpr res = result;
831
- req.resolve.NonBlockingCall(
832
- [res](Napi::Env e, Napi::Function cb){ cb.Call({WExprToNapi(e, res)}); });
833
- req.resolve.Release();
834
- return !dialogEndedHere;
835
- };
836
-
837
- // -----------------------------------------------------------------------
838
- // Outer drain loop — blocking WSNextPacket (unchanged for normal evals).
839
- // Dialog packets trigger the inner loop below.
840
- // -----------------------------------------------------------------------
841
- // In EnterExpressionPacket mode the kernel sends:
842
- // Non-Null result: OUTPUTNAMEPKT → RETURNEXPRPKT
843
- // followed by a trailing INPUTNAMEPKT (next prompt)
844
- // Null result: INPUTNAMEPKT only (no OUTPUTNAMEPKT/RETURNEXPRPKT)
845
- //
846
- // So: INPUTNAMEPKT as the FIRST packet (before any OUTPUTNAMEPKT) = Null.
847
- // INPUTNAMEPKT after RETURNEXPRPKT = next-prompt trailer, consumed here
848
- // so the next evaluate() starts clean.
849
- bool gotOutputName = false;
850
- bool gotResult = false;
851
- while (true) {
852
- int pkt = WSNextPacket(lp);
853
- DiagLog("[Eval] pkt=" + std::to_string(pkt));
854
-
855
- if (pkt == RETURNPKT || pkt == RETURNEXPRPKT) {
856
- // RETURNPKT: response to EvaluatePacket — no In/Out populated.
857
- // RETURNEXPRPKT: response to EnterExpressionPacket — main loop ran,
858
- // In[n] and Out[n] are populated by the kernel.
859
- //
860
- // Safety for interactive RETURNEXPRPKT: peek at the token type before
861
- // calling ReadExprRaw. Atomic results (symbol, int, real, string) are
862
- // safe to read — this covers $Aborted and simple values.
863
- // Complex results (List, Graphics, …) are skipped with WSNewPacket to
864
- // avoid a potential WSGetFunction crash on deep expression trees.
865
- // Out[n] is already set inside the kernel; JS renders via VsCodeRenderNth
866
- // which reads from $vsCodeLastResultList, not from the transferred result.
867
- if (opts && opts->interactive && pkt == RETURNEXPRPKT) {
868
- int tok = WSGetType(lp);
869
- if (tok == WSTKSYM || tok == WSTKINT || tok == WSTKREAL || tok == WSTKSTR) {
870
- r.result = ReadExprRaw(lp);
871
- } else {
872
- // Complex result — discard; Out[n] intact in kernel.
873
- r.result = WExpr::mkSymbol("System`__VsCodeHasResult__");
874
- }
875
- } else {
876
- r.result = ReadExprRaw(lp);
877
- }
878
- WSNewPacket(lp);
879
- if (r.result.kind == WExpr::Symbol && stripCtx(r.result.strVal) == "$Aborted")
880
- r.aborted = true;
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
- //
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.
897
- }
898
- else if (pkt == INPUTNAMEPKT) {
899
- const char* s = nullptr; WSGetString(lp, &s);
900
- std::string nameStr = s ? s : "";
901
- if (s) WSReleaseString(lp, s);
902
- WSNewPacket(lp);
903
- if (opts && opts->interactive) {
904
- if (!gotOutputName && !gotResult) {
905
- // First packet = INPUTNAMEPKT with no preceding OUTPUTNAMEPKT:
906
- // kernel evaluated to Null/suppressed — this IS the result signal.
907
- // cellIndex is one less than this prompt's index.
908
- int64_t nextIdx = parseCellIndex(nameStr);
909
- r.cellIndex = (nextIdx > 1) ? nextIdx - 1 : nextIdx;
910
- r.result = WExpr::mkSymbol("System`Null");
911
- break;
912
- }
913
- // Trailing INPUTNAMEPKT after RETURNEXPRPKT — consume and exit.
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);
917
- break;
918
- }
919
- r.cellIndex = parseCellIndex(nameStr);
920
- }
921
- else if (pkt == OUTPUTNAMEPKT) {
922
- const char* s = nullptr; WSGetString(lp, &s);
923
- if (s) {
924
- std::string name = s; WSReleaseString(lp, s);
925
- // Kernel sends "Out[n]= " with trailing space — normalize to "Out[n]=".
926
- while (!name.empty() && name.back() == ' ') name.pop_back();
927
- r.outputName = name;
928
- // Parse the n from "Out[n]=" to set cellIndex (interactive mode).
929
- r.cellIndex = parseCellIndex(name);
930
- }
931
- WSNewPacket(lp);
932
- gotOutputName = true;
933
- }
934
- else if (pkt == TEXTPKT) {
935
- const char* s = nullptr; WSGetString(lp, &s);
936
- if (s) {
937
- std::string line = rtrimNL(s);
938
- WSReleaseString(lp, s);
939
- DiagLog("[Eval] TEXTPKT content='" + line.substr(0, 60) + "'");
940
- r.print.emplace_back(line);
941
- if (opts && opts->hasOnPrint) {
942
- // Log C++ dispatch time so the JS handler timestamp gives latency.
943
- DiagLog("[TSFN][onPrint] dispatch +" + std::to_string(diagMs())
944
- + "ms \"" + line.substr(0, 30) + "\"");
945
- opts->onPrint.NonBlockingCall(
946
- [line](Napi::Env env, Napi::Function cb){
947
- cb.Call({ Napi::String::New(env, line) }); });
948
- }
949
- }
950
- WSNewPacket(lp);
951
- }
952
- else if (pkt == MESSAGEPKT) {
953
- handleMessage(false);
954
- }
955
- else if (pkt == BEGINDLGPKT) {
956
- // ----------------------------------------------------------------
957
- // Dialog subsession opened by the kernel.
958
- // ----------------------------------------------------------------
959
- wsint64 level = 0;
960
- if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &level);
961
- WSNewPacket(lp);
962
-
963
- // ---- rejectDialog: auto-close without informing JS layer --------
964
- // Non-interactive evals (VsCodeRender, handler install, sub() exprs)
965
- // must never block waiting for JS dialogEval() that never comes.
966
- // Send Return[$Failed] immediately and continue waiting for RETURNPKT.
967
- if (opts && opts->rejectDialog) {
968
- DiagLog("[Eval] rejectDialog: auto-closing BEGINDLGPKT level=" + std::to_string(level));
969
- // Pre-drain INPUTNAMEPKT — Dialog[] from ScheduledTask sends
970
- // INPUTNAMEPKT before accepting EnterTextPacket.
971
- {
972
- auto preDl = std::chrono::steady_clock::now() +
973
- std::chrono::milliseconds(500);
974
- while (std::chrono::steady_clock::now() < preDl) {
975
- if (!WSReady(lp)) {
976
- std::this_thread::sleep_for(std::chrono::milliseconds(2));
977
- continue;
978
- }
979
- int ipkt = WSNextPacket(lp);
980
- DiagLog("[Eval] rejectDialog: pre-drain pkt=" + std::to_string(ipkt));
981
- WSNewPacket(lp);
982
- if (ipkt == INPUTNAMEPKT) break;
983
- if (ipkt == 0 || ipkt == ILLEGALPKT) { WSClearError(lp); break; }
984
- }
985
- }
986
- const char* closeExpr = "Return[$Failed]";
987
- WSPutFunction(lp, "EnterTextPacket", 1);
988
- WSPutUTF8String(lp,
989
- reinterpret_cast<const unsigned char*>(closeExpr),
990
- static_cast<int>(std::strlen(closeExpr)));
991
- WSEndPacket(lp);
992
- WSFlush(lp);
993
- DiagLog("[Eval] rejectDialog: sent Return[$Failed], draining until ENDDLGPKT");
994
- // Drain until ENDDLGPKT — kernel will close the dialog level.
995
- {
996
- auto dlgDeadline = std::chrono::steady_clock::now() +
997
- std::chrono::milliseconds(2000);
998
- while (std::chrono::steady_clock::now() < dlgDeadline) {
999
- if (!WSReady(lp)) {
1000
- std::this_thread::sleep_for(std::chrono::milliseconds(2));
1001
- continue;
1002
- }
1003
- int rp = WSNextPacket(lp);
1004
- DiagLog("[Eval] rejectDialog: drain pkt=" + std::to_string(rp));
1005
- WSNewPacket(lp);
1006
- if (rp == ENDDLGPKT) break;
1007
- if (rp == 0 || rp == ILLEGALPKT) { WSClearError(lp); break; }
1008
- }
1009
- }
1010
- // Continue outer drain loop — the original RETURNPKT is still coming.
1011
- continue;
1012
- }
1013
-
1014
- // ---- dynAutoMode: C++-internal Dynamic evaluation ---------------
1015
- // When dynAutoMode is true, all registered Dynamic expressions are
1016
- // evaluated inline inside the dialog — no JS roundtrip needed.
1017
- // The dialog is then closed unconditionally with Return[$Failed].
1018
- if (!opts || opts->dynAutoMode) {
1019
- // Check if aborted before entering evaluation.
1020
- if (opts && opts->abortFlag && opts->abortFlag->load()) {
1021
- if (opts && opts->dialogOpen) opts->dialogOpen->store(false);
1022
- r.result = WExpr::mkSymbol("System`$Aborted");
1023
- r.aborted = true;
1024
- drainDialogAbortResponse(lp);
1025
- return r;
1026
- }
1027
-
1028
- // If the outer eval's EvaluatePacket was processed inside
1029
- // this Dialog[] context (between-eval ScheduledTask fire),
1030
- // its RETURNPKT will appear before our RETURNTEXTPKT.
1031
- // capturedOuterResult captures that value so we can return
1032
- // it as the cell result.
1033
- WExpr capturedOuterResult;
1034
-
1035
- if (opts && opts->dynMutex && opts->dynRegistry && opts->dynResults) {
1036
- std::lock_guard<std::mutex> lk(*opts->dynMutex);
1037
-
1038
- // Drain any initial packets (INPUTNAMEPKT, etc.) before
1039
- // sending expressions. Dialog[] from ScheduledTask may
1040
- // send INPUTNAMEPKT or TEXTPKT before accepting input.
1041
- {
1042
- auto drainDl = std::chrono::steady_clock::now() +
1043
- std::chrono::milliseconds(500);
1044
- while (std::chrono::steady_clock::now() < drainDl) {
1045
- if (!WSReady(lp)) {
1046
- std::this_thread::sleep_for(std::chrono::milliseconds(2));
1047
- continue;
1048
- }
1049
- int ipkt = WSNextPacket(lp);
1050
- DiagLog("[Eval] dynAutoMode(BEGINDLG): pre-drain pkt=" + std::to_string(ipkt));
1051
- if (ipkt == INPUTNAMEPKT) {
1052
- WSNewPacket(lp);
1053
- break; // ready for input
1054
- }
1055
- if (ipkt == 0 || ipkt == ILLEGALPKT) {
1056
- WSClearError(lp);
1057
- break;
1058
- }
1059
- WSNewPacket(lp); // consume TEXTPKT, MESSAGEPKT, etc.
1060
- }
1061
- }
1062
-
1063
- // Current time in ms since epoch for timestamps.
1064
- auto nowMs = static_cast<double>(
1065
- std::chrono::duration_cast<std::chrono::milliseconds>(
1066
- std::chrono::system_clock::now().time_since_epoch())
1067
- .count());
1068
-
1069
- for (const auto& reg : *opts->dynRegistry) {
1070
- // Send expression via EnterTextPacket so the kernel
1071
- // evaluates it in OutputForm text mode (string result).
1072
- DiagLog("[Eval] dynAutoMode(BEGINDLG): sending expr id=" + reg.id + " expr='" + reg.expr + "'");
1073
- bool sentDyn =
1074
- WSPutFunction(lp, "EnterTextPacket", 1) &&
1075
- WSPutUTF8String(lp,
1076
- reinterpret_cast<const unsigned char*>(reg.expr.c_str()),
1077
- static_cast<int>(reg.expr.size())) &&
1078
- WSEndPacket(lp) &&
1079
- WSFlush(lp);
1080
-
1081
- DynResult dr;
1082
- dr.id = reg.id;
1083
- dr.timestamp = nowMs;
1084
- if (!sentDyn) {
1085
- dr.error = "failed to send Dynamic expression";
1086
- } else {
1087
- readDynResultWithTimeout(lp, dr, 2000, &capturedOuterResult);
1088
- }
1089
- opts->dynResults->push_back(std::move(dr));
14
+ #include "diag.h"
15
+ #include "wstp_session.h"
16
+ #include "wstp_reader.h"
1090
17
 
1091
- // If the outer result was captured, stop processing
1092
- // further dynamic expressions — the eval is done.
1093
- if (capturedOuterResult.kind != WExpr::WError ||
1094
- !capturedOuterResult.strVal.empty()) break;
1095
-
1096
- // Abort check between expressions.
1097
- if (opts->abortFlag && opts->abortFlag->load()) break;
1098
- }
1099
-
1100
- if (opts->dynLastEval)
1101
- *opts->dynLastEval = std::chrono::steady_clock::now();
1102
-
1103
- // Check if the outer eval's RETURNPKT was captured inside
1104
- // this dialog. If so, close the dialog and return the
1105
- // captured result directly — the outer eval is already done.
1106
- bool outerCaptured = capturedOuterResult.kind != WExpr::WError ||
1107
- !capturedOuterResult.strVal.empty();
1108
- if (outerCaptured) {
1109
- DiagLog("[Eval] dynAutoMode: outer RETURNPKT captured inside dialog — returning directly");
1110
- // Close the dialog.
1111
- {
1112
- const char* closeExpr = "Return[$Failed]";
1113
- WSPutFunction(lp, "EnterTextPacket", 1);
1114
- WSPutUTF8String(lp,
1115
- reinterpret_cast<const unsigned char*>(closeExpr),
1116
- static_cast<int>(std::strlen(closeExpr)));
1117
- WSEndPacket(lp);
1118
- WSFlush(lp);
1119
- }
1120
- drainUntilEndDialog(lp, 3000);
1121
- r.result = std::move(capturedOuterResult);
1122
- if (r.result.kind == WExpr::Symbol &&
1123
- stripCtx(r.result.strVal) == "$Aborted")
1124
- r.aborted = true;
1125
- // Drain any remaining stale packets.
1126
- drainStalePackets(lp, opts);
1127
- return r;
1128
- }
1129
- }
1130
-
1131
- // Close the dialog: send Return[$Failed] then drain ENDDLGPKT.
1132
- {
1133
- const char* closeExpr = "Return[$Failed]";
1134
- WSPutFunction(lp, "EnterTextPacket", 1);
1135
- WSPutUTF8String(lp,
1136
- reinterpret_cast<const unsigned char*>(closeExpr),
1137
- static_cast<int>(std::strlen(closeExpr)));
1138
- WSEndPacket(lp);
1139
- WSFlush(lp);
1140
- }
1141
- bool exitOk = drainUntilEndDialog(lp, 3000, &capturedOuterResult);
1142
- if (!exitOk) {
1143
- DiagLog("[Eval] dynAutoMode: drainUntilEndDialog timed out; aborting");
1144
- r.aborted = true;
1145
- r.result = WExpr::mkSymbol("System`$Aborted");
1146
- drainDialogAbortResponse(lp);
1147
- return r;
1148
- }
1149
- // Check if the outer eval's RETURNPKT was captured during the
1150
- // drain (e.g. empty registry but between-eval Dialog[] race).
1151
- {
1152
- bool outerCapturedInDrain =
1153
- capturedOuterResult.kind != WExpr::WError ||
1154
- !capturedOuterResult.strVal.empty();
1155
- if (outerCapturedInDrain) {
1156
- DiagLog("[Eval] dynAutoMode: outer RETURNPKT captured during drain — returning directly");
1157
- r.result = std::move(capturedOuterResult);
1158
- if (r.result.kind == WExpr::Symbol &&
1159
- stripCtx(r.result.strVal) == "$Aborted")
1160
- r.aborted = true;
1161
- drainStalePackets(lp, opts);
1162
- return r;
1163
- }
1164
- }
1165
- // Dialog closed — continue outer loop waiting for the original RETURNPKT.
1166
- continue;
1167
- }
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
-
1223
- if (opts && opts->dialogOpen)
1224
- opts->dialogOpen->store(true);
1225
- if (opts && opts->hasOnDialogBegin)
1226
- opts->onDialogBegin.NonBlockingCall(
1227
- [level](Napi::Env e, Napi::Function cb){
1228
- cb.Call({Napi::Number::New(e, static_cast<double>(level))}); });
1229
-
1230
- // ----------------------------------------------------------------
1231
- // Dialog inner loop — WSReady-gated so dialogQueue_ can be serviced
1232
- // between kernel packets without blocking indefinitely.
1233
- // ----------------------------------------------------------------
1234
- bool dialogDone = false;
1235
- while (!dialogDone) {
1236
- // Abort check — abort() sets abortFlag_ and sends WSAbortMessage.
1237
- // The kernel may be slow to respond (or send RETURNPKT[$Aborted]
1238
- // without a preceding ENDDLGPKT); bail out proactively to avoid
1239
- // spinning forever.
1240
- // CRITICAL: always drain the link before returning. If we exit
1241
- // here before the kernel sends its RETURNPKT[$Aborted], that
1242
- // response stays on the link and corrupts the next evaluation.
1243
- if (opts && opts->abortFlag && opts->abortFlag->load()) {
1244
- if (opts->dialogOpen) opts->dialogOpen->store(false);
1245
- r.result = WExpr::mkSymbol("System`$Aborted");
1246
- r.aborted = true;
1247
- drainDialogAbortResponse(lp); // consume pending abort response
1248
- return r;
1249
- }
1250
-
1251
- // Service any pending dialogEval() requests first.
1252
- if (opts && opts->dialogPending && opts->dialogPending->load()) {
1253
- if (!serviceDialogRequest()) {
1254
- dialogDone = true; // ENDDLGPKT arrived inside the request
1255
- continue;
1256
- }
1257
- }
1258
-
1259
- // Non-blocking check: is the kernel ready to send a packet?
1260
- if (!WSReady(lp)) {
1261
- std::this_thread::sleep_for(std::chrono::milliseconds(2));
1262
- continue;
1263
- }
1264
-
1265
- int dpkt = WSNextPacket(lp);
1266
- DiagLog("[Dialog] dpkt=" + std::to_string(dpkt));
1267
- if (dpkt == ENDDLGPKT) {
1268
- wsint64 endLevel = 0;
1269
- if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &endLevel);
1270
- WSNewPacket(lp);
1271
- if (opts && opts->dialogOpen)
1272
- opts->dialogOpen->store(false);
1273
- if (opts && opts->hasOnDialogEnd)
1274
- opts->onDialogEnd.NonBlockingCall(
1275
- [endLevel](Napi::Env e, Napi::Function cb){
1276
- cb.Call({Napi::Number::New(e, static_cast<double>(endLevel))}); });
1277
- dialogDone = true;
1278
- }
1279
- else if (dpkt == RETURNPKT) {
1280
- // Could be the abort response (RETURNPKT[$Aborted] without a
1281
- // preceding ENDDLGPKT) or an unsolicited in-dialog result.
1282
- WExpr innerExpr = ReadExprRaw(lp);
1283
- WSNewPacket(lp);
1284
- if (innerExpr.kind == WExpr::Symbol &&
1285
- stripCtx(innerExpr.strVal) == "$Aborted") {
1286
- // Kernel ended the dialog due to abort — propagate upward.
1287
- if (opts && opts->dialogOpen) opts->dialogOpen->store(false);
1288
- r.result = innerExpr; // already the $Aborted symbol
1289
- r.aborted = true;
1290
- return r;
1291
- }
1292
- // Otherwise discard — outer loop will see final RETURNPKT.
1293
- }
1294
- else if (dpkt == INPUTNAMEPKT || dpkt == OUTPUTNAMEPKT) {
1295
- WSNewPacket(lp); // dialog prompts — discard
1296
- }
1297
- else if (dpkt == TEXTPKT) {
1298
- DiagLog("[Dialog] TEXTPKT");
1299
- const char* s = nullptr; WSGetString(lp, &s);
1300
- if (s) {
1301
- std::string line = rtrimNL(s); WSReleaseString(lp, s);
1302
- if (opts && opts->hasOnDialogPrint)
1303
- opts->onDialogPrint.NonBlockingCall(
1304
- [line](Napi::Env e, Napi::Function cb){
1305
- cb.Call({Napi::String::New(e, line)}); });
1306
- }
1307
- WSNewPacket(lp);
1308
- }
1309
- else if (dpkt == MESSAGEPKT) {
1310
- handleMessage(true);
1311
- }
1312
- else if (dpkt == 0 || dpkt == ILLEGALPKT) {
1313
- const char* m = WSErrorMessage(lp);
1314
- WSClearError(lp);
1315
- if (opts && opts->dialogOpen) opts->dialogOpen->store(false);
1316
- // If abort() was in flight, the ILLEGALPKT is the expected
1317
- // link-reset response — treat as clean abort, not an error.
1318
- if (opts && opts->abortFlag && opts->abortFlag->load()) {
1319
- r.result = WExpr::mkSymbol("System`$Aborted");
1320
- r.aborted = true;
1321
- } else {
1322
- r.result = WExpr::mkError(m ? m : "WSTP error in dialog");
1323
- }
1324
- return r; // unrecoverable — bail out entirely
1325
- }
1326
- else {
1327
- WSNewPacket(lp);
1328
- }
1329
- }
1330
- // Dialog closed — continue outer loop waiting for the original RETURNPKT.
1331
- }
1332
- else if (pkt == 0 || pkt == ILLEGALPKT) {
1333
- const char* m = WSErrorMessage(lp);
1334
- std::string s = m ? m : "WSTP link error";
1335
- WSClearError(lp);
1336
- r.result = WExpr::mkError(s);
1337
- break;
1338
- }
1339
- else if (pkt == RETURNTEXTPKT) {
1340
- // ReturnTextPacket carries the string-form of the result.
1341
- // This should not normally appear with EvaluatePacket, but handle
1342
- // it defensively so the loop always terminates.
1343
- DiagLog("[Eval] unexpected RETURNTEXTPKT — treating as empty return");
1344
- WSNewPacket(lp);
1345
- // Leave r.result as default WError so the caller gets an informative error.
1346
- r.result = WExpr::mkError("unexpected ReturnTextPacket from kernel");
1347
- break;
1348
- }
1349
- else if (pkt == MENUPKT) {
1350
- // ----------------------------------------------------------------
1351
- // MENUPKT (6) — interrupt menu
1352
- // ----------------------------------------------------------------
1353
- wsint64 menuType_ = 0; WSGetInteger64(lp, &menuType_);
1354
- const char* menuPrompt_ = nullptr; WSGetString(lp, &menuPrompt_);
1355
- if (menuPrompt_) WSReleaseString(lp, menuPrompt_);
1356
- WSNewPacket(lp);
1357
-
1358
- // Legacy dialog path: when dynAutoMode is false and the eval has
1359
- // dialog callbacks, respond 'i' (inspect) so the kernel enters
1360
- // inspect mode and the Internal`AddHandler fires Dialog[].
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.
1369
- bool wantInspect = opts && !opts->dynAutoMode && opts->hasOnDialogBegin;
1370
- const char* resp;
1371
- if (wantInspect) {
1372
- resp = "i";
1373
- } else if (menuType_ == 1) {
1374
- resp = "a";
1375
- } else {
1376
- resp = "c";
1377
- }
1378
- DiagLog("[Eval] MENUPKT type=" + std::to_string(menuType_) + " — responding '" + resp + "'");
1379
- WSPutString(lp, resp);
1380
- WSEndPacket(lp);
1381
- WSFlush(lp);
1382
- }
1383
- else {
1384
- DiagLog("[Eval] unknown pkt=" + std::to_string(pkt) + ", discarding");
1385
- WSNewPacket(lp); // discard unknown packets
1386
- }
1387
- }
1388
-
1389
- return r;
1390
- }
1391
-
1392
- // ---------------------------------------------------------------------------
1393
18
  // ---------------------------------------------------------------------------
1394
- // WExprToNapiconvert WExpr Napi::Value. Main thread only.
19
+ // setDiagHandler(fn | null) register the global diagnostic callback.
20
+ // The TSFN is Unref()'d so it does not hold the Node.js event loop open.
1395
21
  // ---------------------------------------------------------------------------
1396
- static Napi::Value WExprToNapi(Napi::Env env, const WExpr& e) {
1397
- switch (e.kind) {
1398
- case WExpr::Integer: {
1399
- Napi::Object o = Napi::Object::New(env);
1400
- o.Set("type", Napi::String::New(env, "integer"));
1401
- o.Set("value", Napi::Number::New(env, static_cast<double>(e.intVal)));
1402
- return o;
1403
- }
1404
- case WExpr::Real: {
1405
- Napi::Object o = Napi::Object::New(env);
1406
- o.Set("type", Napi::String::New(env, "real"));
1407
- o.Set("value", Napi::Number::New(env, e.realVal));
1408
- return o;
1409
- }
1410
- case WExpr::String: {
1411
- Napi::Object o = Napi::Object::New(env);
1412
- o.Set("type", Napi::String::New(env, "string"));
1413
- o.Set("value", Napi::String::New(env, e.strVal));
1414
- return o;
1415
- }
1416
- case WExpr::Symbol: {
1417
- Napi::Object o = Napi::Object::New(env);
1418
- o.Set("type", Napi::String::New(env, "symbol"));
1419
- o.Set("value", Napi::String::New(env, e.strVal));
1420
- return o;
1421
- }
1422
- case WExpr::Function: {
1423
- Napi::Array argsArr = Napi::Array::New(env, e.args.size());
1424
- for (size_t i = 0; i < e.args.size(); ++i)
1425
- argsArr.Set(static_cast<uint32_t>(i), WExprToNapi(env, e.args[i]));
1426
- Napi::Object o = Napi::Object::New(env);
1427
- o.Set("type", Napi::String::New(env, "function"));
1428
- o.Set("head", Napi::String::New(env, e.head));
1429
- o.Set("args", argsArr);
1430
- return o;
1431
- }
1432
- case WExpr::WError:
1433
- default:
1434
- Napi::Error::New(env, e.strVal).ThrowAsJavaScriptException();
1435
- return env.Undefined();
1436
- }
1437
- }
1438
-
1439
- // ---------------------------------------------------------------------------
1440
- // EvalResultToNapi — convert EvalResult → Napi::Object. Main thread only.
1441
- // ---------------------------------------------------------------------------
1442
- static Napi::Value EvalResultToNapi(Napi::Env env, const EvalResult& r) {
1443
- auto obj = Napi::Object::New(env);
1444
-
1445
- obj.Set("cellIndex", Napi::Number::New(env, static_cast<double>(r.cellIndex)));
1446
- obj.Set("outputName", Napi::String::New(env, r.outputName));
1447
- obj.Set("result", WExprToNapi(env, r.result));
1448
- obj.Set("aborted", Napi::Boolean::New(env, r.aborted));
1449
-
1450
- auto print = Napi::Array::New(env, r.print.size());
1451
- for (size_t i = 0; i < r.print.size(); ++i)
1452
- print.Set(static_cast<uint32_t>(i), Napi::String::New(env, r.print[i]));
1453
- obj.Set("print", print);
1454
-
1455
- auto msgs = Napi::Array::New(env, r.messages.size());
1456
- for (size_t i = 0; i < r.messages.size(); ++i)
1457
- msgs.Set(static_cast<uint32_t>(i), Napi::String::New(env, r.messages[i]));
1458
- obj.Set("messages", msgs);
1459
-
1460
- return obj;
1461
- }
1462
-
1463
- // ===========================================================================
1464
- // EvaluateWorker — ALL blocking WSTP I/O runs on the libuv thread pool.
1465
- // ===========================================================================
1466
- class EvaluateWorker : public Napi::AsyncWorker {
1467
- public:
1468
- EvaluateWorker(Napi::Promise::Deferred deferred,
1469
- WSLINK lp,
1470
- std::string expr,
1471
- EvalOptions opts,
1472
- std::atomic<bool>& abortFlag,
1473
- std::atomic<bool>& workerReadingLink,
1474
- std::function<void()> completionCb,
1475
- int64_t cellIndex,
1476
- bool interactive = false)
1477
- : Napi::AsyncWorker(deferred.Env()),
1478
- deferred_(std::move(deferred)),
1479
- lp_(lp),
1480
- expr_(std::move(expr)),
1481
- interactive_(interactive),
1482
- opts_(std::move(opts)),
1483
- abortFlag_(abortFlag),
1484
- workerReadingLink_(workerReadingLink),
1485
- completionCb_(std::move(completionCb)),
1486
- cellIndex_(cellIndex)
1487
- {}
1488
-
1489
- // ---- thread-pool thread: send packet; block until response ----
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
-
1506
- // ---- Phase 2: ScheduledTask[Dialog[], interval] management ----
1507
- // Install a kernel-side ScheduledTask that calls Dialog[] periodically.
1508
- // Only install when:
1509
- // (a) dynAutoMode is active
1510
- // (b) there are registered Dynamic expressions
1511
- // (c) interval > 0
1512
- // (d) the currently installed task has a DIFFERENT interval (or none)
1513
- //
1514
- // The task expression includes a stop-flag check so the old task can
1515
- // be suppressed during the install eval (which uses rejectDialog=true).
1516
- //
1517
- // When there are no registrations (or interval=0), we do NOT send any
1518
- // stop/remove eval. The old task keeps running but the dynAutoMode
1519
- // handler handles empty-registry Dialog[]s by simply closing them.
1520
- if (opts_.dynAutoMode && opts_.dynIntervalMs > 0) {
1521
- bool hasRegs = false;
1522
- if (opts_.dynMutex && opts_.dynRegistry) {
1523
- std::lock_guard<std::mutex> lk(*opts_.dynMutex);
1524
- hasRegs = !opts_.dynRegistry->empty();
1525
- }
1526
-
1527
- int installedInterval = opts_.dynTaskInstalledInterval
1528
- ? *opts_.dynTaskInstalledInterval : 0;
1529
- bool needInstall = hasRegs && (installedInterval != opts_.dynIntervalMs);
1530
-
1531
- if (needInstall) {
1532
- double intervalSec = opts_.dynIntervalMs / 1000.0;
1533
- std::string taskExpr =
1534
- "Quiet[$wstpDynTaskStop = True;"
1535
- " If[ValueQ[$wstpDynTask], RemoveScheduledTask[$wstpDynTask]];"
1536
- " $wstpDynTaskStop =.;"
1537
- " $wstpDynTask = RunScheduledTask["
1538
- "If[!TrueQ[$wstpDynTaskStop], Dialog[]], " +
1539
- std::to_string(intervalSec) + "]]";
1540
- DiagLog("[Eval] dynAutoMode: installing ScheduledTask interval=" +
1541
- std::to_string(intervalSec) + "s");
1542
- EvalOptions taskOpts;
1543
- taskOpts.rejectDialog = true;
1544
- bool sentT =
1545
- WSPutFunction(lp_, "EvaluatePacket", 1) &&
1546
- WSPutFunction(lp_, "ToExpression", 1) &&
1547
- WSPutUTF8String(lp_,
1548
- reinterpret_cast<const unsigned char*>(taskExpr.c_str()),
1549
- static_cast<int>(taskExpr.size())) &&
1550
- WSEndPacket(lp_) &&
1551
- WSFlush(lp_);
1552
- if (sentT) {
1553
- DrainToEvalResult(lp_, &taskOpts);
1554
- // Track the installed interval so we don't reinstall next time.
1555
- if (opts_.dynTaskInstalledInterval)
1556
- *opts_.dynTaskInstalledInterval = opts_.dynIntervalMs;
1557
- }
1558
- }
1559
- }
1560
-
1561
- bool sent;
1562
- if (!interactive_) {
1563
- // EvaluatePacket + ToExpression: non-interactive, does NOT populate In[n]/Out[n].
1564
- sent = WSPutFunction(lp_, "EvaluatePacket", 1) &&
1565
- WSPutFunction(lp_, "ToExpression", 1) &&
1566
- WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) &&
1567
- WSEndPacket (lp_) &&
1568
- WSFlush (lp_);
1569
- } else {
1570
- // EnterExpressionPacket[ToExpression[str]]: goes through the kernel's
1571
- // full main loop — populates In[n] and Out[n] automatically, exactly
1572
- // as the real Mathematica frontend does. Responds with RETURNEXPRPKT.
1573
- sent = WSPutFunction(lp_, "EnterExpressionPacket", 1) &&
1574
- WSPutFunction(lp_, "ToExpression", 1) &&
1575
- WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) &&
1576
- WSEndPacket (lp_) &&
1577
- WSFlush (lp_);
1578
- }
1579
- if (!sent) {
1580
- workerReadingLink_.store(false, std::memory_order_release);
1581
- SetError("Failed to send packet to kernel");
1582
- } else {
1583
- opts_.interactive = interactive_;
1584
- result_ = DrainToEvalResult(lp_, &opts_);
1585
- // DrainToEvalResult may leave dialogOpen_=true (dynAutoMode suppresses
1586
- // the timer during the exit protocol). Clear it now that we're done.
1587
- if (opts_.dialogOpen) opts_.dialogOpen->store(false);
1588
-
1589
- // ---- Cleanup: ScheduledTask that fires Dialog[] ----
1590
- // The task is left running after the main eval completes.
1591
- // Between evaluations, Dialog[]s from the task accumulate on the
1592
- // link as stale BEGINDLGPKT packets. These are drained at the
1593
- // start of the next eval by drainStalePackets(). The install
1594
- // code at the top of Execute() also removes any old task before
1595
- // installing a new one. On session close(), the kernel is
1596
- // killed, which stops the task automatically.
1597
- //
1598
- // We CANNOT send a cleanup EvaluatePacket here because the
1599
- // ScheduledTask fires Dialog[] preemptively during the cleanup
1600
- // eval, and the RETURNPKT from the cleanup can get interleaved
1601
- // with dialog packets, causing the drain to hang.
1602
-
1603
- workerReadingLink_.store(false, std::memory_order_release); // lp_ no longer in use
1604
- if (!interactive_) {
1605
- // EvaluatePacket mode: kernel never sends INPUTNAMEPKT/OUTPUTNAMEPKT,
1606
- // so stamp the pre-captured counter and derive outputName manually.
1607
- result_.cellIndex = cellIndex_;
1608
- if (!result_.aborted && result_.result.kind == WExpr::Symbol) {
1609
- const std::string& sv = result_.result.strVal;
1610
- std::string bare = sv;
1611
- auto tick = sv.rfind('`');
1612
- if (tick != std::string::npos) bare = sv.substr(tick + 1);
1613
- if (bare != "Null")
1614
- result_.outputName = "Out[" + std::to_string(cellIndex_) + "]=";
1615
- } else if (!result_.aborted && result_.result.kind != WExpr::WError) {
1616
- result_.outputName = "Out[" + std::to_string(cellIndex_) + "]=";
1617
- }
1618
- } else {
1619
- // EnterExpressionPacket mode: DrainToEvalResult already populated
1620
- // cellIndex/outputName from INPUTNAMEPKT/OUTPUTNAMEPKT.
1621
- // Use our counter as fallback if the kernel didn't send them.
1622
- if (result_.cellIndex == 0)
1623
- result_.cellIndex = cellIndex_;
1624
- }
1625
- }
1626
- if (opts_.hasOnPrint) opts_.onPrint.Release();
1627
- if (opts_.hasOnMessage) opts_.onMessage.Release();
1628
- if (opts_.hasOnDialogBegin) opts_.onDialogBegin.Release();
1629
- if (opts_.hasOnDialogPrint) opts_.onDialogPrint.Release();
1630
- if (opts_.hasOnDialogEnd) opts_.onDialogEnd.Release();
1631
- }
1632
-
1633
- // ---- main thread: resolve promise after all TSFN callbacks delivered ----
1634
- void OnOK() override {
1635
- Napi::Env env = Env();
1636
- abortFlag_.store(false);
1637
-
1638
- EvalResult r = std::move(result_);
1639
- Napi::Promise::Deferred d = std::move(deferred_);
1640
- std::function<void()> cb = completionCb_;
1641
-
1642
- auto resolveFn = [env, r = std::move(r), d = std::move(d), cb]() mutable {
1643
- if (r.result.kind == WExpr::WError) {
1644
- d.Reject(Napi::Error::New(env, r.result.strVal).Value());
1645
- } else {
1646
- Napi::Value v = EvalResultToNapi(env, r);
1647
- if (env.IsExceptionPending())
1648
- d.Reject(env.GetAndClearPendingException().Value());
1649
- else
1650
- d.Resolve(v);
1651
- }
1652
- cb();
1653
- };
1654
-
1655
- if (opts_.ctx) {
1656
- opts_.ctx->fn = std::move(resolveFn);
1657
- opts_.ctx->done();
1658
- } else {
1659
- resolveFn();
1660
- }
1661
- }
1662
-
1663
- void OnError(const Napi::Error& e) override {
1664
- Napi::Env env = Env();
1665
- std::string msg = e.Message();
1666
- Napi::Promise::Deferred d = std::move(deferred_);
1667
- std::function<void()> cb = completionCb_;
1668
-
1669
- auto rejectFn = [env, msg, d = std::move(d), cb]() mutable {
1670
- d.Reject(Napi::Error::New(env, msg).Value());
1671
- cb();
1672
- };
1673
-
1674
- if (opts_.ctx) {
1675
- opts_.ctx->fn = std::move(rejectFn);
1676
- opts_.ctx->done();
1677
- } else {
1678
- rejectFn();
1679
- }
1680
- }
1681
-
1682
- private:
1683
- Napi::Promise::Deferred deferred_;
1684
- WSLINK lp_;
1685
- std::string expr_;
1686
- EvalOptions opts_;
1687
- std::atomic<bool>& abortFlag_;
1688
- std::atomic<bool>& workerReadingLink_;
1689
- std::function<void()> completionCb_;
1690
- int64_t cellIndex_;
1691
- bool interactive_;
1692
- EvalResult result_;
1693
- };
1694
-
1695
- // ===========================================================================
1696
- // WstpSession — JS class: new WstpSession(kernelPath?)
1697
- // ===========================================================================
1698
- static const char* kDefaultKernel =
1699
- "/Applications/Wolfram 3.app/Contents/MacOS/WolframKernel";
1700
-
1701
- class WstpSession : public Napi::ObjectWrap<WstpSession> {
1702
- public:
1703
- static Napi::Object Init(Napi::Env env, Napi::Object exports) {
1704
- Napi::Function func = DefineClass(env, "WstpSession", {
1705
- InstanceMethod<&WstpSession::Evaluate> ("evaluate"),
1706
- InstanceMethod<&WstpSession::Sub> ("sub"),
1707
- InstanceMethod<&WstpSession::SubWhenIdle> ("subWhenIdle"),
1708
- InstanceMethod<&WstpSession::DialogEval> ("dialogEval"),
1709
- InstanceMethod<&WstpSession::ExitDialog> ("exitDialog"),
1710
- InstanceMethod<&WstpSession::Interrupt> ("interrupt"),
1711
- InstanceMethod<&WstpSession::Abort> ("abort"),
1712
- InstanceMethod<&WstpSession::CloseAllDialogs> ("closeAllDialogs"),
1713
- InstanceMethod<&WstpSession::CreateSubsession>("createSubsession"),
1714
- InstanceMethod<&WstpSession::Close> ("close"),
1715
- // Dynamic evaluation API (Phase 2)
1716
- InstanceMethod<&WstpSession::RegisterDynamic> ("registerDynamic"),
1717
- InstanceMethod<&WstpSession::UnregisterDynamic> ("unregisterDynamic"),
1718
- InstanceMethod<&WstpSession::ClearDynamicRegistry>("clearDynamicRegistry"),
1719
- InstanceMethod<&WstpSession::GetDynamicResults> ("getDynamicResults"),
1720
- InstanceMethod<&WstpSession::SetDynamicInterval> ("setDynamicInterval"),
1721
- InstanceMethod<&WstpSession::SetDynAutoMode> ("setDynAutoMode"),
1722
- InstanceAccessor<&WstpSession::IsOpen> ("isOpen"),
1723
- InstanceAccessor<&WstpSession::IsDialogOpen> ("isDialogOpen"),
1724
- InstanceAccessor<&WstpSession::IsReady> ("isReady"),
1725
- InstanceAccessor<&WstpSession::KernelPid> ("kernelPid"),
1726
- InstanceAccessor<&WstpSession::DynamicActive> ("dynamicActive"),
1727
- });
1728
-
1729
- Napi::FunctionReference* ctor = new Napi::FunctionReference();
1730
- *ctor = Napi::Persistent(func);
1731
- env.SetInstanceData(ctor);
1732
-
1733
- exports.Set("WstpSession", func);
1734
- return exports;
1735
- }
1736
-
1737
- // -----------------------------------------------------------------------
1738
- // Constructor new WstpSession(kernelPath?)
1739
- // -----------------------------------------------------------------------
1740
- WstpSession(const Napi::CallbackInfo& info)
1741
- : Napi::ObjectWrap<WstpSession>(info), wsEnv_(nullptr), lp_(nullptr), open_(false)
1742
- {
1743
- Napi::Env env = info.Env();
1744
-
1745
- std::string kernelPath = kDefaultKernel;
1746
- if (info.Length() > 0 && info[0].IsString())
1747
- kernelPath = info[0].As<Napi::String>().Utf8Value();
1748
-
1749
- // Parse options object: new WstpSession(opts?) or new WstpSession(path, opts?)
1750
- // Supported options: { interactive: true } — use EnterTextPacket instead of
1751
- // EvaluatePacket so the kernel populates In[n]/Out[n] variables.
1752
- int optsIdx = (info.Length() > 0 && info[0].IsObject()) ? 0
1753
- : (info.Length() > 1 && info[1].IsObject()) ? 1 : -1;
1754
- if (optsIdx >= 0) {
1755
- auto o = info[optsIdx].As<Napi::Object>();
1756
- if (o.Has("interactive") && o.Get("interactive").IsBoolean())
1757
- interactiveMode_ = o.Get("interactive").As<Napi::Boolean>().Value();
1758
- }
1759
-
1760
- WSEnvironmentParameter params = WSNewParameters(WSREVISION, WSAPIREVISION);
1761
- wsEnv_ = WSInitialize(params);
1762
- WSReleaseParameters(params);
1763
- if (!wsEnv_) {
1764
- Napi::Error::New(env, "WSInitialize failed").ThrowAsJavaScriptException();
1765
- return;
1766
- }
1767
-
1768
- // Shell-quote the kernel path so spaces ("Wolfram 3.app") survive
1769
- // being passed through /bin/sh by WSOpenArgcArgv.
1770
- std::string linkName = "\"" + kernelPath + "\" -wstp";
1771
- const char* argv[] = { "wstp", "-linkname", linkName.c_str(),
1772
- "-linkmode", "launch" };
1773
-
1774
- // Retry the entire kernel-launch sequence up to 3 times. On ~20% of
1775
- // consecutive launches the kernel starts with $Output routing broken
1776
- // (Print[]/Message[] produce no TextPacket). Killing the stale kernel
1777
- // and spawning a fresh one resolves it reliably within 1-2 attempts.
1778
- int err = 0;
1779
- for (int attempt = 0; attempt <= 2; ++attempt) {
1780
- if (attempt > 0) {
1781
- DiagLog("[Session] restart attempt " + std::to_string(attempt) +
1782
- " — $Output routing broken on previous kernel");
1783
- WSClose(lp_); lp_ = nullptr;
1784
- // Kill the stale kernel before relaunching; use the same guard
1785
- // as CleanUp() to avoid double-kill on already-dead process.
1786
- if (kernelPid_ > 0 && !kernelKilled_) { kernelKilled_ = true; kill(kernelPid_, SIGTERM); }
1787
- kernelKilled_ = false; // reset for the fresh kernel about to launch
1788
- std::this_thread::sleep_for(std::chrono::milliseconds(200));
1789
- }
1790
-
1791
- err = 0;
1792
- lp_ = WSOpenArgcArgv(wsEnv_, 5, const_cast<char**>(argv), &err);
1793
- if (!lp_ || err != WSEOK) {
1794
- std::string msg = "WSOpenArgcArgv failed (code " + std::to_string(err) + ")";
1795
- WSDeinitialize(wsEnv_); wsEnv_ = nullptr;
1796
- Napi::Error::New(env, msg).ThrowAsJavaScriptException();
1797
- return;
1798
- }
1799
-
1800
- if (!WSActivate(lp_)) {
1801
- const char* m = WSErrorMessage(lp_);
1802
- std::string s = std::string("WSActivate failed: ") + (m ? m : "?");
1803
- WSClose(lp_); lp_ = nullptr;
1804
- WSDeinitialize(wsEnv_); wsEnv_ = nullptr;
1805
- Napi::Error::New(env, s).ThrowAsJavaScriptException();
1806
- return;
1807
- }
1808
-
1809
- // Get the kernel PID so CleanUp() can SIGTERM it.
1810
- kernelPid_ = FetchKernelPid(lp_);
1811
-
1812
- // Confirm $Output→TextPacket routing is live. If not, loop back
1813
- // and restart with a fresh kernel process.
1814
- if (WarmUpOutputRouting(lp_)) break; // success — proceed
1815
-
1816
- // Last attempt — give up and continue with broken $Output rather
1817
- // than looping indefinitely.
1818
- if (attempt == 2)
1819
- DiagLog("[Session] WARNING: $Output broken after 3 kernel launches");
1820
- }
1821
-
1822
- open_ = true;
1823
- abortFlag_.store(false);
1824
- }
1825
-
1826
- ~WstpSession() { CleanUp(); }
1827
-
1828
- // -----------------------------------------------------------------------
1829
- // evaluate(expr, opts?) → Promise<EvalResult>
1830
- //
1831
- // opts may contain { onPrint, onMessage } streaming callbacks.
1832
- // Multiple calls are serialised through an internal queue — no link
1833
- // corruption even if the caller doesn't await between calls.
1834
- // -----------------------------------------------------------------------
1835
- Napi::Value Evaluate(const Napi::CallbackInfo& info) {
1836
- Napi::Env env = info.Env();
1837
- auto deferred = Napi::Promise::Deferred::New(env);
1838
- auto promise = deferred.Promise();
1839
-
1840
- if (!open_) {
1841
- deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
1842
- return promise;
1843
- }
1844
- if (info.Length() < 1 || !info[0].IsString()) {
1845
- deferred.Reject(Napi::TypeError::New(env, "evaluate(expr: string, opts?: object)").Value());
1846
- return promise;
1847
- }
1848
-
1849
- std::string expr = info[0].As<Napi::String>().Utf8Value();
1850
-
1851
- // Parse optional second argument: { onPrint?, onMessage?, onDialogBegin?,
1852
- // onDialogPrint?, onDialogEnd?,
1853
- // interactive?: boolean (override session default) }
1854
- EvalOptions opts;
1855
- int interactiveOverride = -1; // -1 = use session default
1856
- if (info.Length() >= 2 && info[1].IsObject()) {
1857
- auto optsObj = info[1].As<Napi::Object>();
1858
- bool wantPrint = optsObj.Has("onPrint") && optsObj.Get("onPrint").IsFunction();
1859
- bool wantMsg = optsObj.Has("onMessage") && optsObj.Get("onMessage").IsFunction();
1860
- bool wantDBegin = optsObj.Has("onDialogBegin") && optsObj.Get("onDialogBegin").IsFunction();
1861
- bool wantDPrint = optsObj.Has("onDialogPrint") && optsObj.Get("onDialogPrint").IsFunction();
1862
- bool wantDEnd = optsObj.Has("onDialogEnd") && optsObj.Get("onDialogEnd").IsFunction();
1863
- // Per-call interactive override: opts.interactive = true/false
1864
- if (optsObj.Has("interactive") && optsObj.Get("interactive").IsBoolean())
1865
- interactiveOverride = optsObj.Get("interactive").As<Napi::Boolean>().Value() ? 1 : 0;
1866
- // Per-call rejectDialog: auto-close any BEGINDLGPKT without JS roundtrip.
1867
- if (optsObj.Has("rejectDialog") && optsObj.Get("rejectDialog").IsBoolean() &&
1868
- optsObj.Get("rejectDialog").As<Napi::Boolean>().Value())
1869
- opts.rejectDialog = true;
1870
-
1871
- // CompleteCtx: count = numTsfns + 1 (the extra slot is for OnOK/OnError).
1872
- // This ensures the Promise resolves ONLY after every TSFN has been
1873
- // finalized (= all queued callbacks delivered), regardless of whether
1874
- // the TSFN finalizers or OnOK/OnError happen to run first.
1875
- int tsfnCount = (wantPrint ? 1 : 0) + (wantMsg ? 1 : 0)
1876
- + (wantDBegin ? 1 : 0) + (wantDPrint ? 1 : 0)
1877
- + (wantDEnd ? 1 : 0);
1878
- CompleteCtx* ctx = (tsfnCount > 0) ? new CompleteCtx{ tsfnCount + 1, {} } : nullptr;
1879
-
1880
- auto makeTsfn = [&](const char* name, const char* key) {
1881
- return Napi::ThreadSafeFunction::New(
1882
- env, optsObj.Get(key).As<Napi::Function>(), name, 0, 1,
1883
- ctx, [](Napi::Env, CompleteCtx* c) { c->done(); });
1884
- };
1885
- if (wantPrint) { opts.onPrint = makeTsfn("onPrint", "onPrint"); opts.hasOnPrint = true; }
1886
- if (wantMsg) { opts.onMessage = makeTsfn("onMessage", "onMessage"); opts.hasOnMessage = true; }
1887
- if (wantDBegin) { opts.onDialogBegin = makeTsfn("onDialogBegin", "onDialogBegin"); opts.hasOnDialogBegin = true; }
1888
- if (wantDPrint) { opts.onDialogPrint = makeTsfn("onDialogPrint", "onDialogPrint"); opts.hasOnDialogPrint = true; }
1889
- if (wantDEnd) { opts.onDialogEnd = makeTsfn("onDialogEnd", "onDialogEnd"); opts.hasOnDialogEnd = true; }
1890
- opts.ctx = ctx;
1891
- }
1892
- // Wire up the dialog queue pointers so DrainToEvalResult can service
1893
- // dialogEval() calls from the thread pool during a Dialog[] subsession.
1894
- opts.dialogMutex = &dialogMutex_;
1895
- opts.dialogQueue = &dialogQueue_;
1896
- opts.dialogPending = &dialogPending_;
1897
- opts.dialogOpen = &dialogOpen_;
1898
- opts.abortFlag = &abortFlag_;
1899
- // Wire up Dynamic eval pointers so DrainToEvalResult can evaluate
1900
- // registered Dynamic expressions inline when BEGINDLGPKT arrives.
1901
- opts.dynMutex = &dynMutex_;
1902
- opts.dynRegistry = &dynRegistry_;
1903
- opts.dynResults = &dynResults_;
1904
- opts.dynLastEval = &dynLastEval_;
1905
- opts.dynAutoMode = dynAutoMode_.load();
1906
- opts.dynIntervalMs = dynIntervalMs_.load();
1907
- opts.dynTaskInstalledInterval = &dynTaskInstalledInterval_;
1908
-
1909
- {
1910
- std::lock_guard<std::mutex> lk(queueMutex_);
1911
- queue_.push(QueuedEval{ std::move(expr), std::move(opts), std::move(deferred), interactiveOverride });
1912
- }
1913
- MaybeStartNext();
1914
- return promise;
1915
- }
1916
-
1917
- // -----------------------------------------------------------------------
1918
- // MaybeStartNext — pop the front of the queue and launch it, but only if
1919
- // no evaluation is currently running (busy_ CAS ensures atomicity).
1920
- // Called from: Evaluate() on main thread; completionCb_ on main thread.
1921
- // Queue entry — one pending sub() call when the session is idle.
1922
- struct QueuedSubIdle {
1923
- std::string expr;
1924
- Napi::Promise::Deferred deferred;
1925
- };
1926
-
1927
- // Queue entry — one pending subWhenIdle() call (lowest priority).
1928
- // Executed only when both subIdleQueue_ and queue_ are empty.
1929
- struct QueuedWhenIdle {
1930
- std::string expr;
1931
- Napi::Promise::Deferred deferred;
1932
- // Absolute deadline; time_point::max() = no timeout.
1933
- std::chrono::steady_clock::time_point deadline =
1934
- std::chrono::steady_clock::time_point::max();
1935
- };
1936
-
1937
- // Sub-idle evals are preferred over normal evals so sub()-when-idle gets
1938
- // a quick result without waiting for a queued evaluate().
1939
- // -----------------------------------------------------------------------
1940
- void MaybeStartNext() {
1941
- bool expected = false;
1942
- if (!busy_.compare_exchange_strong(expected, true))
1943
- return; // already running
1944
-
1945
- std::unique_lock<std::mutex> lk(queueMutex_);
1946
-
1947
- // Check sub-idle queue first.
1948
- if (!subIdleQueue_.empty()) {
1949
- auto item = std::move(subIdleQueue_.front());
1950
- subIdleQueue_.pop();
1951
- lk.unlock();
1952
- StartSubIdleWorker(std::move(item));
1953
- return;
1954
- }
1955
-
1956
- if (queue_.empty()) {
1957
- // No normal evaluate() items — check lowest-priority when-idle queue.
1958
- // Drain any timeout-expired entries first, then run the next live one.
1959
- while (!whenIdleQueue_.empty()) {
1960
- auto& front = whenIdleQueue_.front();
1961
- bool expired =
1962
- (front.deadline != std::chrono::steady_clock::time_point::max() &&
1963
- std::chrono::steady_clock::now() >= front.deadline);
1964
- if (expired) {
1965
- Napi::Env wenv = front.deferred.Env();
1966
- front.deferred.Reject(
1967
- Napi::Error::New(wenv, "subWhenIdle: timeout").Value());
1968
- whenIdleQueue_.pop();
1969
- continue;
1970
- }
1971
- auto wiItem = std::move(whenIdleQueue_.front());
1972
- whenIdleQueue_.pop();
1973
- lk.unlock();
1974
- StartWhenIdleWorker(std::move(wiItem));
1975
- return;
1976
- }
1977
- busy_.store(false);
1978
- return;
1979
- }
1980
- auto item = std::move(queue_.front());
1981
- queue_.pop();
1982
- lk.unlock();
1983
-
1984
- // Resolve interactive mode: per-call override takes precedence over session default.
1985
- bool evalInteractive = (item.interactiveOverride == -1)
1986
- ? interactiveMode_
1987
- : (item.interactiveOverride == 1);
1988
- workerReadingLink_.store(true, std::memory_order_release);
1989
- auto* worker = new EvaluateWorker(
1990
- std::move(item.deferred),
1991
- lp_,
1992
- std::move(item.expr),
1993
- std::move(item.opts),
1994
- abortFlag_,
1995
- workerReadingLink_,
1996
- [this]() { busy_.store(false); MaybeStartNext(); },
1997
- nextLine_.fetch_add(1),
1998
- evalInteractive
1999
- );
2000
- worker->Queue();
2001
- }
2002
-
2003
- // Launch a lightweight EvaluateWorker that resolves with just the WExpr
2004
- // (not a full EvalResult) — used by sub() when the session is idle.
2005
- void StartSubIdleWorker(QueuedSubIdle item) {
2006
- struct SubIdleWorker : public Napi::AsyncWorker {
2007
- SubIdleWorker(Napi::Promise::Deferred d, WSLINK lp, std::string expr,
2008
- std::atomic<bool>& workerReadingLink,
2009
- std::function<void()> done)
2010
- : Napi::AsyncWorker(d.Env()),
2011
- deferred_(std::move(d)), lp_(lp), expr_(std::move(expr)),
2012
- workerReadingLink_(workerReadingLink), done_(std::move(done)) {}
2013
-
2014
- void Execute() override {
2015
- if (!WSPutFunction(lp_, "EvaluatePacket", 1) ||
2016
- !WSPutFunction(lp_, "ToExpression", 1) ||
2017
- !WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) ||
2018
- !WSEndPacket(lp_) ||
2019
- !WSFlush(lp_)) {
2020
- workerReadingLink_.store(false, std::memory_order_release);
2021
- SetError("sub (idle): failed to send EvaluatePacket");
2022
- return;
2023
- }
2024
- result_ = DrainToEvalResult(lp_);
2025
- workerReadingLink_.store(false, std::memory_order_release); // lp_ no longer in use
2026
- }
2027
- void OnOK() override {
2028
- Napi::Env env = Env();
2029
- if (result_.result.kind == WExpr::WError) {
2030
- deferred_.Reject(Napi::Error::New(env, result_.result.strVal).Value());
2031
- } else {
2032
- Napi::Value v = WExprToNapi(env, result_.result);
2033
- if (env.IsExceptionPending())
2034
- deferred_.Reject(env.GetAndClearPendingException().Value());
2035
- else
2036
- deferred_.Resolve(v);
2037
- }
2038
- done_();
2039
- }
2040
- void OnError(const Napi::Error& e) override {
2041
- deferred_.Reject(e.Value());
2042
- done_();
2043
- }
2044
- private:
2045
- Napi::Promise::Deferred deferred_;
2046
- WSLINK lp_;
2047
- std::string expr_;
2048
- std::atomic<bool>& workerReadingLink_;
2049
- std::function<void()> done_;
2050
- EvalResult result_;
2051
- };
2052
-
2053
- workerReadingLink_.store(true, std::memory_order_release);
2054
- (new SubIdleWorker(std::move(item.deferred), lp_, std::move(item.expr),
2055
- workerReadingLink_,
2056
- [this]() { busy_.store(false); MaybeStartNext(); }))->Queue();
2057
- }
2058
-
2059
- // Launch a lightweight EvaluateWorker for one subWhenIdle() entry.
2060
- // Identical to StartSubIdleWorker — reuses the same SubIdleWorker inner
2061
- // class pattern; separated here so it can receive QueuedWhenIdle.
2062
- void StartWhenIdleWorker(QueuedWhenIdle item) {
2063
- struct WhenIdleWorker : public Napi::AsyncWorker {
2064
- WhenIdleWorker(Napi::Promise::Deferred d, WSLINK lp, std::string expr,
2065
- std::atomic<bool>& workerReadingLink,
2066
- std::function<void()> done)
2067
- : Napi::AsyncWorker(d.Env()),
2068
- deferred_(std::move(d)), lp_(lp), expr_(std::move(expr)),
2069
- workerReadingLink_(workerReadingLink), done_(std::move(done)) {}
2070
-
2071
- void Execute() override {
2072
- if (!WSPutFunction(lp_, "EvaluatePacket", 1) ||
2073
- !WSPutFunction(lp_, "ToExpression", 1) ||
2074
- !WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) ||
2075
- !WSEndPacket(lp_) ||
2076
- !WSFlush(lp_)) {
2077
- workerReadingLink_.store(false, std::memory_order_release);
2078
- SetError("subWhenIdle: failed to send EvaluatePacket");
2079
- return;
2080
- }
2081
- result_ = DrainToEvalResult(lp_);
2082
- workerReadingLink_.store(false, std::memory_order_release);
2083
- }
2084
- void OnOK() override {
2085
- Napi::Env env = Env();
2086
- if (result_.result.kind == WExpr::WError) {
2087
- deferred_.Reject(Napi::Error::New(env, result_.result.strVal).Value());
2088
- } else {
2089
- Napi::Value v = WExprToNapi(env, result_.result);
2090
- if (env.IsExceptionPending())
2091
- deferred_.Reject(env.GetAndClearPendingException().Value());
2092
- else
2093
- deferred_.Resolve(v);
2094
- }
2095
- done_();
2096
- }
2097
- void OnError(const Napi::Error& e) override {
2098
- deferred_.Reject(e.Value());
2099
- done_();
2100
- }
2101
- private:
2102
- Napi::Promise::Deferred deferred_;
2103
- WSLINK lp_;
2104
- std::string expr_;
2105
- std::atomic<bool>& workerReadingLink_;
2106
- std::function<void()> done_;
2107
- EvalResult result_;
2108
- };
2109
-
2110
- workerReadingLink_.store(true, std::memory_order_release);
2111
- (new WhenIdleWorker(std::move(item.deferred), lp_, std::move(item.expr),
2112
- workerReadingLink_,
2113
- [this]() { busy_.store(false); MaybeStartNext(); }))->Queue();
2114
- }
2115
-
2116
- // -----------------------------------------------------------------------
2117
- // sub(expr) → Promise<WExpr>
2118
- //
2119
- // Queues a lightweight evaluation that resolves with just the WExpr result
2120
- // (not a full EvalResult). If the session is busy the sub() is prioritised
2121
- // over any pending evaluate() calls and starts as soon as the current eval
2122
- // finishes. If idle it starts immediately.
2123
- // -----------------------------------------------------------------------
2124
- Napi::Value Sub(const Napi::CallbackInfo& info) {
2125
- Napi::Env env = info.Env();
2126
- auto deferred = Napi::Promise::Deferred::New(env);
2127
- auto promise = deferred.Promise();
2128
-
2129
- if (!open_) {
2130
- deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
2131
- return promise;
2132
- }
2133
- if (info.Length() < 1 || !info[0].IsString()) {
2134
- deferred.Reject(Napi::TypeError::New(env, "sub(expr: string)").Value());
2135
- return promise;
2136
- }
2137
- std::string expr = info[0].As<Napi::String>().Utf8Value();
2138
-
2139
- {
2140
- std::lock_guard<std::mutex> lk(queueMutex_);
2141
- subIdleQueue_.push(QueuedSubIdle{ std::move(expr), std::move(deferred) });
2142
- }
2143
- MaybeStartNext();
2144
- return promise;
2145
- }
2146
-
2147
- // -----------------------------------------------------------------------
2148
- // subWhenIdle(expr, opts?) → Promise<WExpr>
2149
- //
2150
- // Queues a lightweight evaluation at the LOWEST priority: it runs only
2151
- // when both subIdleQueue_ and queue_ are empty (kernel truly idle).
2152
- // Ideal for background queries that must not compete with cell evaluations.
2153
- //
2154
- // opts may contain:
2155
- // timeout?: number — milliseconds to wait before the Promise rejects
2156
- // -----------------------------------------------------------------------
2157
- Napi::Value SubWhenIdle(const Napi::CallbackInfo& info) {
2158
- Napi::Env env = info.Env();
2159
- auto deferred = Napi::Promise::Deferred::New(env);
2160
- auto promise = deferred.Promise();
2161
-
2162
- if (!open_) {
2163
- deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
2164
- return promise;
2165
- }
2166
- if (info.Length() < 1 || !info[0].IsString()) {
2167
- deferred.Reject(Napi::TypeError::New(env,
2168
- "subWhenIdle(expr: string, opts?: {timeout?: number})").Value());
2169
- return promise;
2170
- }
2171
- std::string expr = info[0].As<Napi::String>().Utf8Value();
2172
-
2173
- // Parse optional timeout from opts object.
2174
- auto deadline = std::chrono::steady_clock::time_point::max();
2175
- if (info.Length() >= 2 && info[1].IsObject()) {
2176
- auto optsObj = info[1].As<Napi::Object>();
2177
- if (optsObj.Has("timeout") && optsObj.Get("timeout").IsNumber()) {
2178
- double ms = optsObj.Get("timeout").As<Napi::Number>().DoubleValue();
2179
- if (ms > 0)
2180
- deadline = std::chrono::steady_clock::now() +
2181
- std::chrono::milliseconds(static_cast<int64_t>(ms));
2182
- }
2183
- }
2184
-
2185
- {
2186
- std::lock_guard<std::mutex> lk(queueMutex_);
2187
- whenIdleQueue_.push(QueuedWhenIdle{
2188
- std::move(expr), std::move(deferred), deadline });
2189
- }
2190
- MaybeStartNext();
2191
- return promise;
2192
- }
2193
-
2194
- // -----------------------------------------------------------------------
2195
- // exitDialog(retVal?) → Promise<void>
2196
- //
2197
- // Exits the currently-open Dialog[] subsession by entering
2198
- // "Return[retVal]" as if the user typed it at the interactive prompt
2199
- // (EnterTextPacket). This is different from plain dialogEval('Return[]')
2200
- // which uses EvaluatePacket and does NOT exit the dialog because Return[]
2201
- // at the top level of EvaluatePacket is unevaluated.
2202
- //
2203
- // Returns a Promise that resolves (with Null) when ENDDLGPKT is received,
2204
- // or rejects immediately if no dialog is open.
2205
- // -----------------------------------------------------------------------
2206
- Napi::Value ExitDialog(const Napi::CallbackInfo& info) {
2207
- Napi::Env env = info.Env();
2208
- auto deferred = Napi::Promise::Deferred::New(env);
2209
- auto promise = deferred.Promise();
2210
-
2211
- if (!open_) {
2212
- deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
2213
- return promise;
2214
- }
2215
- if (!dialogOpen_.load()) {
2216
- deferred.Reject(Napi::Error::New(env,
2217
- "no dialog subsession is open").Value());
2218
- return promise;
2219
- }
2220
- // Stale-state guard: dialogOpen_=true but the drain loop has already
2221
- // exited (busy_=false). Nobody will service the queue, so resolve
2222
- // immediately and clean up rather than enqueuing a hanging request.
2223
- if (!busy_.load()) {
2224
- FlushDialogQueueWithError("dialog closed: session idle");
2225
- dialogOpen_.store(false);
2226
- deferred.Resolve(env.Null());
2227
- return promise;
2228
- }
2229
- // Build "Return[]" or "Return[retVal]" as the exit expression.
2230
- std::string exitExpr = "Return[]";
2231
- if (info.Length() >= 1 && info[0].IsString())
2232
- exitExpr = "Return[" + info[0].As<Napi::String>().Utf8Value() + "]";
2233
-
2234
- auto tsfn = Napi::ThreadSafeFunction::New(
2235
- env,
2236
- Napi::Function::New(env, [deferred](const Napi::CallbackInfo& ci) mutable {
2237
- Napi::Env e = ci.Env();
2238
- if (ci.Length() > 0) {
2239
- auto obj = ci[0];
2240
- if (obj.IsObject()) {
2241
- auto o = obj.As<Napi::Object>();
2242
- if (o.Has("error") && o.Get("error").IsString()) {
2243
- deferred.Reject(Napi::Error::New(e,
2244
- o.Get("error").As<Napi::String>().Utf8Value()).Value());
2245
- return;
2246
- }
2247
- }
2248
- }
2249
- deferred.Resolve(e.Null());
2250
- }),
2251
- "exitDialogResolve", 0, 1);
2252
-
2253
- DialogRequest req;
2254
- req.expr = std::move(exitExpr);
2255
- req.useEnterText = true;
2256
- req.resolve = std::move(tsfn);
2257
- {
2258
- std::lock_guard<std::mutex> lk(dialogMutex_);
2259
- dialogQueue_.push(std::move(req));
2260
- }
2261
- dialogPending_.store(true);
2262
- return promise;
2263
- }
2264
-
2265
- // -----------------------------------------------------------------------
2266
- // dialogEval(expr) → Promise<WExpr>
2267
- // Rejects immediately if no dialog is open.
2268
- // -----------------------------------------------------------------------
2269
- Napi::Value DialogEval(const Napi::CallbackInfo& info) {
2270
- Napi::Env env = info.Env();
2271
- auto deferred = Napi::Promise::Deferred::New(env);
2272
- auto promise = deferred.Promise();
2273
-
2274
- if (!open_) {
2275
- deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
2276
- return promise;
2277
- }
2278
- if (!dialogOpen_.load()) {
2279
- deferred.Reject(Napi::Error::New(env,
2280
- "no dialog subsession is open — call Dialog[] first").Value());
2281
- return promise;
2282
- }
2283
- if (info.Length() < 1 || !info[0].IsString()) {
2284
- deferred.Reject(Napi::TypeError::New(env, "dialogEval(expr: string)").Value());
2285
- return promise;
2286
- }
2287
- std::string expr = info[0].As<Napi::String>().Utf8Value();
2288
-
2289
- // Create a TSFN that resolves/rejects the deferred on the main thread.
2290
- // The TSFN is Released by serviceDialogRequest() after the single call.
2291
- auto tsfn = Napi::ThreadSafeFunction::New(
2292
- env,
2293
- Napi::Function::New(env, [deferred](const Napi::CallbackInfo& ci) mutable {
2294
- Napi::Env e = ci.Env();
2295
- if (ci.Length() > 0 && ci[0].IsObject()) {
2296
- auto obj = ci[0].As<Napi::Object>();
2297
- // Check for error sentinel: { error: string }
2298
- if (obj.Has("error") && obj.Get("error").IsString()) {
2299
- deferred.Reject(Napi::Error::New(e,
2300
- obj.Get("error").As<Napi::String>().Utf8Value()).Value());
2301
- } else {
2302
- deferred.Resolve(ci[0]);
2303
- }
2304
- } else {
2305
- deferred.Reject(Napi::Error::New(e, "dialogEval: bad result").Value());
2306
- }
2307
- }),
2308
- "dialogResolve", 0, 1);
2309
-
2310
- DialogRequest req;
2311
- req.expr = std::move(expr);
2312
- req.useEnterText = false;
2313
- req.resolve = std::move(tsfn);
2314
- {
2315
- std::lock_guard<std::mutex> lk(dialogMutex_);
2316
- dialogQueue_.push(std::move(req));
2317
- }
2318
- dialogPending_.store(true);
2319
- return promise;
2320
- }
2321
-
2322
- // -----------------------------------------------------------------------
2323
- // interrupt() — send WSInterruptMessage (best-effort).
2324
- //
2325
- // This is NOT abort() — it does not cancel the evaluation. Its effect
2326
- // depends entirely on whether a Wolfram-side interrupt handler has been
2327
- // installed (e.g. Internal`AddHandler["Interrupt", Function[Null, Dialog[]]]).
2328
- // On kernels without such a handler this is a no-op.
2329
- // Main-thread only — same thread-safety guarantee as abort().
2330
- // -----------------------------------------------------------------------
2331
- Napi::Value Interrupt(const Napi::CallbackInfo& info) {
2332
- Napi::Env env = info.Env();
2333
- if (!open_) return Napi::Boolean::New(env, false);
2334
- int ok = WSPutMessage(lp_, WSInterruptMessage);
2335
- return Napi::Boolean::New(env, ok != 0);
2336
- }
2337
-
2338
- // -----------------------------------------------------------------------
2339
- // registerDynamic(id, expr) → void
2340
- // Register or replace a Dynamic expression for periodic evaluation.
2341
- // -----------------------------------------------------------------------
2342
- Napi::Value RegisterDynamic(const Napi::CallbackInfo& info) {
2343
- Napi::Env env = info.Env();
2344
- if (info.Length() < 2 || !info[0].IsString() || !info[1].IsString()) {
2345
- Napi::TypeError::New(env, "registerDynamic(id: string, expr: string)")
2346
- .ThrowAsJavaScriptException();
2347
- return env.Undefined();
2348
- }
2349
- std::string id = info[0].As<Napi::String>().Utf8Value();
2350
- std::string expr = info[1].As<Napi::String>().Utf8Value();
2351
- {
2352
- std::lock_guard<std::mutex> lk(dynMutex_);
2353
- for (auto& reg : dynRegistry_) {
2354
- if (reg.id == id) { reg.expr = expr; return env.Undefined(); }
2355
- }
2356
- dynRegistry_.push_back({id, expr});
2357
- }
2358
- return env.Undefined();
2359
- }
2360
-
2361
- // -----------------------------------------------------------------------
2362
- // unregisterDynamic(id) → void
2363
- // -----------------------------------------------------------------------
2364
- Napi::Value UnregisterDynamic(const Napi::CallbackInfo& info) {
2365
- Napi::Env env = info.Env();
2366
- if (info.Length() < 1 || !info[0].IsString()) {
2367
- Napi::TypeError::New(env, "unregisterDynamic(id: string)")
2368
- .ThrowAsJavaScriptException();
2369
- return env.Undefined();
2370
- }
2371
- std::string id = info[0].As<Napi::String>().Utf8Value();
2372
- {
2373
- std::lock_guard<std::mutex> lk(dynMutex_);
2374
- dynRegistry_.erase(
2375
- std::remove_if(dynRegistry_.begin(), dynRegistry_.end(),
2376
- [&id](const DynRegistration& r){ return r.id == id; }),
2377
- dynRegistry_.end());
2378
- }
2379
- return env.Undefined();
2380
- }
2381
-
2382
- // -----------------------------------------------------------------------
2383
- // clearDynamicRegistry() → void
2384
- // -----------------------------------------------------------------------
2385
- Napi::Value ClearDynamicRegistry(const Napi::CallbackInfo& info) {
2386
- std::lock_guard<std::mutex> lk(dynMutex_);
2387
- dynRegistry_.clear();
2388
- dynResults_.clear();
2389
- return info.Env().Undefined();
2390
- }
2391
-
2392
- // -----------------------------------------------------------------------
2393
- // getDynamicResults() → Record<string, DynResult>
2394
- // Swaps and returns accumulated results; clears the internal buffer.
2395
- // -----------------------------------------------------------------------
2396
- Napi::Value GetDynamicResults(const Napi::CallbackInfo& info) {
2397
- Napi::Env env = info.Env();
2398
- std::vector<DynResult> snap;
2399
- {
2400
- std::lock_guard<std::mutex> lk(dynMutex_);
2401
- snap.swap(dynResults_);
2402
- }
2403
- auto obj = Napi::Object::New(env);
2404
- for (const auto& dr : snap) {
2405
- auto entry = Napi::Object::New(env);
2406
- entry.Set("value", Napi::String::New(env, dr.value));
2407
- entry.Set("timestamp", Napi::Number::New(env, dr.timestamp));
2408
- if (!dr.error.empty())
2409
- entry.Set("error", Napi::String::New(env, dr.error));
2410
- obj.Set(dr.id, entry);
2411
- }
2412
- return obj;
2413
- }
2414
-
2415
- // -----------------------------------------------------------------------
2416
- // setDynamicInterval(ms) → void
2417
- // Set the auto-interrupt interval. 0 = disabled.
2418
- // Starting/stopping the timer thread is handled here.
2419
- // -----------------------------------------------------------------------
2420
- Napi::Value SetDynamicInterval(const Napi::CallbackInfo& info) {
2421
- Napi::Env env = info.Env();
2422
- if (info.Length() < 1 || !info[0].IsNumber()) {
2423
- Napi::TypeError::New(env, "setDynamicInterval(ms: number)")
2424
- .ThrowAsJavaScriptException();
2425
- return env.Undefined();
2426
- }
2427
- int ms = static_cast<int>(info[0].As<Napi::Number>().Int32Value());
2428
- if (ms < 0) ms = 0;
2429
- int prev = dynIntervalMs_.exchange(ms);
2430
- // Auto-enable dynAutoMode when interval > 0, auto-disable when 0.
2431
- dynAutoMode_.store(ms > 0);
2432
- // Start timer thread if transitioning from 0 to non-zero.
2433
- if (prev == 0 && ms > 0 && !dynTimerRunning_.load()) {
2434
- StartDynTimer();
2435
- }
2436
- return env.Undefined();
2437
- }
2438
-
2439
- // -----------------------------------------------------------------------
2440
- // setDynAutoMode(auto) → void
2441
- // true = C++-internal inline dialog path
2442
- // false = legacy JS dialogEval/exitDialog path (default; for debugger)
2443
- // -----------------------------------------------------------------------
2444
- Napi::Value SetDynAutoMode(const Napi::CallbackInfo& info) {
2445
- Napi::Env env = info.Env();
2446
- if (info.Length() < 1 || !info[0].IsBoolean()) {
2447
- Napi::TypeError::New(env, "setDynAutoMode(auto: boolean)")
2448
- .ThrowAsJavaScriptException();
2449
- return env.Undefined();
2450
- }
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
- }
2460
- return env.Undefined();
2461
- }
2462
-
2463
- // -----------------------------------------------------------------------
2464
- // dynamicActive (accessor) → boolean
2465
- // True if registry non-empty and interval > 0.
2466
- // -----------------------------------------------------------------------
2467
- Napi::Value DynamicActive(const Napi::CallbackInfo& info) {
2468
- std::lock_guard<std::mutex> lk(dynMutex_);
2469
- bool active = !dynRegistry_.empty() && dynIntervalMs_.load() > 0;
2470
- return Napi::Boolean::New(info.Env(), active);
2471
- }
2472
-
2473
- // -----------------------------------------------------------------------
2474
- // abort() — interrupt the currently running evaluation.
2475
- Napi::Value Abort(const Napi::CallbackInfo& info) {
2476
- Napi::Env env = info.Env();
2477
- if (!open_) return Napi::Boolean::New(env, false);
2478
- // Only signal the kernel if an evaluation is actually in flight.
2479
- // Sending WSAbortMessage to an idle kernel causes it to emit a
2480
- // spurious RETURNPKT[$Aborted] that would corrupt the next evaluation.
2481
- if (!busy_.load()) return Napi::Boolean::New(env, false);
2482
- // Deduplication: if abortFlag_ is already true, another abort() is already
2483
- // in flight — sending a second WSAbortMessage causes stale response packets
2484
- // that corrupt the next evaluation. Just return true (already aborting).
2485
- bool expected = false;
2486
- if (!abortFlag_.compare_exchange_strong(expected, true))
2487
- return Napi::Boolean::New(env, true); // already aborting — no-op
2488
- // Flush any queued dialogEval/exitDialog requests so their promises
2489
- // reject immediately instead of hanging forever.
2490
- FlushDialogQueueWithError("abort");
2491
- dialogOpen_.store(false);
2492
- int ok = WSPutMessage(lp_, WSAbortMessage);
2493
- return Napi::Boolean::New(env, ok != 0);
2494
- }
2495
-
2496
- // -----------------------------------------------------------------------
2497
- // closeAllDialogs() → boolean
2498
- //
2499
- // Unconditionally resets all dialog state on the JS side:
2500
- // • Drains dialogQueue_, rejecting every pending dialogEval/exitDialog
2501
- // promise with an error (so callers don't hang).
2502
- // • Clears dialogOpen_ so isDialogOpen returns false.
2503
- //
2504
- // This does NOT send any packet to the kernel — it only fixes the Node
2505
- // side bookkeeping. Use it in error-recovery paths (before abort() or
2506
- // after an unexpected kernel state change) to guarantee clean state.
2507
- //
2508
- // Returns true if dialogOpen_ was set before the call (i.e. something
2509
- // was actually cleaned up), false if it was already clear.
2510
- // -----------------------------------------------------------------------
2511
- Napi::Value CloseAllDialogs(const Napi::CallbackInfo& info) {
2512
- Napi::Env env = info.Env();
2513
- bool wasOpen = dialogOpen_.load();
2514
- FlushDialogQueueWithError("dialog closed by closeAllDialogs");
2515
- dialogOpen_.store(false);
2516
- return Napi::Boolean::New(env, wasOpen);
2517
- }
2518
-
2519
- // -----------------------------------------------------------------------
2520
- // createSubsession(kernelPath?) → WstpSession
2521
- //
2522
- // Spawns a completely independent WolframKernel process. Its entire
2523
- // state (variables, definitions, memory) is isolated from the parent
2524
- // and from every other session. Ideal for sandboxed or parallel work.
2525
- // -----------------------------------------------------------------------
2526
- Napi::Value CreateSubsession(const Napi::CallbackInfo& info) {
2527
- Napi::Env env = info.Env();
2528
- Napi::FunctionReference* ctor = env.GetInstanceData<Napi::FunctionReference>();
2529
- if (info.Length() > 0 && info[0].IsString())
2530
- return ctor->New({ info[0] });
2531
- return ctor->New({});
2532
- }
2533
-
2534
- // -----------------------------------------------------------------------
2535
- // close()
2536
- // -----------------------------------------------------------------------
2537
- Napi::Value Close(const Napi::CallbackInfo& info) {
2538
- CleanUp();
2539
- return info.Env().Undefined();
2540
- }
2541
-
2542
- // -----------------------------------------------------------------------
2543
- // isOpen (read-only accessor)
2544
- // -----------------------------------------------------------------------
2545
- Napi::Value IsOpen(const Napi::CallbackInfo& info) {
2546
- return Napi::Boolean::New(info.Env(), open_);
2547
- }
2548
-
2549
- // -----------------------------------------------------------------------
2550
- // isDialogOpen (read-only accessor)
2551
- // -----------------------------------------------------------------------
2552
- Napi::Value IsDialogOpen(const Napi::CallbackInfo& info) {
2553
- return Napi::Boolean::New(info.Env(), dialogOpen_.load());
2554
- }
2555
-
2556
- // -----------------------------------------------------------------------
2557
- // isReady (read-only accessor)
2558
- // true iff the session is open, the kernel has no active evaluation
2559
- // (busy_ is false), no dialog is open, and the eval + sub queues are
2560
- // both empty — i.e. the next evaluate() will start immediately.
2561
- // All reads are on the JS main thread (same thread that writes open_ and
2562
- // the queues), so no extra locking is needed.
2563
- // -----------------------------------------------------------------------
2564
- Napi::Value IsReady(const Napi::CallbackInfo& info) {
2565
- return Napi::Boolean::New(info.Env(),
2566
- open_
2567
- && !busy_.load(std::memory_order_relaxed)
2568
- && !dialogOpen_.load(std::memory_order_relaxed)
2569
- && queue_.empty()
2570
- && subIdleQueue_.empty());
2571
- }
2572
-
2573
- // -----------------------------------------------------------------------
2574
- // kernelPid (read-only accessor)
2575
- // Returns the OS process ID of the WolframKernel child process.
2576
- // Returns 0 if the PID could not be fetched (non-fatal, rare fallback).
2577
- // The PID can be used by the caller to monitor or force-terminate the
2578
- // kernel process independently of the WSTP link (e.g. after a restart).
2579
- // -----------------------------------------------------------------------
2580
- Napi::Value KernelPid(const Napi::CallbackInfo& info) {
2581
- return Napi::Number::New(info.Env(), static_cast<double>(kernelPid_));
2582
- }
2583
-
2584
- private:
2585
- // Queue entry — one pending evaluate() call.
2586
- // interactiveOverride: -1 = use session default, 0 = force batch, 1 = force interactive
2587
- struct QueuedEval {
2588
- std::string expr;
2589
- EvalOptions opts;
2590
- Napi::Promise::Deferred deferred;
2591
- int interactiveOverride = -1;
2592
- };
2593
-
2594
- // -----------------------------------------------------------------------
2595
- // FlushDialogQueueWithError — drain dialogQueue_, rejecting every pending
2596
- // promise with errMsg. Caller must hold no locks; this acquires
2597
- // dialogMutex_ internally. Resets dialogPending_.
2598
- // -----------------------------------------------------------------------
2599
- void FlushDialogQueueWithError(const std::string& errMsg) {
2600
- std::queue<DialogRequest> toFlush;
2601
- {
2602
- std::lock_guard<std::mutex> lk(dialogMutex_);
2603
- std::swap(toFlush, dialogQueue_);
2604
- dialogPending_.store(false);
2605
- }
2606
- while (!toFlush.empty()) {
2607
- DialogRequest req = std::move(toFlush.front());
2608
- toFlush.pop();
2609
- std::string msg = errMsg;
2610
- req.resolve.NonBlockingCall([msg](Napi::Env e, Napi::Function cb) {
2611
- auto obj = Napi::Object::New(e);
2612
- obj.Set("error", Napi::String::New(e, msg));
2613
- cb.Call({obj});
2614
- });
2615
- req.resolve.Release();
2616
- }
2617
- }
2618
-
2619
- // -----------------------------------------------------------------------
2620
- // StartDynTimer — launches the background timer thread that sends
2621
- // WSInterruptMessage at the configured interval when the kernel is busy
2622
- // and Dynamic expressions are registered.
2623
- // Called on the JS main thread; harmless to call if already running.
2624
- // -----------------------------------------------------------------------
2625
- void StartDynTimer() {
2626
- if (dynTimerRunning_.exchange(true)) return; // already running
2627
- if (dynTimerThread_.joinable()) dynTimerThread_.join();
2628
- dynTimerThread_ = std::thread([this]() {
2629
- while (open_ && dynIntervalMs_.load() > 0) {
2630
- int ms = dynIntervalMs_.load();
2631
- std::this_thread::sleep_for(std::chrono::milliseconds(ms > 0 ? ms : 100));
2632
- if (!open_) break;
2633
- if (!busy_.load()) continue; // kernel idle
2634
- {
2635
- std::lock_guard<std::mutex> lk(dynMutex_);
2636
- if (dynRegistry_.empty()) continue; // nothing registered
2637
- }
2638
- if (dynIntervalMs_.load() == 0) break;
2639
- // Check enough time has elapsed since last eval.
2640
- auto elapsed = std::chrono::duration_cast<std::chrono::milliseconds>(
2641
- std::chrono::steady_clock::now() - dynLastEval_).count();
2642
- if (elapsed < dynIntervalMs_.load()) continue;
2643
-
2644
- if (workerReadingLink_.load() && !dialogOpen_.load() && !dynAutoMode_.load()) {
2645
- WSPutMessage(lp_, WSInterruptMessage);
2646
- }
2647
- }
2648
- dynTimerRunning_.store(false);
2649
- });
2650
- dynTimerThread_.detach();
2651
- }
2652
-
2653
- void CleanUp() {
2654
- // Stop the Dynamic timer thread.
2655
- dynIntervalMs_.store(0);
2656
- dynTimerRunning_.store(false);
2657
-
2658
- // Immediately reject all queued subWhenIdle() requests before the link
2659
- // is torn down. These items have never been dispatched to a worker so
2660
- // they won't receive an OnError callback — we must reject them here.
2661
- {
2662
- std::lock_guard<std::mutex> lk(queueMutex_);
2663
- while (!whenIdleQueue_.empty()) {
2664
- auto& wi = whenIdleQueue_.front();
2665
- wi.deferred.Reject(
2666
- Napi::Error::New(wi.deferred.Env(), "Session is closed").Value());
2667
- whenIdleQueue_.pop();
2668
- }
2669
- }
2670
- // If a worker thread is currently reading from lp_, calling WSClose()
2671
- // on it from the JS main thread causes a concurrent-access crash
2672
- // (heap-use-after-free / SIGSEGV).
2673
- //
2674
- // We spin on workerReadingLink_ (set false by Execute() on the background
2675
- // thread) rather than busy_ (set false by OnOK/OnError on the main thread).
2676
- // Spinning on busy_ from the main thread would deadlock because the main
2677
- // thread's event loop is blocked — NAPI never gets to call OnOK.
2678
- open_ = false; // prevent new evals from queuing during shutdown
2679
- if (workerReadingLink_.load(std::memory_order_acquire) && lp_) {
2680
- abortFlag_.store(true);
2681
- FlushDialogQueueWithError("session closed");
2682
- dialogOpen_.store(false);
2683
- WSPutMessage(lp_, WSAbortMessage);
2684
- const auto deadline =
2685
- std::chrono::steady_clock::now() + std::chrono::seconds(10);
2686
- while (workerReadingLink_.load(std::memory_order_acquire) &&
2687
- std::chrono::steady_clock::now() < deadline)
2688
- std::this_thread::sleep_for(std::chrono::milliseconds(5));
2689
- }
2690
- if (lp_) { WSClose(lp_); lp_ = nullptr; }
2691
- if (wsEnv_) { WSDeinitialize(wsEnv_); wsEnv_ = nullptr; }
2692
- // Kill the child kernel process so it doesn't become a zombie.
2693
- // WSClose() closes the link but does not terminate the WolframKernel
2694
- // child process — without this, each session leaks a kernel.
2695
- // kernelPid_ is intentionally NOT zeroed here so .kernelPid remains
2696
- // readable after close() (useful for post-mortem logging / force-kill).
2697
- // kernelKilled_ prevents a double-kill when the destructor later calls
2698
- // CleanUp() a second time.
2699
- if (kernelPid_ > 0 && !kernelKilled_) { kernelKilled_ = true; kill(kernelPid_, SIGTERM); }
2700
- }
2701
-
2702
-
2703
- // ---------------------------------------------------------------------------
2704
- // Verify that $Output→TextPacket routing is live before the session is used.
2705
- // Sends Print["$WARMUP$"] and looks for a TextPacket response, retrying up to
2706
- // 5 times with a 100 ms pause between attempts. On some WolframKernel
2707
- // launches (observed ~20% of consecutive runs) the kernel processes the
2708
- // first EvaluatePacket before its internal $Output stream is wired to the
2709
- // WSTP link, causing ALL subsequent Print[]/Message[] calls to silently
2710
- // drop their output. One extra round-trip here forces the kernel to
2711
- // complete its output-routing setup before any user code is evaluated.
2712
- // Returns true if output routing is confirmed; false if it cannot be fixed.
2713
- // ---------------------------------------------------------------------------
2714
- static bool WarmUpOutputRouting(WSLINK lp) {
2715
- // Brief initial pause after WSActivate: lets the kernel scheduler
2716
- // fully wire $Output → WSTP TextPacket before the first probe.
2717
- std::this_thread::sleep_for(std::chrono::milliseconds(100));
2718
- for (int attempt = 0; attempt < 4; ++attempt) {
2719
- if (attempt > 0)
2720
- std::this_thread::sleep_for(std::chrono::milliseconds(200));
2721
-
2722
- // Send EvaluatePacket[Print["$WARMUP$"]]
2723
- if (!WSPutFunction(lp, "EvaluatePacket", 1) ||
2724
- !WSPutFunction(lp, "Print", 1) ||
2725
- !WSPutString (lp, "$WARMUP$") ||
2726
- !WSEndPacket (lp) ||
2727
- !WSFlush (lp))
2728
- return false; // link error
2729
-
2730
- bool gotText = false;
2731
- while (true) {
2732
- int pkt = WSNextPacket(lp);
2733
- if (pkt == TEXTPKT) {
2734
- WSNewPacket(lp);
2735
- gotText = true;
2736
- } else if (pkt == RETURNPKT) {
2737
- WSNewPacket(lp);
2738
- break;
2739
- } else if (pkt == 0 || pkt == ILLEGALPKT) {
2740
- WSClearError(lp);
2741
- return false;
2742
- } else {
2743
- WSNewPacket(lp);
2744
- }
2745
- }
2746
- if (gotText) {
2747
- DiagLog("[WarmUp] $Output routing verified on attempt " + std::to_string(attempt + 1));
2748
- return true;
2749
- }
2750
- DiagLog("[WarmUp] no TextPacket on attempt " + std::to_string(attempt + 1) + ", retrying...");
2751
- }
2752
- DiagLog("[WarmUp] $Output routing NOT confirmed — kernel restart needed");
2753
- return false;
2754
- }
2755
-
2756
- // ---------------------------------------------------------------------------
2757
- // Synchronously evaluate $ProcessID and return the kernel's PID.
2758
- // Called once from the constructor after WSActivate, while the link is idle.
2759
- // Returns 0 on failure (non-fatal — cleanup falls back to no-kill).
2760
- // ---------------------------------------------------------------------------
2761
- static pid_t FetchKernelPid(WSLINK lp) {
2762
- // Send: EvaluatePacket[ToExpression["$ProcessID"]]
2763
- if (!WSPutFunction(lp, "EvaluatePacket", 1) ||
2764
- !WSPutFunction(lp, "ToExpression", 1) ||
2765
- !WSPutString (lp, "$ProcessID") ||
2766
- !WSEndPacket (lp) ||
2767
- !WSFlush (lp))
2768
- return 0;
2769
-
2770
- // Drain packets until we see ReturnPacket with an integer.
2771
- pid_t pid = 0;
2772
- while (true) {
2773
- int pkt = WSNextPacket(lp);
2774
- if (pkt == RETURNPKT) {
2775
- if (WSGetType(lp) == WSTKINT) {
2776
- wsint64 v = 0;
2777
- WSGetInteger64(lp, &v);
2778
- pid = static_cast<pid_t>(v);
2779
- }
2780
- WSNewPacket(lp);
2781
- break;
2782
- }
2783
- if (pkt == 0 || pkt == ILLEGALPKT) { WSClearError(lp); break; }
2784
- WSNewPacket(lp); // skip INPUTNAMEPKT, OUTPUTNAMEPKT, etc.
2785
- }
2786
- return pid;
2787
- }
2788
-
2789
- WSEnvironment wsEnv_;
2790
- WSLINK lp_;
2791
- bool open_;
2792
- bool interactiveMode_ = false; // true → EnterTextPacket, populates In/Out
2793
- pid_t kernelPid_ = 0; // child process PID — preserved after close() for .kernelPid accessor
2794
- bool kernelKilled_ = false; // guards against double-kill across close() + destructor
2795
- std::atomic<int64_t> nextLine_{1}; // 1-based In[n] counter for EvalResult.cellIndex
2796
- std::atomic<bool> abortFlag_{false};
2797
- std::atomic<bool> busy_{false};
2798
- // Set true before queuing a worker, set false from within Execute() (background
2799
- // thread) right after DrainToEvalResult returns. CleanUp() spins on this flag
2800
- // rather than busy_ (which is cleared on the main thread and cannot be polled
2801
- // from a main-thread spin loop).
2802
- std::atomic<bool> workerReadingLink_{false};
2803
- std::mutex queueMutex_;
2804
- std::queue<QueuedEval> queue_;
2805
- std::queue<QueuedSubIdle> subIdleQueue_; // sub() — runs before queue_ items
2806
- std::queue<QueuedWhenIdle> whenIdleQueue_; // subWhenIdle() — lowest priority, runs when truly idle
2807
- // Dialog subsession state — written on main thread, consumed on thread pool
2808
- std::mutex dialogMutex_;
2809
- std::queue<DialogRequest> dialogQueue_; // dialogEval() requests
2810
- std::atomic<bool> dialogPending_{false};
2811
- std::atomic<bool> dialogOpen_{false};
2812
-
2813
- // -----------------------------------------------------------------------
2814
- // Dynamic evaluation state (Phase 2 — C++-internal Dynamic eval)
2815
- // -----------------------------------------------------------------------
2816
- std::mutex dynMutex_;
2817
- std::vector<DynRegistration> dynRegistry_; // registered exprs
2818
- std::vector<DynResult> dynResults_; // accumulated results (swapped on getDynamicResults)
2819
- std::atomic<int> dynIntervalMs_{0}; // 0 = disabled
2820
- std::atomic<bool> dynAutoMode_{false}; // true = inline C++ path; false = legacy JS path
2821
- int dynTaskInstalledInterval_{0}; // interval of currently installed ScheduledTask (0 = none)
2822
- std::chrono::steady_clock::time_point dynLastEval_{}; // time of last successful dialog eval
2823
- std::thread dynTimerThread_;
2824
- std::atomic<bool> dynTimerRunning_{false};
2825
- };
2826
-
2827
- // ===========================================================================
2828
- // ReadNextWorker — async "read one expression" for WstpReader
2829
- // ===========================================================================
2830
- class ReadNextWorker : public Napi::AsyncWorker {
2831
- public:
2832
- ReadNextWorker(Napi::Promise::Deferred deferred, WSLINK lp, std::atomic<bool>& activated)
2833
- : Napi::AsyncWorker(deferred.Env()), deferred_(deferred), lp_(lp), activated_(activated) {}
2834
-
2835
- // Thread-pool: activate on first call, then read next expression.
2836
- void Execute() override {
2837
- // WSActivate must happen on a thread-pool thread, never on the JS
2838
- // main thread. The kernel's WSTP runtime can only complete the
2839
- // handshake once it is inside the Do-loop calling LinkWrite.
2840
- if (!activated_.load()) {
2841
- DiagLog("[WstpReader] WSActivate starting...");
2842
- if (!WSActivate(lp_)) {
2843
- const char* msg = WSErrorMessage(lp_);
2844
- std::string err = std::string("WstpReader: WSActivate failed: ") + (msg ? msg : "?");
2845
- DiagLog("[WstpReader] " + err);
2846
- SetError(err);
2847
- WSClearError(lp_);
2848
- return;
2849
- }
2850
- // DO NOT call WSGetType / WSNewPacket here.
2851
- // On TCPIP connect-mode links WSGetType is non-blocking: it returns 0
2852
- // for *both* "no data in buffer yet" and a genuine WSTKEND boundary
2853
- // token. Calling WSNewPacket when there is no data in the buffer
2854
- // corrupts the link's internal read state so that every subsequent
2855
- // WSGetType also returns 0, making all future reads hang forever.
2856
- // The WSTKEND / preamble case (if it occurs at all) is handled below
2857
- // after WSWaitForLinkActivity returns and data is confirmed present.
2858
- DiagLog("[WstpReader] activated.");
2859
- activated_.store(true);
2860
- }
2861
-
2862
- // Spin on WSGetType() waiting for the link buffer to become non-empty.
2863
- //
2864
- // WSGetType() is non-blocking on TCPIP connect-mode links: it returns
2865
- // 0 immediately if no data is buffered. We spin in 5 ms increments
2866
- // rather than using WSWaitForLinkActivity(), which has been observed to
2867
- // return WSWAITSUCCESS before the buffer is actually readable on fast
2868
- // consecutive runs, leading to a ReadExprRaw(type=0) error.
2869
- //
2870
- // The first 500 ms are traced at each distinct type value so failures
2871
- // can be diagnosed from the log. Hard timeout: 5 seconds.
2872
- {
2873
- auto spinStart = std::chrono::steady_clock::now();
2874
- auto deadline = spinStart + std::chrono::seconds(5);
2875
- auto traceWindow = spinStart + std::chrono::milliseconds(500);
2876
- int iters = 0, lastLoggedType = -999;
2877
- while (true) {
2878
- int t = WSGetType(lp_);
2879
- ++iters;
2880
- auto now = std::chrono::steady_clock::now();
2881
- // Within the first 500 ms log every distinct type change.
2882
- if (now < traceWindow && t != lastLoggedType) {
2883
- auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
2884
- now - spinStart).count();
2885
- DiagLog("[WstpReader] spin t=" + std::to_string(t)
2886
- + " +" + std::to_string(ms) + "ms iter=" + std::to_string(iters));
2887
- lastLoggedType = t;
2888
- }
2889
- if (t != 0) {
2890
- auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
2891
- now - spinStart).count();
2892
- DiagLog("[WstpReader] spin done: type=" + std::to_string(t)
2893
- + " after " + std::to_string(iters) + " iters +"
2894
- + std::to_string(ms) + "ms");
2895
- break;
2896
- }
2897
- if (now >= deadline) {
2898
- DiagLog("[WstpReader] spin TIMEOUT after " + std::to_string(iters)
2899
- + " iters (5s)");
2900
- SetError("WstpReader: 5-second timeout — link dead or data never arrived");
2901
- return;
2902
- }
2903
- std::this_thread::sleep_for(std::chrono::milliseconds(5));
2904
- }
2905
- }
2906
-
2907
- result_ = ReadExprRaw(lp_);
2908
-
2909
- // If ReadExprRaw still encountered WSTKEND (type=0) it means the spin
2910
- // exited on a protocol boundary token, not an expression token. Skip
2911
- // it once with WSNewPacket and re-spin for the real expression.
2912
- if (result_.kind == WExpr::WError
2913
- && result_.strVal.find("unexpected token type: 0") != std::string::npos) {
2914
- DiagLog("[WstpReader] got WSTKEND from ReadExprRaw — skipping preamble, re-spinning");
2915
- WSNewPacket(lp_);
2916
- auto spinStart2 = std::chrono::steady_clock::now();
2917
- auto deadline2 = spinStart2 + std::chrono::seconds(5);
2918
- int iters2 = 0;
2919
- while (true) {
2920
- int t = WSGetType(lp_);
2921
- ++iters2;
2922
- if (t != 0) {
2923
- DiagLog("[WstpReader] re-spin done: type=" + std::to_string(t)
2924
- + " after " + std::to_string(iters2) + " iters");
2925
- break;
2926
- }
2927
- if (std::chrono::steady_clock::now() >= deadline2) {
2928
- DiagLog("[WstpReader] re-spin TIMEOUT after " + std::to_string(iters2)
2929
- + " iters");
2930
- result_ = WExpr::mkError("WstpReader: 5-second timeout after WSTKEND skip");
2931
- return;
2932
- }
2933
- std::this_thread::sleep_for(std::chrono::milliseconds(5));
2934
- }
2935
- result_ = ReadExprRaw(lp_);
2936
- }
2937
-
2938
- // After a successful expression read, advance past the expression
2939
- // boundary so the next WSGetType() call sees the next expression's
2940
- // start marker rather than a residual WSTKEND.
2941
- if (result_.kind != WExpr::WError)
2942
- WSNewPacket(lp_);
2943
-
2944
- DiagLog("[WstpReader] ReadExprRaw kind=" + std::to_string((int)result_.kind)
2945
- + (result_.kind == WExpr::WError ? " err=" + result_.strVal : ""));
2946
- }
2947
-
2948
- void OnOK() override {
2949
- Napi::Env env = Env();
2950
- if (result_.kind == WExpr::WError) {
2951
- deferred_.Reject(Napi::Error::New(env, result_.strVal).Value());
2952
- return;
2953
- }
2954
- Napi::Value v = WExprToNapi(env, result_);
2955
- if (env.IsExceptionPending())
2956
- deferred_.Reject(env.GetAndClearPendingException().Value());
2957
- else
2958
- deferred_.Resolve(v);
2959
- }
2960
-
2961
- void OnError(const Napi::Error& e) override {
2962
- deferred_.Reject(e.Value());
2963
- }
2964
-
2965
- private:
2966
- Napi::Promise::Deferred deferred_;
2967
- WSLINK lp_;
2968
- std::atomic<bool>& activated_;
2969
- WExpr result_;
2970
- };
2971
-
2972
- // ===========================================================================
2973
- // WstpReader — connects to a named WSTP link created by a Wolfram kernel.
2974
- //
2975
- // Usage pattern:
2976
- // 1. Main kernel: $link = LinkCreate[LinkProtocol->"TCPIP"]
2977
- // linkName = LinkName[$link] // → "port@host,0@host"
2978
- // 2. JS: const reader = new WstpReader(linkName)
2979
- // 3. Loop: while (reader.isOpen) { const v = await reader.readNext() }
2980
- //
2981
- // Each call to readNext() blocks (on the thread pool) until the next
2982
- // expression is available, then resolves with an ExprTree.
2983
- // When the kernel closes the link (LinkClose[$link]), readNext() rejects
2984
- // with a "link closed" error.
2985
- // ===========================================================================
2986
- class WstpReader : public Napi::ObjectWrap<WstpReader> {
2987
- public:
2988
- static Napi::Object Init(Napi::Env env, Napi::Object exports) {
2989
- Napi::Function func = DefineClass(env, "WstpReader", {
2990
- InstanceMethod<&WstpReader::ReadNext>("readNext"),
2991
- InstanceMethod<&WstpReader::Close> ("close"),
2992
- InstanceAccessor<&WstpReader::IsOpen>("isOpen"),
2993
- });
2994
- exports.Set("WstpReader", func);
2995
- return exports;
2996
- }
2997
-
2998
- // -----------------------------------------------------------------------
2999
- // Constructor new WstpReader(linkName, protocol?)
3000
- // linkName — value returned by Wolfram's LinkName[$link]
3001
- // protocol — link protocol string, default "TCPIP"
3002
- // -----------------------------------------------------------------------
3003
- WstpReader(const Napi::CallbackInfo& info)
3004
- : Napi::ObjectWrap<WstpReader>(info), wsEnv_(nullptr), lp_(nullptr), open_(false)
3005
- {
3006
- Napi::Env env = info.Env();
3007
- if (info.Length() < 1 || !info[0].IsString()) {
3008
- Napi::TypeError::New(env, "WstpReader(linkName: string, protocol?: string)")
3009
- .ThrowAsJavaScriptException();
3010
- return;
3011
- }
3012
- std::string linkName = info[0].As<Napi::String>().Utf8Value();
3013
- std::string protocol = "TCPIP";
3014
- if (info.Length() >= 2 && info[1].IsString())
3015
- protocol = info[1].As<Napi::String>().Utf8Value();
3016
-
3017
- WSEnvironmentParameter params = WSNewParameters(WSREVISION, WSAPIREVISION);
3018
- wsEnv_ = WSInitialize(params);
3019
- WSReleaseParameters(params);
3020
- if (!wsEnv_) {
3021
- Napi::Error::New(env, "WSInitialize failed (WstpReader)")
3022
- .ThrowAsJavaScriptException();
3023
- return;
3024
- }
3025
-
3026
- // Connect to the already-listening link.
3027
- const char* argv[] = {
3028
- "reader",
3029
- "-linkname", linkName.c_str(),
3030
- "-linkmode", "connect",
3031
- "-linkprotocol", protocol.c_str()
3032
- };
3033
- int err = 0;
3034
- lp_ = WSOpenArgcArgv(wsEnv_, 7, const_cast<char**>(argv), &err);
3035
- if (!lp_ || err != WSEOK) {
3036
- std::string msg = "WstpReader: WSOpenArgcArgv failed (code "
3037
- + std::to_string(err) + ") for link \"" + linkName + "\"";
3038
- WSDeinitialize(wsEnv_); wsEnv_ = nullptr;
3039
- Napi::Error::New(env, msg).ThrowAsJavaScriptException();
3040
- return;
3041
- }
3042
-
3043
- // Do NOT call WSActivate here — it would block the JS main thread.
3044
- // Activation is deferred to the first ReadNextWorker::Execute() call,
3045
- // which runs on the libuv thread pool. The kernel's WSTP runtime
3046
- // completes the handshake once it enters the Do-loop and calls LinkWrite.
3047
- open_ = true;
3048
- activated_.store(false);
3049
- }
3050
-
3051
- ~WstpReader() { CleanUp(); }
3052
-
3053
- // readNext() → Promise<ExprTree>
3054
- Napi::Value ReadNext(const Napi::CallbackInfo& info) {
3055
- Napi::Env env = info.Env();
3056
- auto deferred = Napi::Promise::Deferred::New(env);
3057
- if (!open_) {
3058
- deferred.Reject(Napi::Error::New(env, "WstpReader is closed").Value());
3059
- return deferred.Promise();
3060
- }
3061
- (new ReadNextWorker(deferred, lp_, activated_))->Queue();
3062
- return deferred.Promise();
3063
- }
3064
-
3065
- // close()
3066
- Napi::Value Close(const Napi::CallbackInfo& info) {
3067
- CleanUp();
3068
- return info.Env().Undefined();
3069
- }
3070
-
3071
- // isOpen
3072
- Napi::Value IsOpen(const Napi::CallbackInfo& info) {
3073
- return Napi::Boolean::New(info.Env(), open_);
3074
- }
3075
-
3076
- private:
3077
- void CleanUp() {
3078
- if (lp_) { WSClose(lp_); lp_ = nullptr; }
3079
- if (wsEnv_) { WSDeinitialize(wsEnv_); wsEnv_ = nullptr; }
3080
- open_ = false;
3081
- }
3082
-
3083
- WSEnvironment wsEnv_;
3084
- WSLINK lp_;
3085
- bool open_;
3086
- std::atomic<bool> activated_{false};
3087
- };
3088
-
3089
- // ===========================================================================
3090
- // setDiagHandler(fn) — JS-callable; registers the global diagnostic callback.
3091
- // Pass null / no argument to clear. The TSFN is Unref()'d so it does not
3092
- // hold the Node.js event loop open.
3093
- // ===========================================================================
3094
22
  static Napi::Value SetDiagHandler(const Napi::CallbackInfo& info) {
3095
23
  Napi::Env env = info.Env();
3096
24
  std::lock_guard<std::mutex> lk(g_diagMutex);
@@ -3105,22 +33,21 @@ static Napi::Value SetDiagHandler(const Napi::CallbackInfo& info) {
3105
33
  "diagHandler",
3106
34
  /*maxQueueSize=*/0,
3107
35
  /*initialThreadCount=*/1);
3108
- g_diagTsfn.Unref(env); // do not prevent process exit
36
+ g_diagTsfn.Unref(env);
3109
37
  g_diagActive = true;
3110
38
  }
3111
39
  return env.Undefined();
3112
40
  }
3113
41
 
3114
- // ===========================================================================
42
+ // ---------------------------------------------------------------------------
3115
43
  // Module entry point
3116
- // ===========================================================================
44
+ // ---------------------------------------------------------------------------
3117
45
  Napi::Object InitModule(Napi::Env env, Napi::Object exports) {
3118
46
  WstpSession::Init(env, exports);
3119
47
  WstpReader::Init(env, exports);
3120
48
  exports.Set("setDiagHandler",
3121
49
  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"));
50
+ exports.Set("version", Napi::String::New(env, "0.7.0"));
3124
51
  return exports;
3125
52
  }
3126
53