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/README.md +49 -32
- package/binding.gyp +9 -1
- package/build/Release/wstp.node +0 -0
- package/build.sh +33 -20
- package/package.json +1 -1
- package/src/addon.cc +19 -3092
- package/test.js +0 -1653
- package/test_interrupt_dialog.js +0 -220
package/src/addon.cc
CHANGED
|
@@ -1,3096 +1,24 @@
|
|
|
1
1
|
// =============================================================================
|
|
2
|
-
// wstp-backend/src/addon.cc (
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
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.0 — thin 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
|
|
17
|
-
#include
|
|
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
|
-
//
|
|
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);
|
|
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
|
-
|
|
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
|
|