wstp-node 0.3.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/LICENSE +21 -0
- package/README.md +1046 -0
- package/binding.gyp +52 -0
- package/build/Release/wstp.node +0 -0
- package/build.sh +134 -0
- package/examples/demo.js +340 -0
- package/examples/monitor_demo.js +206 -0
- package/index.d.ts +284 -0
- package/package.json +51 -0
- package/src/addon.cc +2116 -0
- package/test.js +660 -0
- package/test_interrupt_dialog.js +220 -0
package/src/addon.cc
ADDED
|
@@ -0,0 +1,2116 @@
|
|
|
1
|
+
// =============================================================================
|
|
2
|
+
// wstp-backend/src/addon.cc (v6 — dialog subsession support)
|
|
3
|
+
//
|
|
4
|
+
// Architecture:
|
|
5
|
+
// Execute() runs on the libuv thread pool → does ALL blocking WSTP I/O,
|
|
6
|
+
// stores result in plain C++ structs (WExpr / EvalResult, no NAPI).
|
|
7
|
+
// OnOK() runs on the JS main thread → converts to Napi::Value.
|
|
8
|
+
//
|
|
9
|
+
// EvalResult captures everything the kernel sends for one cell:
|
|
10
|
+
// cellIndex (1-based, tracked by WstpSession::nextLine_), outputName,
|
|
11
|
+
// return value, Print[] lines, and messages.
|
|
12
|
+
// This means the Node.js event loop is NEVER blocked, so abort() fires
|
|
13
|
+
// correctly while Execute() is waiting inside WSNextPacket() on the thread pool.
|
|
14
|
+
// =============================================================================
|
|
15
|
+
|
|
16
|
+
#include <napi.h>
|
|
17
|
+
#include <wstp.h>
|
|
18
|
+
|
|
19
|
+
#include <atomic>
|
|
20
|
+
#include <chrono>
|
|
21
|
+
#include <cstdint>
|
|
22
|
+
#include <thread>
|
|
23
|
+
#include <functional>
|
|
24
|
+
#include <memory>
|
|
25
|
+
#include <mutex>
|
|
26
|
+
#include <queue>
|
|
27
|
+
#include <string>
|
|
28
|
+
#include <thread>
|
|
29
|
+
#include <vector>
|
|
30
|
+
|
|
31
|
+
#include <signal.h> // kill(), SIGTERM
|
|
32
|
+
#include <sys/types.h>
|
|
33
|
+
|
|
34
|
+
// ===========================================================================
|
|
35
|
+
// Plain C++ expression tree — no NAPI, safe to build on any thread.
|
|
36
|
+
// ===========================================================================
|
|
37
|
+
struct WExpr {
|
|
38
|
+
enum Kind { Integer, Real, String, Symbol, Function, WError } kind = WError;
|
|
39
|
+
|
|
40
|
+
int64_t intVal = 0;
|
|
41
|
+
double realVal = 0.0;
|
|
42
|
+
std::string strVal; // string content, symbol name, or error msg
|
|
43
|
+
std::string head; // function head symbol
|
|
44
|
+
std::vector<WExpr> args; // function arguments
|
|
45
|
+
|
|
46
|
+
static WExpr mkError(const std::string& msg) {
|
|
47
|
+
WExpr e; e.kind = WError; e.strVal = msg; return e;
|
|
48
|
+
}
|
|
49
|
+
static WExpr mkSymbol(const std::string& name) {
|
|
50
|
+
WExpr e; e.kind = Symbol; e.strVal = name; return e;
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// EvalResult — everything the kernel sends for one cell (thread-pool safe).
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
struct EvalResult {
|
|
58
|
+
int64_t cellIndex = 0; // 1-based counter, tracked by WstpSession::nextLine_
|
|
59
|
+
std::string outputName; // "Out[n]=" when result is non-Null, "" otherwise
|
|
60
|
+
WExpr result; // the ReturnPacket payload
|
|
61
|
+
std::vector<std::string> print; // TextPacket lines in order
|
|
62
|
+
std::vector<std::string> messages; // e.g. "Power::infy: Infinite expression..."
|
|
63
|
+
bool aborted = false;
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
// EvalOptions — optional streaming callbacks for one evaluate() call.
|
|
68
|
+
// ThreadSafeFunctions (TSFNs) are safe to call from any thread, which is
|
|
69
|
+
// what makes streaming from Execute() (thread pool) possible.
|
|
70
|
+
// ---------------------------------------------------------------------------
|
|
71
|
+
|
|
72
|
+
// Latch that fires fn() once after ALL parties have called done().
|
|
73
|
+
// remaining is initialised to numTsfns + 1 (the +1 is for OnOK / OnError).
|
|
74
|
+
// IMPORTANT: all calls happen on the main thread — no atomics needed.
|
|
75
|
+
struct CompleteCtx {
|
|
76
|
+
int remaining;
|
|
77
|
+
std::function<void()> fn; // set by OnOK/OnError; called when remaining → 0
|
|
78
|
+
void done() {
|
|
79
|
+
if (--remaining == 0) { if (fn) fn(); delete this; }
|
|
80
|
+
}
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
// ---------------------------------------------------------------------------
|
|
84
|
+
// DialogRequest — one dialogEval() call queued for the thread pool.
|
|
85
|
+
// Written on the main thread; consumed exclusively on the thread pool while
|
|
86
|
+
// the dialog inner loop is running. The TSFN delivers the result back.
|
|
87
|
+
// ---------------------------------------------------------------------------
|
|
88
|
+
struct DialogRequest {
|
|
89
|
+
std::string expr;
|
|
90
|
+
bool useEnterText = false; // true → EnterTextPacket; false → EvaluatePacket
|
|
91
|
+
Napi::ThreadSafeFunction resolve; // NonBlockingCall'd with the WExpr result
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
struct EvalOptions {
|
|
95
|
+
Napi::ThreadSafeFunction onPrint; // fires once per Print[] line
|
|
96
|
+
Napi::ThreadSafeFunction onMessage; // fires once per kernel message
|
|
97
|
+
Napi::ThreadSafeFunction onDialogBegin; // fires with dialog level (int)
|
|
98
|
+
Napi::ThreadSafeFunction onDialogPrint; // fires with dialog Print[] lines
|
|
99
|
+
Napi::ThreadSafeFunction onDialogEnd; // fires with dialog level (int)
|
|
100
|
+
bool hasOnPrint = false;
|
|
101
|
+
bool hasOnMessage = false;
|
|
102
|
+
bool hasOnDialogBegin = false;
|
|
103
|
+
bool hasOnDialogPrint = false;
|
|
104
|
+
bool hasOnDialogEnd = false;
|
|
105
|
+
// When true, DrainToEvalResult expects EnterExpressionPacket protocol:
|
|
106
|
+
// INPUTNAMEPKT → OUTPUTNAMEPKT → RETURNEXPRPKT (or INPUTNAMEPKT for Null).
|
|
107
|
+
bool interactive = false;
|
|
108
|
+
CompleteCtx* ctx = nullptr; // non-owning; set when TSFNs are in use
|
|
109
|
+
|
|
110
|
+
// Pointers to session's dialog queue — set by WstpSession::Evaluate() so the
|
|
111
|
+
// drain loop can service dialogEval() requests from the thread pool.
|
|
112
|
+
// Non-owning; valid for the lifetime of the EvaluateWorker.
|
|
113
|
+
std::mutex* dialogMutex = nullptr;
|
|
114
|
+
std::queue<DialogRequest>* dialogQueue = nullptr;
|
|
115
|
+
std::atomic<bool>* dialogPending = nullptr;
|
|
116
|
+
std::atomic<bool>* dialogOpen = nullptr;
|
|
117
|
+
// Session-level abort flag — set by abort() on the main thread; checked in
|
|
118
|
+
// the dialog inner loop to break out proactively when abort() is called.
|
|
119
|
+
std::atomic<bool>* abortFlag = nullptr;
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
// ===========================================================================
|
|
123
|
+
// Module-level diagnostic channel.
|
|
124
|
+
// setDiagHandler(fn) registers a JS callback; DiagLog(msg) fires it from any
|
|
125
|
+
// C++ thread. Both the global flag and the TSFN are guarded by g_diagMutex.
|
|
126
|
+
// The TSFN is Unref()'d so it does not prevent the Node.js event loop from
|
|
127
|
+
// exiting normally.
|
|
128
|
+
// ===========================================================================
|
|
129
|
+
static std::mutex g_diagMutex;
|
|
130
|
+
static Napi::ThreadSafeFunction g_diagTsfn;
|
|
131
|
+
static bool g_diagActive = false;
|
|
132
|
+
|
|
133
|
+
// If DEBUG_WSTP=1 is set in the environment at module-load time, every
|
|
134
|
+
// DiagLog message is also written synchronously to stderr via fwrite.
|
|
135
|
+
// Useful when no JS setDiagHandler is registered (e.g. bare node runs).
|
|
136
|
+
static const bool g_debugToStderr = []() {
|
|
137
|
+
const char* v = getenv("DEBUG_WSTP");
|
|
138
|
+
return v && v[0] == '1';
|
|
139
|
+
}();
|
|
140
|
+
|
|
141
|
+
// Module-relative timestamp helper — milliseconds since module load.
|
|
142
|
+
// Used to embed C++ dispatch times in log messages so JS-side handler
|
|
143
|
+
// timestamps can be compared to measure TSFN delivery latency.
|
|
144
|
+
static auto g_startTime = std::chrono::steady_clock::now();
|
|
145
|
+
static long long diagMs() {
|
|
146
|
+
return std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
147
|
+
std::chrono::steady_clock::now() - g_startTime).count();
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
static void DiagLog(const std::string& msg) {
|
|
151
|
+
if (g_debugToStderr) {
|
|
152
|
+
std::string out = "[wstp +" + std::to_string(diagMs()) + "ms] " + msg + "\n";
|
|
153
|
+
fwrite(out.c_str(), 1, out.size(), stderr);
|
|
154
|
+
}
|
|
155
|
+
std::lock_guard<std::mutex> lk(g_diagMutex);
|
|
156
|
+
if (!g_diagActive) return;
|
|
157
|
+
// NonBlockingCall with lambda — copies msg by value into the captured closure.
|
|
158
|
+
g_diagTsfn.NonBlockingCall(
|
|
159
|
+
[msg](Napi::Env env, Napi::Function fn) {
|
|
160
|
+
fn.Call({ Napi::String::New(env, msg) });
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ---------------------------------------------------------------------------
|
|
165
|
+
// ReadExprRaw — build a WExpr from one WSTP token/expression (any thread).
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
static WExpr ReadExprRaw(WSLINK lp, int depth = 0) {
|
|
168
|
+
if (depth > 512) return WExpr::mkError("expression too deep");
|
|
169
|
+
|
|
170
|
+
int type = WSGetType(lp);
|
|
171
|
+
|
|
172
|
+
if (type == WSTKINT) {
|
|
173
|
+
wsint64 i = 0;
|
|
174
|
+
if (!WSGetInteger64(lp, &i))
|
|
175
|
+
return WExpr::mkError("WSGetInteger64 failed");
|
|
176
|
+
WExpr e; e.kind = WExpr::Integer; e.intVal = i;
|
|
177
|
+
return e;
|
|
178
|
+
}
|
|
179
|
+
if (type == WSTKREAL) {
|
|
180
|
+
double d = 0.0;
|
|
181
|
+
if (!WSGetReal64(lp, &d))
|
|
182
|
+
return WExpr::mkError("WSGetReal64 failed");
|
|
183
|
+
WExpr e; e.kind = WExpr::Real; e.realVal = d;
|
|
184
|
+
return e;
|
|
185
|
+
}
|
|
186
|
+
if (type == WSTKSTR) {
|
|
187
|
+
const char* s = nullptr;
|
|
188
|
+
if (!WSGetString(lp, &s))
|
|
189
|
+
return WExpr::mkError("WSGetString failed");
|
|
190
|
+
WExpr e; e.kind = WExpr::String; e.strVal = s;
|
|
191
|
+
WSReleaseString(lp, s);
|
|
192
|
+
return e;
|
|
193
|
+
}
|
|
194
|
+
if (type == WSTKSYM) {
|
|
195
|
+
const char* s = nullptr;
|
|
196
|
+
if (!WSGetSymbol(lp, &s))
|
|
197
|
+
return WExpr::mkError("WSGetSymbol failed");
|
|
198
|
+
WExpr e; e.kind = WExpr::Symbol; e.strVal = s;
|
|
199
|
+
WSReleaseSymbol(lp, s);
|
|
200
|
+
return e;
|
|
201
|
+
}
|
|
202
|
+
if (type == WSTKFUNC) {
|
|
203
|
+
const char* head = nullptr;
|
|
204
|
+
int argc = 0;
|
|
205
|
+
if (!WSGetFunction(lp, &head, &argc))
|
|
206
|
+
return WExpr::mkError("WSGetFunction failed");
|
|
207
|
+
WExpr e;
|
|
208
|
+
e.kind = WExpr::Function;
|
|
209
|
+
e.head = head;
|
|
210
|
+
WSReleaseSymbol(lp, head);
|
|
211
|
+
e.args.reserve(argc);
|
|
212
|
+
for (int i = 0; i < argc; ++i) {
|
|
213
|
+
WExpr child = ReadExprRaw(lp, depth + 1);
|
|
214
|
+
if (child.kind == WExpr::WError) return child;
|
|
215
|
+
e.args.push_back(std::move(child));
|
|
216
|
+
}
|
|
217
|
+
return e;
|
|
218
|
+
}
|
|
219
|
+
return WExpr::mkError("unexpected token type: " + std::to_string(type));
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Forward declaration — WExprToNapi is defined after WstpSession.
|
|
223
|
+
static Napi::Value WExprToNapi(Napi::Env env, const WExpr& e);
|
|
224
|
+
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// DrainToEvalResult — consume all packets for one cell, capturing Print[]
|
|
227
|
+
// output and messages. Blocks until RETURNPKT. Thread-pool thread only.
|
|
228
|
+
// opts may be nullptr (no streaming callbacks).
|
|
229
|
+
// ---------------------------------------------------------------------------
|
|
230
|
+
static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
231
|
+
EvalResult r;
|
|
232
|
+
|
|
233
|
+
// Parse "In[42]:=" → 42
|
|
234
|
+
auto parseCellIndex = [](const std::string& s) -> int64_t {
|
|
235
|
+
auto a = s.find('[');
|
|
236
|
+
auto b = s.find(']');
|
|
237
|
+
if (a == std::string::npos || b == std::string::npos) return 0;
|
|
238
|
+
try { return std::stoll(s.substr(a + 1, b - a - 1)); }
|
|
239
|
+
catch (...) { return 0; }
|
|
240
|
+
};
|
|
241
|
+
|
|
242
|
+
// Strip "Context`" prefix from fully-qualified symbol names.
|
|
243
|
+
// WSTP sends e.g. "System`MessageName" or "System`Power"; we want the bare name.
|
|
244
|
+
auto stripCtx = [](const std::string& s) -> std::string {
|
|
245
|
+
auto p = s.rfind('`');
|
|
246
|
+
return p != std::string::npos ? s.substr(p + 1) : s;
|
|
247
|
+
};
|
|
248
|
+
|
|
249
|
+
// Remove trailing newline(s) that Print[] appends to TextPacket content.
|
|
250
|
+
// WSTP delivers actual '\n' bytes on some platforms and the 4-char literal
|
|
251
|
+
// "\012" (backslash + '0' + '1' + '2') on others (Wolfram's octal escape).
|
|
252
|
+
auto rtrimNL = [](std::string s) -> std::string {
|
|
253
|
+
// Strip actual ASCII newlines / carriage returns
|
|
254
|
+
while (!s.empty() && (s.back() == '\n' || s.back() == '\r'))
|
|
255
|
+
s.pop_back();
|
|
256
|
+
// Strip Wolfram's 4-char literal octal escape "\\012"
|
|
257
|
+
while (s.size() >= 4 && s.compare(s.size() - 4, 4, "\\012") == 0)
|
|
258
|
+
s.resize(s.size() - 4);
|
|
259
|
+
return s;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
// -----------------------------------------------------------------------
|
|
263
|
+
// Helper: handle a MESSAGEPKT (already current) — shared by outer and
|
|
264
|
+
// dialog inner loops. Reads the follow-up TEXTPKT, extracts the message
|
|
265
|
+
// string, appends it to r.messages, and fires the onMessage TSFN.
|
|
266
|
+
// -----------------------------------------------------------------------
|
|
267
|
+
auto handleMessage = [&](bool forDialog) {
|
|
268
|
+
WSNewPacket(lp); // discard message-name expression
|
|
269
|
+
if (WSNextPacket(lp) == TEXTPKT) {
|
|
270
|
+
const char* s = nullptr; WSGetString(lp, &s);
|
|
271
|
+
if (s) {
|
|
272
|
+
std::string text = s;
|
|
273
|
+
WSReleaseString(lp, s);
|
|
274
|
+
static const std::string NL = "\\012";
|
|
275
|
+
std::string msg;
|
|
276
|
+
auto dc = text.find("::");
|
|
277
|
+
if (dc != std::string::npos) {
|
|
278
|
+
auto nl_before = text.rfind(NL, dc);
|
|
279
|
+
size_t start = (nl_before != std::string::npos) ? nl_before + 4 : 0;
|
|
280
|
+
auto nl_after = text.find(NL, dc);
|
|
281
|
+
size_t end = (nl_after != std::string::npos) ? nl_after : text.size();
|
|
282
|
+
while (start < end && text[start] == ' ') ++start;
|
|
283
|
+
msg = text.substr(start, end - start);
|
|
284
|
+
} else {
|
|
285
|
+
for (size_t i = 0; i < text.size(); ) {
|
|
286
|
+
if (text.compare(i, 4, NL) == 0) { msg += ' '; i += 4; }
|
|
287
|
+
else { msg += text[i++]; }
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
if (!forDialog) {
|
|
291
|
+
r.messages.push_back(msg);
|
|
292
|
+
if (opts && opts->hasOnMessage)
|
|
293
|
+
opts->onMessage.NonBlockingCall(
|
|
294
|
+
[msg](Napi::Env e, Napi::Function cb){
|
|
295
|
+
cb.Call({Napi::String::New(e, msg)}); });
|
|
296
|
+
} else {
|
|
297
|
+
// Dialog messages: still append to outer result so nothing is lost.
|
|
298
|
+
r.messages.push_back(msg);
|
|
299
|
+
if (opts && opts->hasOnMessage)
|
|
300
|
+
opts->onMessage.NonBlockingCall(
|
|
301
|
+
[msg](Napi::Env e, Napi::Function cb){
|
|
302
|
+
cb.Call({Napi::String::New(e, msg)}); });
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
WSNewPacket(lp);
|
|
307
|
+
};
|
|
308
|
+
|
|
309
|
+
// -----------------------------------------------------------------------
|
|
310
|
+
// Helper: service one pending DialogRequest from dialogQueue_.
|
|
311
|
+
//
|
|
312
|
+
// menuDlgProto = false (default, BEGINDLGPKT path):
|
|
313
|
+
// Sends EvaluatePacket[ToExpression[...]], drains until RETURNPKT.
|
|
314
|
+
//
|
|
315
|
+
// menuDlgProto = true (MENUPKT-dialog path, interrupt-triggered Dialog[]):
|
|
316
|
+
// Sends EnterExpressionPacket[ToExpression[...]], drains until RETURNEXPRPKT.
|
|
317
|
+
// The kernel uses MENUPKT as the dialog-prompt between evaluations.
|
|
318
|
+
//
|
|
319
|
+
// Returns true → dialog still open.
|
|
320
|
+
// Returns false → dialog closed (ENDDLGPKT or exitDialog via MENUPKT 'c').
|
|
321
|
+
// -----------------------------------------------------------------------
|
|
322
|
+
auto serviceDialogRequest = [&](bool menuDlgProto = false) -> bool {
|
|
323
|
+
DialogRequest req;
|
|
324
|
+
{
|
|
325
|
+
std::lock_guard<std::mutex> lk(*opts->dialogMutex);
|
|
326
|
+
if (opts->dialogQueue->empty()) return true;
|
|
327
|
+
req = std::move(opts->dialogQueue->front());
|
|
328
|
+
opts->dialogQueue->pop();
|
|
329
|
+
if (opts->dialogQueue->empty())
|
|
330
|
+
opts->dialogPending->store(false);
|
|
331
|
+
}
|
|
332
|
+
// Send the expression to the kernel's dialog REPL.
|
|
333
|
+
// menuDlgProto / EnterExpressionPacket — interrupt-triggered Dialog[] context
|
|
334
|
+
// EvaluatePacket — BEGINDLGPKT Dialog[] context
|
|
335
|
+
// EnterTextPacket — exitDialog (Return[] in interactive ctx)
|
|
336
|
+
bool sent;
|
|
337
|
+
if (!req.useEnterText) {
|
|
338
|
+
if (menuDlgProto) {
|
|
339
|
+
// MENUPKT-dialog (interrupt-triggered): text-mode I/O.
|
|
340
|
+
// The kernel expects EnterTextPacket and returns OutputForm via TEXTPKT.
|
|
341
|
+
sent = WSPutFunction(lp, "EnterTextPacket", 1) &&
|
|
342
|
+
WSPutUTF8String(lp, (const unsigned char *)req.expr.c_str(), (int)req.expr.size()) &&
|
|
343
|
+
WSEndPacket (lp) &&
|
|
344
|
+
WSFlush (lp);
|
|
345
|
+
} else {
|
|
346
|
+
// BEGINDLGPKT dialog: batch mode.
|
|
347
|
+
sent = WSPutFunction(lp, "EvaluatePacket", 1) &&
|
|
348
|
+
WSPutFunction(lp, "ToExpression", 1) &&
|
|
349
|
+
WSPutUTF8String(lp, (const unsigned char *)req.expr.c_str(), (int)req.expr.size()) &&
|
|
350
|
+
WSEndPacket (lp) &&
|
|
351
|
+
WSFlush (lp);
|
|
352
|
+
}
|
|
353
|
+
} else {
|
|
354
|
+
sent = WSPutFunction(lp, "EnterTextPacket", 1) &&
|
|
355
|
+
WSPutUTF8String(lp, (const unsigned char *)req.expr.c_str(), (int)req.expr.size()) &&
|
|
356
|
+
WSEndPacket (lp) &&
|
|
357
|
+
WSFlush (lp);
|
|
358
|
+
}
|
|
359
|
+
if (!sent) {
|
|
360
|
+
// Send failure: resolve with a WError.
|
|
361
|
+
WExpr err = WExpr::mkError("dialogEval: failed to send to kernel");
|
|
362
|
+
req.resolve.NonBlockingCall(
|
|
363
|
+
[err](Napi::Env e, Napi::Function cb){
|
|
364
|
+
Napi::Object o = Napi::Object::New(e);
|
|
365
|
+
o.Set("type", Napi::String::New(e, "error"));
|
|
366
|
+
o.Set("error", Napi::String::New(e, err.strVal));
|
|
367
|
+
cb.Call({o});
|
|
368
|
+
});
|
|
369
|
+
req.resolve.Release();
|
|
370
|
+
return true; // link might still work; let outer loop decide
|
|
371
|
+
}
|
|
372
|
+
// Read response packets until RETURNPKT/RETURNEXPRPKT or ENDDLGPKT.
|
|
373
|
+
WExpr result;
|
|
374
|
+
std::string lastDlgText; // accumulated OutputForm text for menuDlgProto dialogEval
|
|
375
|
+
bool dialogEndedHere = false;
|
|
376
|
+
bool menuDlgFirstSkipped = false; // whether the pre-result setup MENUPKT was already skipped
|
|
377
|
+
DiagLog("[SDR] waiting for response, menuDlgProto=" + std::to_string(menuDlgProto)
|
|
378
|
+
+ " useEnterText=" + std::to_string(req.useEnterText));
|
|
379
|
+
while (true) {
|
|
380
|
+
int p2 = WSNextPacket(lp);
|
|
381
|
+
DiagLog("[SDR] p2=" + std::to_string(p2));
|
|
382
|
+
if (p2 == RETURNPKT) {
|
|
383
|
+
result = ReadExprRaw(lp);
|
|
384
|
+
WSNewPacket(lp);
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
if (p2 == RETURNTEXTPKT) {
|
|
388
|
+
// ReturnTextPacket: the dialog/inspect-mode result as OutputForm text.
|
|
389
|
+
// This is how 'i' (inspect) mode returns results in Wolfram 3.
|
|
390
|
+
const char* s = nullptr; WSGetString(lp, &s);
|
|
391
|
+
std::string txt = s ? rtrimNL(s) : ""; if (s) WSReleaseString(lp, s);
|
|
392
|
+
WSNewPacket(lp);
|
|
393
|
+
DiagLog("[SDR] RETURNTEXTPKT text='" + txt.substr(0, 60) + "'");
|
|
394
|
+
if (txt.empty()) {
|
|
395
|
+
result = WExpr::mkSymbol("System`Null");
|
|
396
|
+
} else {
|
|
397
|
+
// Try integer
|
|
398
|
+
try {
|
|
399
|
+
size_t pos = 0;
|
|
400
|
+
long long iv = std::stoll(txt, &pos);
|
|
401
|
+
if (pos == txt.size()) { result.kind = WExpr::Integer; result.intVal = iv; }
|
|
402
|
+
else { throw std::exception(); }
|
|
403
|
+
} catch (...) {
|
|
404
|
+
// Try real
|
|
405
|
+
try {
|
|
406
|
+
size_t pos = 0;
|
|
407
|
+
double rv = std::stod(txt, &pos);
|
|
408
|
+
if (pos == txt.size()) { result.kind = WExpr::Real; result.realVal = rv; }
|
|
409
|
+
else { throw std::exception(); }
|
|
410
|
+
} catch (...) {
|
|
411
|
+
result.kind = WExpr::String; result.strVal = txt;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
}
|
|
415
|
+
break;
|
|
416
|
+
}
|
|
417
|
+
if (p2 == RETURNEXPRPKT) {
|
|
418
|
+
// EnterExpressionPacket path (menuDlgProto): collect structured result.
|
|
419
|
+
// The kernel will follow with INPUTNAMEPKT or MENUPKT (next prompt);
|
|
420
|
+
// we break after collecting and let the outer loop consume that.
|
|
421
|
+
result = ReadExprRaw(lp);
|
|
422
|
+
WSNewPacket(lp);
|
|
423
|
+
if (!menuDlgProto) break; // BEGINDLGPKT: also terminates
|
|
424
|
+
// menuDlgProto: keep looping to consume OUTPUTNAMEPKT/INPUTNAMEPKT
|
|
425
|
+
// and the trailing MENUPKT (which triggers the outer break below)
|
|
426
|
+
continue;
|
|
427
|
+
}
|
|
428
|
+
if (p2 == ENDDLGPKT) {
|
|
429
|
+
// The expression exited the dialog (e.g. Return[]).
|
|
430
|
+
// Handle ENDDLGPKT here so the outer inner-loop doesn't see it.
|
|
431
|
+
wsint64 endLevel = 0;
|
|
432
|
+
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &endLevel);
|
|
433
|
+
WSNewPacket(lp);
|
|
434
|
+
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
435
|
+
if (opts->hasOnDialogEnd)
|
|
436
|
+
opts->onDialogEnd.NonBlockingCall(
|
|
437
|
+
[endLevel](Napi::Env e, Napi::Function cb){
|
|
438
|
+
cb.Call({Napi::Number::New(e, static_cast<double>(endLevel))}); });
|
|
439
|
+
// Resolve with Null — the caller asked for Return[], gets Null back.
|
|
440
|
+
result.kind = WExpr::Symbol;
|
|
441
|
+
result.strVal = "Null";
|
|
442
|
+
dialogEndedHere = true;
|
|
443
|
+
break;
|
|
444
|
+
}
|
|
445
|
+
if (p2 == MESSAGEPKT) { handleMessage(true); continue; }
|
|
446
|
+
if (p2 == TEXTPKT) {
|
|
447
|
+
const char* s = nullptr; WSGetString(lp, &s);
|
|
448
|
+
if (s) {
|
|
449
|
+
std::string line = rtrimNL(s); WSReleaseString(lp, s);
|
|
450
|
+
if (menuDlgProto && !req.useEnterText) {
|
|
451
|
+
DiagLog("[SDR] TEXTPKT(menuDlg) text='" + line + "'");
|
|
452
|
+
if (!line.empty()) {
|
|
453
|
+
if (!lastDlgText.empty()) lastDlgText += "\n";
|
|
454
|
+
lastDlgText += line;
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
if (opts->hasOnDialogPrint)
|
|
458
|
+
opts->onDialogPrint.NonBlockingCall(
|
|
459
|
+
[line](Napi::Env e, Napi::Function cb){
|
|
460
|
+
cb.Call({Napi::String::New(e, line)}); });
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
WSNewPacket(lp); continue;
|
|
464
|
+
}
|
|
465
|
+
if (p2 == INPUTNAMEPKT || p2 == OUTPUTNAMEPKT) { WSNewPacket(lp); continue; }
|
|
466
|
+
if (p2 == MENUPKT) {
|
|
467
|
+
// MENUPKT in dialog context:
|
|
468
|
+
// * menuDlgProto && exitDialog: respond 'c' to resume main eval.
|
|
469
|
+
// * menuDlgProto && dialogEval: result arrived via TEXTPKT; parse it.
|
|
470
|
+
// * BEGINDLGPKT path: result via RETURNPKT (should already have it).
|
|
471
|
+
if (req.useEnterText) {
|
|
472
|
+
// exitDialog: respond bare string 'c' (continue) per JLink MENUPKT protocol.
|
|
473
|
+
wsint64 menuType2_ = 0; WSGetInteger64(lp, &menuType2_);
|
|
474
|
+
const char* menuPrompt2_ = nullptr; WSGetString(lp, &menuPrompt2_);
|
|
475
|
+
if (menuPrompt2_) WSReleaseString(lp, menuPrompt2_);
|
|
476
|
+
WSNewPacket(lp);
|
|
477
|
+
WSPutString(lp, "c");
|
|
478
|
+
WSEndPacket(lp);
|
|
479
|
+
WSFlush(lp);
|
|
480
|
+
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
481
|
+
if (opts->hasOnDialogEnd)
|
|
482
|
+
opts->onDialogEnd.NonBlockingCall(
|
|
483
|
+
[](Napi::Env e, Napi::Function cb){
|
|
484
|
+
cb.Call({Napi::Number::New(e, 0.0)}); });
|
|
485
|
+
result.kind = WExpr::Symbol;
|
|
486
|
+
result.strVal = "Null";
|
|
487
|
+
dialogEndedHere = true;
|
|
488
|
+
} else if (menuDlgProto) {
|
|
489
|
+
// dialogEval in text-mode dialog: result arrives as TEXTPKT before MENUPKT.
|
|
490
|
+
// However: the kernel may send a "setup" MENUPKT immediately after we
|
|
491
|
+
// send our expression (buffered from the dialog-open sequence), before
|
|
492
|
+
// sending the TEXTPKT result. Skip that one MENUPKT and keep waiting.
|
|
493
|
+
WSNewPacket(lp);
|
|
494
|
+
if (lastDlgText.empty() && !menuDlgFirstSkipped) {
|
|
495
|
+
menuDlgFirstSkipped = true;
|
|
496
|
+
DiagLog("[SDR] menuDlg: skipping pre-result MENUPKT, waiting for TEXTPKT");
|
|
497
|
+
continue; // keep waiting for TEXTPKT result
|
|
498
|
+
}
|
|
499
|
+
// Second MENUPKT (or first if we already have text): end of result.
|
|
500
|
+
DiagLog("[SDR] menuDlg: end-of-result MENUPKT, lastDlgText='" + lastDlgText + "'");
|
|
501
|
+
if (lastDlgText.empty()) {
|
|
502
|
+
result = WExpr::mkSymbol("System`Null");
|
|
503
|
+
} else {
|
|
504
|
+
// Try integer
|
|
505
|
+
try {
|
|
506
|
+
size_t pos = 0;
|
|
507
|
+
long long iv = std::stoll(lastDlgText, &pos);
|
|
508
|
+
if (pos == lastDlgText.size()) {
|
|
509
|
+
result.kind = WExpr::Integer;
|
|
510
|
+
result.intVal = iv;
|
|
511
|
+
} else { throw std::exception(); }
|
|
512
|
+
} catch (...) {
|
|
513
|
+
// Try real
|
|
514
|
+
try {
|
|
515
|
+
size_t pos = 0;
|
|
516
|
+
double rv = std::stod(lastDlgText, &pos);
|
|
517
|
+
if (pos == lastDlgText.size()) {
|
|
518
|
+
result.kind = WExpr::Real;
|
|
519
|
+
result.realVal = rv;
|
|
520
|
+
} else { throw std::exception(); }
|
|
521
|
+
} catch (...) {
|
|
522
|
+
result.kind = WExpr::String;
|
|
523
|
+
result.strVal = lastDlgText;
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
} else {
|
|
528
|
+
// BEGINDLGPKT path: result should have arrived via RETURNPKT.
|
|
529
|
+
WSNewPacket(lp);
|
|
530
|
+
if (result.kind == WExpr::WError)
|
|
531
|
+
result = WExpr::mkSymbol("System`Null");
|
|
532
|
+
}
|
|
533
|
+
break;
|
|
534
|
+
}
|
|
535
|
+
if (p2 == 0 || p2 == ILLEGALPKT) {
|
|
536
|
+
const char* m = WSErrorMessage(lp); WSClearError(lp);
|
|
537
|
+
result = WExpr::mkError(m ? m : "WSTP error in dialogEval");
|
|
538
|
+
break;
|
|
539
|
+
}
|
|
540
|
+
WSNewPacket(lp);
|
|
541
|
+
}
|
|
542
|
+
// Deliver result to the JS Promise via TSFN.
|
|
543
|
+
WExpr res = result;
|
|
544
|
+
req.resolve.NonBlockingCall(
|
|
545
|
+
[res](Napi::Env e, Napi::Function cb){ cb.Call({WExprToNapi(e, res)}); });
|
|
546
|
+
req.resolve.Release();
|
|
547
|
+
return !dialogEndedHere;
|
|
548
|
+
};
|
|
549
|
+
|
|
550
|
+
// -----------------------------------------------------------------------
|
|
551
|
+
// Outer drain loop — blocking WSNextPacket (unchanged for normal evals).
|
|
552
|
+
// Dialog packets trigger the inner loop below.
|
|
553
|
+
// -----------------------------------------------------------------------
|
|
554
|
+
// In EnterExpressionPacket mode the kernel sends:
|
|
555
|
+
// Non-Null result: OUTPUTNAMEPKT → RETURNEXPRPKT
|
|
556
|
+
// followed by a trailing INPUTNAMEPKT (next prompt)
|
|
557
|
+
// Null result: INPUTNAMEPKT only (no OUTPUTNAMEPKT/RETURNEXPRPKT)
|
|
558
|
+
//
|
|
559
|
+
// So: INPUTNAMEPKT as the FIRST packet (before any OUTPUTNAMEPKT) = Null.
|
|
560
|
+
// INPUTNAMEPKT after RETURNEXPRPKT = next-prompt trailer, consumed here
|
|
561
|
+
// so the next evaluate() starts clean.
|
|
562
|
+
bool gotOutputName = false;
|
|
563
|
+
bool gotResult = false;
|
|
564
|
+
while (true) {
|
|
565
|
+
int pkt = WSNextPacket(lp);
|
|
566
|
+
DiagLog("[Eval] pkt=" + std::to_string(pkt));
|
|
567
|
+
|
|
568
|
+
if (pkt == RETURNPKT || pkt == RETURNEXPRPKT) {
|
|
569
|
+
// RETURNPKT: response to EvaluatePacket — no In/Out populated.
|
|
570
|
+
// RETURNEXPRPKT: response to EnterExpressionPacket — main loop ran,
|
|
571
|
+
// In[n] and Out[n] are populated by the kernel.
|
|
572
|
+
//
|
|
573
|
+
// Safety for interactive RETURNEXPRPKT: peek at the token type before
|
|
574
|
+
// calling ReadExprRaw. Atomic results (symbol, int, real, string) are
|
|
575
|
+
// safe to read — this covers $Aborted and simple values.
|
|
576
|
+
// Complex results (List, Graphics, …) are skipped with WSNewPacket to
|
|
577
|
+
// avoid a potential WSGetFunction crash on deep expression trees.
|
|
578
|
+
// Out[n] is already set inside the kernel; JS renders via VsCodeRenderNth
|
|
579
|
+
// which reads from $vsCodeLastResultList, not from the transferred result.
|
|
580
|
+
if (opts && opts->interactive && pkt == RETURNEXPRPKT) {
|
|
581
|
+
int tok = WSGetType(lp);
|
|
582
|
+
if (tok == WSTKSYM || tok == WSTKINT || tok == WSTKREAL || tok == WSTKSTR) {
|
|
583
|
+
r.result = ReadExprRaw(lp);
|
|
584
|
+
} else {
|
|
585
|
+
// Complex result — discard; Out[n] intact in kernel.
|
|
586
|
+
r.result = WExpr::mkSymbol("System`__VsCodeHasResult__");
|
|
587
|
+
}
|
|
588
|
+
} else {
|
|
589
|
+
r.result = ReadExprRaw(lp);
|
|
590
|
+
}
|
|
591
|
+
WSNewPacket(lp);
|
|
592
|
+
if (r.result.kind == WExpr::Symbol && stripCtx(r.result.strVal) == "$Aborted")
|
|
593
|
+
r.aborted = true;
|
|
594
|
+
gotResult = true;
|
|
595
|
+
// In interactive mode the kernel always follows RETURNEXPRPKT with a
|
|
596
|
+
// trailing INPUTNAMEPKT (the next "In[n+1]:=" prompt). We must consume
|
|
597
|
+
// that packet before returning so the wire is clean for the next eval.
|
|
598
|
+
// Continue the loop; the INPUTNAMEPKT branch below will break cleanly.
|
|
599
|
+
if (!opts || !opts->interactive) break;
|
|
600
|
+
// (fall through to the next WSNextPacket iteration)
|
|
601
|
+
}
|
|
602
|
+
else if (pkt == INPUTNAMEPKT) {
|
|
603
|
+
const char* s = nullptr; WSGetString(lp, &s);
|
|
604
|
+
std::string nameStr = s ? s : "";
|
|
605
|
+
if (s) WSReleaseString(lp, s);
|
|
606
|
+
WSNewPacket(lp);
|
|
607
|
+
if (opts && opts->interactive) {
|
|
608
|
+
if (!gotOutputName && !gotResult) {
|
|
609
|
+
// First packet = INPUTNAMEPKT with no preceding OUTPUTNAMEPKT:
|
|
610
|
+
// kernel evaluated to Null/suppressed — this IS the result signal.
|
|
611
|
+
// cellIndex is one less than this prompt's index.
|
|
612
|
+
int64_t nextIdx = parseCellIndex(nameStr);
|
|
613
|
+
r.cellIndex = (nextIdx > 1) ? nextIdx - 1 : nextIdx;
|
|
614
|
+
r.result = WExpr::mkSymbol("System`Null");
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
// Trailing INPUTNAMEPKT after RETURNEXPRPKT — consume and exit.
|
|
618
|
+
// (Next eval starts with no leftover INPUTNAMEPKT on the wire.)
|
|
619
|
+
break;
|
|
620
|
+
}
|
|
621
|
+
r.cellIndex = parseCellIndex(nameStr);
|
|
622
|
+
}
|
|
623
|
+
else if (pkt == OUTPUTNAMEPKT) {
|
|
624
|
+
const char* s = nullptr; WSGetString(lp, &s);
|
|
625
|
+
if (s) {
|
|
626
|
+
std::string name = s; WSReleaseString(lp, s);
|
|
627
|
+
// Kernel sends "Out[n]= " with trailing space — normalize to "Out[n]=".
|
|
628
|
+
while (!name.empty() && name.back() == ' ') name.pop_back();
|
|
629
|
+
r.outputName = name;
|
|
630
|
+
// Parse the n from "Out[n]=" to set cellIndex (interactive mode).
|
|
631
|
+
r.cellIndex = parseCellIndex(name);
|
|
632
|
+
}
|
|
633
|
+
WSNewPacket(lp);
|
|
634
|
+
gotOutputName = true;
|
|
635
|
+
}
|
|
636
|
+
else if (pkt == TEXTPKT) {
|
|
637
|
+
const char* s = nullptr; WSGetString(lp, &s);
|
|
638
|
+
if (s) {
|
|
639
|
+
std::string line = rtrimNL(s);
|
|
640
|
+
WSReleaseString(lp, s);
|
|
641
|
+
DiagLog("[Eval] TEXTPKT content='" + line.substr(0, 60) + "'");
|
|
642
|
+
r.print.emplace_back(line);
|
|
643
|
+
if (opts && opts->hasOnPrint) {
|
|
644
|
+
// Log C++ dispatch time so the JS handler timestamp gives latency.
|
|
645
|
+
DiagLog("[TSFN][onPrint] dispatch +" + std::to_string(diagMs())
|
|
646
|
+
+ "ms \"" + line.substr(0, 30) + "\"");
|
|
647
|
+
opts->onPrint.NonBlockingCall(
|
|
648
|
+
[line](Napi::Env env, Napi::Function cb){
|
|
649
|
+
cb.Call({ Napi::String::New(env, line) }); });
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
WSNewPacket(lp);
|
|
653
|
+
}
|
|
654
|
+
else if (pkt == MESSAGEPKT) {
|
|
655
|
+
handleMessage(false);
|
|
656
|
+
}
|
|
657
|
+
else if (pkt == BEGINDLGPKT) {
|
|
658
|
+
// ----------------------------------------------------------------
|
|
659
|
+
// Dialog subsession opened by the kernel.
|
|
660
|
+
// Read the dialog level integer, set dialogOpen_ flag, fire callback.
|
|
661
|
+
// ----------------------------------------------------------------
|
|
662
|
+
wsint64 level = 0;
|
|
663
|
+
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &level);
|
|
664
|
+
WSNewPacket(lp);
|
|
665
|
+
|
|
666
|
+
if (opts && opts->dialogOpen)
|
|
667
|
+
opts->dialogOpen->store(true);
|
|
668
|
+
if (opts && opts->hasOnDialogBegin)
|
|
669
|
+
opts->onDialogBegin.NonBlockingCall(
|
|
670
|
+
[level](Napi::Env e, Napi::Function cb){
|
|
671
|
+
cb.Call({Napi::Number::New(e, static_cast<double>(level))}); });
|
|
672
|
+
|
|
673
|
+
// ----------------------------------------------------------------
|
|
674
|
+
// Dialog inner loop — WSReady-gated so dialogQueue_ can be serviced
|
|
675
|
+
// between kernel packets without blocking indefinitely.
|
|
676
|
+
// ----------------------------------------------------------------
|
|
677
|
+
bool dialogDone = false;
|
|
678
|
+
while (!dialogDone) {
|
|
679
|
+
// Abort check — abort() sets abortFlag_ and sends WSAbortMessage.
|
|
680
|
+
// The kernel may be slow to respond (or send RETURNPKT[$Aborted]
|
|
681
|
+
// without a preceding ENDDLGPKT); bail out proactively to avoid
|
|
682
|
+
// spinning forever.
|
|
683
|
+
if (opts && opts->abortFlag && opts->abortFlag->load()) {
|
|
684
|
+
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
685
|
+
r.result = WExpr::mkSymbol("System`$Aborted");
|
|
686
|
+
r.aborted = true;
|
|
687
|
+
return r;
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// Service any pending dialogEval() requests first.
|
|
691
|
+
if (opts && opts->dialogPending && opts->dialogPending->load()) {
|
|
692
|
+
if (!serviceDialogRequest()) {
|
|
693
|
+
dialogDone = true; // ENDDLGPKT arrived inside the request
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Non-blocking check: is the kernel ready to send a packet?
|
|
699
|
+
if (!WSReady(lp)) {
|
|
700
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
701
|
+
continue;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
int dpkt = WSNextPacket(lp);
|
|
705
|
+
DiagLog("[Dialog] dpkt=" + std::to_string(dpkt));
|
|
706
|
+
if (dpkt == ENDDLGPKT) {
|
|
707
|
+
wsint64 endLevel = 0;
|
|
708
|
+
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &endLevel);
|
|
709
|
+
WSNewPacket(lp);
|
|
710
|
+
if (opts && opts->dialogOpen)
|
|
711
|
+
opts->dialogOpen->store(false);
|
|
712
|
+
if (opts && opts->hasOnDialogEnd)
|
|
713
|
+
opts->onDialogEnd.NonBlockingCall(
|
|
714
|
+
[endLevel](Napi::Env e, Napi::Function cb){
|
|
715
|
+
cb.Call({Napi::Number::New(e, static_cast<double>(endLevel))}); });
|
|
716
|
+
dialogDone = true;
|
|
717
|
+
}
|
|
718
|
+
else if (dpkt == RETURNPKT) {
|
|
719
|
+
// Could be the abort response (RETURNPKT[$Aborted] without a
|
|
720
|
+
// preceding ENDDLGPKT) or an unsolicited in-dialog result.
|
|
721
|
+
WExpr innerExpr = ReadExprRaw(lp);
|
|
722
|
+
WSNewPacket(lp);
|
|
723
|
+
if (innerExpr.kind == WExpr::Symbol &&
|
|
724
|
+
stripCtx(innerExpr.strVal) == "$Aborted") {
|
|
725
|
+
// Kernel ended the dialog due to abort — propagate upward.
|
|
726
|
+
if (opts && opts->dialogOpen) opts->dialogOpen->store(false);
|
|
727
|
+
r.result = innerExpr; // already the $Aborted symbol
|
|
728
|
+
r.aborted = true;
|
|
729
|
+
return r;
|
|
730
|
+
}
|
|
731
|
+
// Otherwise discard — outer loop will see final RETURNPKT.
|
|
732
|
+
}
|
|
733
|
+
else if (dpkt == INPUTNAMEPKT || dpkt == OUTPUTNAMEPKT) {
|
|
734
|
+
WSNewPacket(lp); // dialog prompts — discard
|
|
735
|
+
}
|
|
736
|
+
else if (dpkt == TEXTPKT) {
|
|
737
|
+
DiagLog("[Dialog] TEXTPKT");
|
|
738
|
+
const char* s = nullptr; WSGetString(lp, &s);
|
|
739
|
+
if (s) {
|
|
740
|
+
std::string line = rtrimNL(s); WSReleaseString(lp, s);
|
|
741
|
+
if (opts && opts->hasOnDialogPrint)
|
|
742
|
+
opts->onDialogPrint.NonBlockingCall(
|
|
743
|
+
[line](Napi::Env e, Napi::Function cb){
|
|
744
|
+
cb.Call({Napi::String::New(e, line)}); });
|
|
745
|
+
}
|
|
746
|
+
WSNewPacket(lp);
|
|
747
|
+
}
|
|
748
|
+
else if (dpkt == MESSAGEPKT) {
|
|
749
|
+
handleMessage(true);
|
|
750
|
+
}
|
|
751
|
+
else if (dpkt == 0 || dpkt == ILLEGALPKT) {
|
|
752
|
+
const char* m = WSErrorMessage(lp);
|
|
753
|
+
WSClearError(lp);
|
|
754
|
+
if (opts && opts->dialogOpen) opts->dialogOpen->store(false);
|
|
755
|
+
// If abort() was in flight, the ILLEGALPKT is the expected
|
|
756
|
+
// link-reset response — treat as clean abort, not an error.
|
|
757
|
+
if (opts && opts->abortFlag && opts->abortFlag->load()) {
|
|
758
|
+
r.result = WExpr::mkSymbol("System`$Aborted");
|
|
759
|
+
r.aborted = true;
|
|
760
|
+
} else {
|
|
761
|
+
r.result = WExpr::mkError(m ? m : "WSTP error in dialog");
|
|
762
|
+
}
|
|
763
|
+
return r; // unrecoverable — bail out entirely
|
|
764
|
+
}
|
|
765
|
+
else {
|
|
766
|
+
WSNewPacket(lp);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
// Dialog closed — continue outer loop waiting for the original RETURNPKT.
|
|
770
|
+
}
|
|
771
|
+
else if (pkt == 0 || pkt == ILLEGALPKT) {
|
|
772
|
+
const char* m = WSErrorMessage(lp);
|
|
773
|
+
std::string s = m ? m : "WSTP link error";
|
|
774
|
+
WSClearError(lp);
|
|
775
|
+
r.result = WExpr::mkError(s);
|
|
776
|
+
break;
|
|
777
|
+
}
|
|
778
|
+
else if (pkt == RETURNTEXTPKT) {
|
|
779
|
+
// ReturnTextPacket carries the string-form of the result.
|
|
780
|
+
// This should not normally appear with EvaluatePacket, but handle
|
|
781
|
+
// it defensively so the loop always terminates.
|
|
782
|
+
DiagLog("[Eval] unexpected RETURNTEXTPKT — treating as empty return");
|
|
783
|
+
WSNewPacket(lp);
|
|
784
|
+
// Leave r.result as default WError so the caller gets an informative error.
|
|
785
|
+
r.result = WExpr::mkError("unexpected ReturnTextPacket from kernel");
|
|
786
|
+
break;
|
|
787
|
+
}
|
|
788
|
+
else if (pkt == MENUPKT) {
|
|
789
|
+
// ----------------------------------------------------------------
|
|
790
|
+
// MENUPKT (6) — interrupt menu protocol per JLink source:
|
|
791
|
+
//
|
|
792
|
+
// Protocol (from JLink InterruptDialog.java):
|
|
793
|
+
// 1. WSNextPacket(lp) → MENUPKT
|
|
794
|
+
// 2. WSGetInteger(lp, &type) — menu type (1=interrupt, 3=LinkRead)
|
|
795
|
+
// 3. WSGetString(lp, &prompt) — prompt string
|
|
796
|
+
// 4. WSNewPacket(lp)
|
|
797
|
+
// 5. WSPutString(lp, "i") — respond with bare string
|
|
798
|
+
// Options: a=abort, c=continue, i=inspect/dialog
|
|
799
|
+
// 6. WSEndPacket; WSFlush
|
|
800
|
+
//
|
|
801
|
+
// After "i", the kernel enters interactive inspect mode and sends
|
|
802
|
+
// MENUPKT(inspect) as the dialog prompt → handled in menuDlgDone.
|
|
803
|
+
// ----------------------------------------------------------------
|
|
804
|
+
{
|
|
805
|
+
wsint64 menuType_ = 0; WSGetInteger64(lp, &menuType_);
|
|
806
|
+
const char* menuPrompt_ = nullptr; WSGetString(lp, &menuPrompt_);
|
|
807
|
+
if (menuPrompt_) WSReleaseString(lp, menuPrompt_);
|
|
808
|
+
WSNewPacket(lp);
|
|
809
|
+
DiagLog("[Eval] MENUPKT type=" + std::to_string(menuType_) + " — responding 'i' (inspect)");
|
|
810
|
+
|
|
811
|
+
// Respond with bare string 'i' to enter inspect/dialog mode.
|
|
812
|
+
// Per JLink protocol: WSPutString(link, "i"). Not wrapped in any packet.
|
|
813
|
+
WSPutString(lp, "i");
|
|
814
|
+
WSEndPacket(lp);
|
|
815
|
+
WSFlush(lp);
|
|
816
|
+
}
|
|
817
|
+
// The kernel will now send an inspect-mode MENUPKT as the dialog prompt.
|
|
818
|
+
bool menuDlgDone = false;
|
|
819
|
+
while (!menuDlgDone) {
|
|
820
|
+
if (opts && opts->abortFlag && opts->abortFlag->load()) {
|
|
821
|
+
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
822
|
+
r.result = WExpr::mkSymbol("System`$Aborted");
|
|
823
|
+
r.aborted = true;
|
|
824
|
+
return r;
|
|
825
|
+
}
|
|
826
|
+
if (opts && opts->dialogPending && opts->dialogPending->load()) {
|
|
827
|
+
if (!serviceDialogRequest(/*menuDlgProto=*/true)) {
|
|
828
|
+
menuDlgDone = true;
|
|
829
|
+
continue;
|
|
830
|
+
}
|
|
831
|
+
}
|
|
832
|
+
if (!WSReady(lp)) {
|
|
833
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(2));
|
|
834
|
+
continue;
|
|
835
|
+
}
|
|
836
|
+
int dpkt = WSNextPacket(lp);
|
|
837
|
+
DiagLog("[MenuDlg] dpkt=" + std::to_string(dpkt));
|
|
838
|
+
if (dpkt == ENDDLGPKT) {
|
|
839
|
+
wsint64 el = 0;
|
|
840
|
+
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &el);
|
|
841
|
+
WSNewPacket(lp);
|
|
842
|
+
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
843
|
+
if (opts->hasOnDialogEnd)
|
|
844
|
+
opts->onDialogEnd.NonBlockingCall(
|
|
845
|
+
[el](Napi::Env e, Napi::Function cb){
|
|
846
|
+
cb.Call({Napi::Number::New(e, static_cast<double>(el))}); });
|
|
847
|
+
menuDlgDone = true;
|
|
848
|
+
} else if (dpkt == BEGINDLGPKT) {
|
|
849
|
+
// BEGINDLGPKT: dialog subsession started.
|
|
850
|
+
// This arrives after 'i' response to MENUPKT, before INPUTNAMEPKT.
|
|
851
|
+
// The dialog level integer follows.
|
|
852
|
+
wsint64 beginLevel = 0;
|
|
853
|
+
if (WSGetType(lp) == WSTKINT) WSGetInteger64(lp, &beginLevel);
|
|
854
|
+
WSNewPacket(lp);
|
|
855
|
+
DiagLog("[MenuDlg] BEGINDLGPKT level=" + std::to_string(beginLevel) + " — dialog is open");
|
|
856
|
+
// isDialogOpen will be set when INPUTNAMEPKT arrives (see below).
|
|
857
|
+
// Just consume and wait for INPUTNAMEPKT.
|
|
858
|
+
} else if (dpkt == RETURNPKT) {
|
|
859
|
+
WExpr inner = ReadExprRaw(lp); WSNewPacket(lp);
|
|
860
|
+
if (inner.kind == WExpr::Symbol &&
|
|
861
|
+
stripCtx(inner.strVal) == "$Aborted") {
|
|
862
|
+
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
863
|
+
r.result = inner; r.aborted = true; return r;
|
|
864
|
+
}
|
|
865
|
+
} else if (dpkt == MENUPKT) {
|
|
866
|
+
// In the menuDlgDone loop, MENUPKTs serve two purposes:
|
|
867
|
+
// 1. FIRST MENUPKT (isDialogOpen still false):
|
|
868
|
+
// The dialog subsession has just opened.
|
|
869
|
+
// Set isDialogOpen = true, fire onDialogBegin, consume & wait.
|
|
870
|
+
// 2. LATER MENUPKTs (isDialogOpen true):
|
|
871
|
+
// Either the interrupt menu again (if interrupt handler also opened
|
|
872
|
+
// Dialog[], these menus pile up) — respond "c" to dismiss it.
|
|
873
|
+
// Or if exitDialog sent 'c' and cleared dialogOpen → we're done.
|
|
874
|
+
bool isOpen = opts->dialogOpen && opts->dialogOpen->load();
|
|
875
|
+
if (!isOpen) {
|
|
876
|
+
// First MENUPKT in menuDlgDone: the inspect/dialog-mode prompt.
|
|
877
|
+
// The kernel has entered interactive inspection mode.
|
|
878
|
+
// Read the type and prompt following JLink MENUPKT protocol.
|
|
879
|
+
wsint64 menuTypeInsp = 0; WSGetInteger64(lp, &menuTypeInsp);
|
|
880
|
+
const char* menuPromptInsp = nullptr; WSGetString(lp, &menuPromptInsp);
|
|
881
|
+
if (menuPromptInsp) WSReleaseString(lp, menuPromptInsp);
|
|
882
|
+
WSNewPacket(lp);
|
|
883
|
+
DiagLog("[MenuDlg] inspect MENUPKT type=" + std::to_string(menuTypeInsp) + " — isDialogOpen=true");
|
|
884
|
+
if (opts->dialogOpen) opts->dialogOpen->store(true);
|
|
885
|
+
if (opts->hasOnDialogBegin)
|
|
886
|
+
opts->onDialogBegin.NonBlockingCall(
|
|
887
|
+
[](Napi::Env e, Napi::Function cb){
|
|
888
|
+
cb.Call({Napi::Number::New(e, 1.0)}); });
|
|
889
|
+
} else if (opts->dialogOpen && !opts->dialogOpen->load()) {
|
|
890
|
+
// exitDialog already closed the dialog (cleared by serviceDialogRequest)
|
|
891
|
+
WSNewPacket(lp);
|
|
892
|
+
menuDlgDone = true;
|
|
893
|
+
} else {
|
|
894
|
+
// Subsequent MENUPKT while dialog is open — this is likely the
|
|
895
|
+
// interrupt-level menu that appeared AFTER the dialog-open MENUPKT.
|
|
896
|
+
// Dismiss it with bare string 'c' (continue) per JLink MENUPKT protocol.
|
|
897
|
+
DiagLog("[MenuDlg] subsequent MENUPKT while dialog open — responding 'c' (continue)");
|
|
898
|
+
wsint64 menuTypeDis = 0; WSGetInteger64(lp, &menuTypeDis);
|
|
899
|
+
const char* menuPromptDis = nullptr; WSGetString(lp, &menuPromptDis);
|
|
900
|
+
if (menuPromptDis) WSReleaseString(lp, menuPromptDis);
|
|
901
|
+
WSNewPacket(lp);
|
|
902
|
+
WSPutString(lp, "c");
|
|
903
|
+
WSEndPacket(lp);
|
|
904
|
+
WSFlush(lp);
|
|
905
|
+
}
|
|
906
|
+
} else if (dpkt == INPUTNAMEPKT || dpkt == OUTPUTNAMEPKT) {
|
|
907
|
+
const char* nm = nullptr; WSGetString(lp, &nm);
|
|
908
|
+
if (nm) {
|
|
909
|
+
std::string nml = nm; WSReleaseString(lp, nm);
|
|
910
|
+
DiagLog("[MenuDlg] pkt=" + std::to_string(dpkt) + " name='" + nml + "'");
|
|
911
|
+
// INPUTNAMEPKT in menuDlgDone may mean the dialog/inspect
|
|
912
|
+
// subsession is prompting for input (alternative to MENUPKT).
|
|
913
|
+
if (dpkt == INPUTNAMEPKT && opts && opts->dialogOpen &&
|
|
914
|
+
!opts->dialogOpen->load()) {
|
|
915
|
+
DiagLog("[MenuDlg] INPUTNAMEPKT while !dialogOpen — dialog opened via INPUTNAMEPKT");
|
|
916
|
+
opts->dialogOpen->store(true);
|
|
917
|
+
if (opts->hasOnDialogBegin)
|
|
918
|
+
opts->onDialogBegin.NonBlockingCall(
|
|
919
|
+
[](Napi::Env e, Napi::Function cb){
|
|
920
|
+
cb.Call({Napi::Number::New(e, 1.0)}); });
|
|
921
|
+
}
|
|
922
|
+
}
|
|
923
|
+
WSNewPacket(lp);
|
|
924
|
+
} else if (dpkt == TEXTPKT) {
|
|
925
|
+
const char* ts = nullptr; WSGetString(lp, &ts);
|
|
926
|
+
if (ts) {
|
|
927
|
+
std::string tl = rtrimNL(ts); WSReleaseString(lp, ts);
|
|
928
|
+
DiagLog("[MenuDlg] TEXTPKT text='" + tl.substr(0, 80) + "'");
|
|
929
|
+
// Filter out interrupt menu options text (informational only).
|
|
930
|
+
bool isMenuOptions = (tl.find("Your options are") != std::string::npos);
|
|
931
|
+
if (!isMenuOptions && opts && opts->hasOnDialogPrint)
|
|
932
|
+
opts->onDialogPrint.NonBlockingCall(
|
|
933
|
+
[tl](Napi::Env e, Napi::Function cb){
|
|
934
|
+
cb.Call({Napi::String::New(e, tl)}); });
|
|
935
|
+
}
|
|
936
|
+
WSNewPacket(lp);
|
|
937
|
+
} else if (dpkt == MESSAGEPKT) {
|
|
938
|
+
handleMessage(true);
|
|
939
|
+
} else if (dpkt == 0 || dpkt == ILLEGALPKT) {
|
|
940
|
+
const char* em = WSErrorMessage(lp); WSClearError(lp);
|
|
941
|
+
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
942
|
+
r.result = WExpr::mkError(em ? em : "WSTP link error in dialog");
|
|
943
|
+
return r;
|
|
944
|
+
} else {
|
|
945
|
+
WSNewPacket(lp);
|
|
946
|
+
}
|
|
947
|
+
}
|
|
948
|
+
// Dialog closed — outer loop continues to collect main eval result.
|
|
949
|
+
}
|
|
950
|
+
else {
|
|
951
|
+
DiagLog("[Eval] unknown pkt=" + std::to_string(pkt) + ", discarding");
|
|
952
|
+
WSNewPacket(lp); // discard unknown packets
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return r;
|
|
957
|
+
}
|
|
958
|
+
|
|
959
|
+
// ---------------------------------------------------------------------------
|
|
960
|
+
// ---------------------------------------------------------------------------
|
|
961
|
+
// WExprToNapi — convert WExpr → Napi::Value. Main thread only.
|
|
962
|
+
// ---------------------------------------------------------------------------
|
|
963
|
+
static Napi::Value WExprToNapi(Napi::Env env, const WExpr& e) {
|
|
964
|
+
switch (e.kind) {
|
|
965
|
+
case WExpr::Integer: {
|
|
966
|
+
Napi::Object o = Napi::Object::New(env);
|
|
967
|
+
o.Set("type", Napi::String::New(env, "integer"));
|
|
968
|
+
o.Set("value", Napi::Number::New(env, static_cast<double>(e.intVal)));
|
|
969
|
+
return o;
|
|
970
|
+
}
|
|
971
|
+
case WExpr::Real: {
|
|
972
|
+
Napi::Object o = Napi::Object::New(env);
|
|
973
|
+
o.Set("type", Napi::String::New(env, "real"));
|
|
974
|
+
o.Set("value", Napi::Number::New(env, e.realVal));
|
|
975
|
+
return o;
|
|
976
|
+
}
|
|
977
|
+
case WExpr::String: {
|
|
978
|
+
Napi::Object o = Napi::Object::New(env);
|
|
979
|
+
o.Set("type", Napi::String::New(env, "string"));
|
|
980
|
+
o.Set("value", Napi::String::New(env, e.strVal));
|
|
981
|
+
return o;
|
|
982
|
+
}
|
|
983
|
+
case WExpr::Symbol: {
|
|
984
|
+
Napi::Object o = Napi::Object::New(env);
|
|
985
|
+
o.Set("type", Napi::String::New(env, "symbol"));
|
|
986
|
+
o.Set("value", Napi::String::New(env, e.strVal));
|
|
987
|
+
return o;
|
|
988
|
+
}
|
|
989
|
+
case WExpr::Function: {
|
|
990
|
+
Napi::Array argsArr = Napi::Array::New(env, e.args.size());
|
|
991
|
+
for (size_t i = 0; i < e.args.size(); ++i)
|
|
992
|
+
argsArr.Set(static_cast<uint32_t>(i), WExprToNapi(env, e.args[i]));
|
|
993
|
+
Napi::Object o = Napi::Object::New(env);
|
|
994
|
+
o.Set("type", Napi::String::New(env, "function"));
|
|
995
|
+
o.Set("head", Napi::String::New(env, e.head));
|
|
996
|
+
o.Set("args", argsArr);
|
|
997
|
+
return o;
|
|
998
|
+
}
|
|
999
|
+
case WExpr::WError:
|
|
1000
|
+
default:
|
|
1001
|
+
Napi::Error::New(env, e.strVal).ThrowAsJavaScriptException();
|
|
1002
|
+
return env.Undefined();
|
|
1003
|
+
}
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
// ---------------------------------------------------------------------------
|
|
1007
|
+
// EvalResultToNapi — convert EvalResult → Napi::Object. Main thread only.
|
|
1008
|
+
// ---------------------------------------------------------------------------
|
|
1009
|
+
static Napi::Value EvalResultToNapi(Napi::Env env, const EvalResult& r) {
|
|
1010
|
+
auto obj = Napi::Object::New(env);
|
|
1011
|
+
|
|
1012
|
+
obj.Set("cellIndex", Napi::Number::New(env, static_cast<double>(r.cellIndex)));
|
|
1013
|
+
obj.Set("outputName", Napi::String::New(env, r.outputName));
|
|
1014
|
+
obj.Set("result", WExprToNapi(env, r.result));
|
|
1015
|
+
obj.Set("aborted", Napi::Boolean::New(env, r.aborted));
|
|
1016
|
+
|
|
1017
|
+
auto print = Napi::Array::New(env, r.print.size());
|
|
1018
|
+
for (size_t i = 0; i < r.print.size(); ++i)
|
|
1019
|
+
print.Set(static_cast<uint32_t>(i), Napi::String::New(env, r.print[i]));
|
|
1020
|
+
obj.Set("print", print);
|
|
1021
|
+
|
|
1022
|
+
auto msgs = Napi::Array::New(env, r.messages.size());
|
|
1023
|
+
for (size_t i = 0; i < r.messages.size(); ++i)
|
|
1024
|
+
msgs.Set(static_cast<uint32_t>(i), Napi::String::New(env, r.messages[i]));
|
|
1025
|
+
obj.Set("messages", msgs);
|
|
1026
|
+
|
|
1027
|
+
return obj;
|
|
1028
|
+
}
|
|
1029
|
+
|
|
1030
|
+
// ===========================================================================
|
|
1031
|
+
// EvaluateWorker — ALL blocking WSTP I/O runs on the libuv thread pool.
|
|
1032
|
+
// ===========================================================================
|
|
1033
|
+
class EvaluateWorker : public Napi::AsyncWorker {
|
|
1034
|
+
public:
|
|
1035
|
+
EvaluateWorker(Napi::Promise::Deferred deferred,
|
|
1036
|
+
WSLINK lp,
|
|
1037
|
+
std::string expr,
|
|
1038
|
+
EvalOptions opts,
|
|
1039
|
+
std::atomic<bool>& abortFlag,
|
|
1040
|
+
std::function<void()> completionCb,
|
|
1041
|
+
int64_t cellIndex,
|
|
1042
|
+
bool interactive = false)
|
|
1043
|
+
: Napi::AsyncWorker(deferred.Env()),
|
|
1044
|
+
deferred_(std::move(deferred)),
|
|
1045
|
+
lp_(lp),
|
|
1046
|
+
expr_(std::move(expr)),
|
|
1047
|
+
interactive_(interactive),
|
|
1048
|
+
opts_(std::move(opts)),
|
|
1049
|
+
abortFlag_(abortFlag),
|
|
1050
|
+
completionCb_(std::move(completionCb)),
|
|
1051
|
+
cellIndex_(cellIndex)
|
|
1052
|
+
{}
|
|
1053
|
+
|
|
1054
|
+
// ---- thread-pool thread: send packet; block until response ----
|
|
1055
|
+
void Execute() override {
|
|
1056
|
+
bool sent;
|
|
1057
|
+
if (!interactive_) {
|
|
1058
|
+
// EvaluatePacket + ToExpression: non-interactive, does NOT populate In[n]/Out[n].
|
|
1059
|
+
sent = WSPutFunction(lp_, "EvaluatePacket", 1) &&
|
|
1060
|
+
WSPutFunction(lp_, "ToExpression", 1) &&
|
|
1061
|
+
WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) &&
|
|
1062
|
+
WSEndPacket (lp_) &&
|
|
1063
|
+
WSFlush (lp_);
|
|
1064
|
+
} else {
|
|
1065
|
+
// EnterExpressionPacket[ToExpression[str]]: goes through the kernel's
|
|
1066
|
+
// full main loop — populates In[n] and Out[n] automatically, exactly
|
|
1067
|
+
// as the real Mathematica frontend does. Responds with RETURNEXPRPKT.
|
|
1068
|
+
sent = WSPutFunction(lp_, "EnterExpressionPacket", 1) &&
|
|
1069
|
+
WSPutFunction(lp_, "ToExpression", 1) &&
|
|
1070
|
+
WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) &&
|
|
1071
|
+
WSEndPacket (lp_) &&
|
|
1072
|
+
WSFlush (lp_);
|
|
1073
|
+
}
|
|
1074
|
+
if (!sent) {
|
|
1075
|
+
SetError("Failed to send packet to kernel");
|
|
1076
|
+
} else {
|
|
1077
|
+
opts_.interactive = interactive_;
|
|
1078
|
+
result_ = DrainToEvalResult(lp_, &opts_);
|
|
1079
|
+
if (!interactive_) {
|
|
1080
|
+
// EvaluatePacket mode: kernel never sends INPUTNAMEPKT/OUTPUTNAMEPKT,
|
|
1081
|
+
// so stamp the pre-captured counter and derive outputName manually.
|
|
1082
|
+
result_.cellIndex = cellIndex_;
|
|
1083
|
+
if (!result_.aborted && result_.result.kind == WExpr::Symbol) {
|
|
1084
|
+
const std::string& sv = result_.result.strVal;
|
|
1085
|
+
std::string bare = sv;
|
|
1086
|
+
auto tick = sv.rfind('`');
|
|
1087
|
+
if (tick != std::string::npos) bare = sv.substr(tick + 1);
|
|
1088
|
+
if (bare != "Null")
|
|
1089
|
+
result_.outputName = "Out[" + std::to_string(cellIndex_) + "]=";
|
|
1090
|
+
} else if (!result_.aborted && result_.result.kind != WExpr::WError) {
|
|
1091
|
+
result_.outputName = "Out[" + std::to_string(cellIndex_) + "]=";
|
|
1092
|
+
}
|
|
1093
|
+
} else {
|
|
1094
|
+
// EnterExpressionPacket mode: DrainToEvalResult already populated
|
|
1095
|
+
// cellIndex/outputName from INPUTNAMEPKT/OUTPUTNAMEPKT.
|
|
1096
|
+
// Use our counter as fallback if the kernel didn't send them.
|
|
1097
|
+
if (result_.cellIndex == 0)
|
|
1098
|
+
result_.cellIndex = cellIndex_;
|
|
1099
|
+
}
|
|
1100
|
+
}
|
|
1101
|
+
if (opts_.hasOnPrint) opts_.onPrint.Release();
|
|
1102
|
+
if (opts_.hasOnMessage) opts_.onMessage.Release();
|
|
1103
|
+
if (opts_.hasOnDialogBegin) opts_.onDialogBegin.Release();
|
|
1104
|
+
if (opts_.hasOnDialogPrint) opts_.onDialogPrint.Release();
|
|
1105
|
+
if (opts_.hasOnDialogEnd) opts_.onDialogEnd.Release();
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
// ---- main thread: resolve promise after all TSFN callbacks delivered ----
|
|
1109
|
+
void OnOK() override {
|
|
1110
|
+
Napi::Env env = Env();
|
|
1111
|
+
abortFlag_.store(false);
|
|
1112
|
+
|
|
1113
|
+
EvalResult r = std::move(result_);
|
|
1114
|
+
Napi::Promise::Deferred d = std::move(deferred_);
|
|
1115
|
+
std::function<void()> cb = completionCb_;
|
|
1116
|
+
|
|
1117
|
+
auto resolveFn = [env, r = std::move(r), d = std::move(d), cb]() mutable {
|
|
1118
|
+
if (r.result.kind == WExpr::WError) {
|
|
1119
|
+
d.Reject(Napi::Error::New(env, r.result.strVal).Value());
|
|
1120
|
+
} else {
|
|
1121
|
+
Napi::Value v = EvalResultToNapi(env, r);
|
|
1122
|
+
if (env.IsExceptionPending())
|
|
1123
|
+
d.Reject(env.GetAndClearPendingException().Value());
|
|
1124
|
+
else
|
|
1125
|
+
d.Resolve(v);
|
|
1126
|
+
}
|
|
1127
|
+
cb();
|
|
1128
|
+
};
|
|
1129
|
+
|
|
1130
|
+
if (opts_.ctx) {
|
|
1131
|
+
opts_.ctx->fn = std::move(resolveFn);
|
|
1132
|
+
opts_.ctx->done();
|
|
1133
|
+
} else {
|
|
1134
|
+
resolveFn();
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
void OnError(const Napi::Error& e) override {
|
|
1139
|
+
Napi::Env env = Env();
|
|
1140
|
+
std::string msg = e.Message();
|
|
1141
|
+
Napi::Promise::Deferred d = std::move(deferred_);
|
|
1142
|
+
std::function<void()> cb = completionCb_;
|
|
1143
|
+
|
|
1144
|
+
auto rejectFn = [env, msg, d = std::move(d), cb]() mutable {
|
|
1145
|
+
d.Reject(Napi::Error::New(env, msg).Value());
|
|
1146
|
+
cb();
|
|
1147
|
+
};
|
|
1148
|
+
|
|
1149
|
+
if (opts_.ctx) {
|
|
1150
|
+
opts_.ctx->fn = std::move(rejectFn);
|
|
1151
|
+
opts_.ctx->done();
|
|
1152
|
+
} else {
|
|
1153
|
+
rejectFn();
|
|
1154
|
+
}
|
|
1155
|
+
}
|
|
1156
|
+
|
|
1157
|
+
private:
|
|
1158
|
+
Napi::Promise::Deferred deferred_;
|
|
1159
|
+
WSLINK lp_;
|
|
1160
|
+
std::string expr_;
|
|
1161
|
+
EvalOptions opts_;
|
|
1162
|
+
std::atomic<bool>& abortFlag_;
|
|
1163
|
+
std::function<void()> completionCb_;
|
|
1164
|
+
int64_t cellIndex_;
|
|
1165
|
+
bool interactive_;
|
|
1166
|
+
EvalResult result_;
|
|
1167
|
+
};
|
|
1168
|
+
|
|
1169
|
+
// ===========================================================================
|
|
1170
|
+
// WstpSession — JS class: new WstpSession(kernelPath?)
|
|
1171
|
+
// ===========================================================================
|
|
1172
|
+
static const char* kDefaultKernel =
|
|
1173
|
+
"/Applications/Wolfram 3.app/Contents/MacOS/WolframKernel";
|
|
1174
|
+
|
|
1175
|
+
class WstpSession : public Napi::ObjectWrap<WstpSession> {
|
|
1176
|
+
public:
|
|
1177
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1178
|
+
Napi::Function func = DefineClass(env, "WstpSession", {
|
|
1179
|
+
InstanceMethod<&WstpSession::Evaluate> ("evaluate"),
|
|
1180
|
+
InstanceMethod<&WstpSession::Sub> ("sub"),
|
|
1181
|
+
InstanceMethod<&WstpSession::DialogEval> ("dialogEval"),
|
|
1182
|
+
InstanceMethod<&WstpSession::ExitDialog> ("exitDialog"),
|
|
1183
|
+
InstanceMethod<&WstpSession::Interrupt> ("interrupt"),
|
|
1184
|
+
InstanceMethod<&WstpSession::Abort> ("abort"),
|
|
1185
|
+
InstanceMethod<&WstpSession::CreateSubsession>("createSubsession"),
|
|
1186
|
+
InstanceMethod<&WstpSession::Close> ("close"),
|
|
1187
|
+
InstanceAccessor<&WstpSession::IsOpen> ("isOpen"),
|
|
1188
|
+
InstanceAccessor<&WstpSession::IsDialogOpen> ("isDialogOpen"),
|
|
1189
|
+
});
|
|
1190
|
+
|
|
1191
|
+
Napi::FunctionReference* ctor = new Napi::FunctionReference();
|
|
1192
|
+
*ctor = Napi::Persistent(func);
|
|
1193
|
+
env.SetInstanceData(ctor);
|
|
1194
|
+
|
|
1195
|
+
exports.Set("WstpSession", func);
|
|
1196
|
+
return exports;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
// -----------------------------------------------------------------------
|
|
1200
|
+
// Constructor new WstpSession(kernelPath?)
|
|
1201
|
+
// -----------------------------------------------------------------------
|
|
1202
|
+
WstpSession(const Napi::CallbackInfo& info)
|
|
1203
|
+
: Napi::ObjectWrap<WstpSession>(info), wsEnv_(nullptr), lp_(nullptr), open_(false)
|
|
1204
|
+
{
|
|
1205
|
+
Napi::Env env = info.Env();
|
|
1206
|
+
|
|
1207
|
+
std::string kernelPath = kDefaultKernel;
|
|
1208
|
+
if (info.Length() > 0 && info[0].IsString())
|
|
1209
|
+
kernelPath = info[0].As<Napi::String>().Utf8Value();
|
|
1210
|
+
|
|
1211
|
+
// Parse options object: new WstpSession(opts?) or new WstpSession(path, opts?)
|
|
1212
|
+
// Supported options: { interactive: true } — use EnterTextPacket instead of
|
|
1213
|
+
// EvaluatePacket so the kernel populates In[n]/Out[n] variables.
|
|
1214
|
+
int optsIdx = (info.Length() > 0 && info[0].IsObject()) ? 0
|
|
1215
|
+
: (info.Length() > 1 && info[1].IsObject()) ? 1 : -1;
|
|
1216
|
+
if (optsIdx >= 0) {
|
|
1217
|
+
auto o = info[optsIdx].As<Napi::Object>();
|
|
1218
|
+
if (o.Has("interactive") && o.Get("interactive").IsBoolean())
|
|
1219
|
+
interactiveMode_ = o.Get("interactive").As<Napi::Boolean>().Value();
|
|
1220
|
+
}
|
|
1221
|
+
|
|
1222
|
+
WSEnvironmentParameter params = WSNewParameters(WSREVISION, WSAPIREVISION);
|
|
1223
|
+
wsEnv_ = WSInitialize(params);
|
|
1224
|
+
WSReleaseParameters(params);
|
|
1225
|
+
if (!wsEnv_) {
|
|
1226
|
+
Napi::Error::New(env, "WSInitialize failed").ThrowAsJavaScriptException();
|
|
1227
|
+
return;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
// Shell-quote the kernel path so spaces ("Wolfram 3.app") survive
|
|
1231
|
+
// being passed through /bin/sh by WSOpenArgcArgv.
|
|
1232
|
+
std::string linkName = "\"" + kernelPath + "\" -wstp";
|
|
1233
|
+
const char* argv[] = { "wstp", "-linkname", linkName.c_str(),
|
|
1234
|
+
"-linkmode", "launch" };
|
|
1235
|
+
|
|
1236
|
+
// Retry the entire kernel-launch sequence up to 3 times. On ~20% of
|
|
1237
|
+
// consecutive launches the kernel starts with $Output routing broken
|
|
1238
|
+
// (Print[]/Message[] produce no TextPacket). Killing the stale kernel
|
|
1239
|
+
// and spawning a fresh one resolves it reliably within 1-2 attempts.
|
|
1240
|
+
int err = 0;
|
|
1241
|
+
for (int attempt = 0; attempt <= 2; ++attempt) {
|
|
1242
|
+
if (attempt > 0) {
|
|
1243
|
+
DiagLog("[Session] restart attempt " + std::to_string(attempt) +
|
|
1244
|
+
" — $Output routing broken on previous kernel");
|
|
1245
|
+
WSClose(lp_); lp_ = nullptr;
|
|
1246
|
+
if (kernelPid_ > 0) { kill(kernelPid_, SIGTERM); kernelPid_ = 0; }
|
|
1247
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
err = 0;
|
|
1251
|
+
lp_ = WSOpenArgcArgv(wsEnv_, 5, const_cast<char**>(argv), &err);
|
|
1252
|
+
if (!lp_ || err != WSEOK) {
|
|
1253
|
+
std::string msg = "WSOpenArgcArgv failed (code " + std::to_string(err) + ")";
|
|
1254
|
+
WSDeinitialize(wsEnv_); wsEnv_ = nullptr;
|
|
1255
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
1256
|
+
return;
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
if (!WSActivate(lp_)) {
|
|
1260
|
+
const char* m = WSErrorMessage(lp_);
|
|
1261
|
+
std::string s = std::string("WSActivate failed: ") + (m ? m : "?");
|
|
1262
|
+
WSClose(lp_); lp_ = nullptr;
|
|
1263
|
+
WSDeinitialize(wsEnv_); wsEnv_ = nullptr;
|
|
1264
|
+
Napi::Error::New(env, s).ThrowAsJavaScriptException();
|
|
1265
|
+
return;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Get the kernel PID so CleanUp() can SIGTERM it.
|
|
1269
|
+
kernelPid_ = FetchKernelPid(lp_);
|
|
1270
|
+
|
|
1271
|
+
// Confirm $Output→TextPacket routing is live. If not, loop back
|
|
1272
|
+
// and restart with a fresh kernel process.
|
|
1273
|
+
if (WarmUpOutputRouting(lp_)) break; // success — proceed
|
|
1274
|
+
|
|
1275
|
+
// Last attempt — give up and continue with broken $Output rather
|
|
1276
|
+
// than looping indefinitely.
|
|
1277
|
+
if (attempt == 2)
|
|
1278
|
+
DiagLog("[Session] WARNING: $Output broken after 3 kernel launches");
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
open_ = true;
|
|
1282
|
+
abortFlag_.store(false);
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
~WstpSession() { CleanUp(); }
|
|
1286
|
+
|
|
1287
|
+
// -----------------------------------------------------------------------
|
|
1288
|
+
// evaluate(expr, opts?) → Promise<EvalResult>
|
|
1289
|
+
//
|
|
1290
|
+
// opts may contain { onPrint, onMessage } streaming callbacks.
|
|
1291
|
+
// Multiple calls are serialised through an internal queue — no link
|
|
1292
|
+
// corruption even if the caller doesn't await between calls.
|
|
1293
|
+
// -----------------------------------------------------------------------
|
|
1294
|
+
Napi::Value Evaluate(const Napi::CallbackInfo& info) {
|
|
1295
|
+
Napi::Env env = info.Env();
|
|
1296
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
1297
|
+
auto promise = deferred.Promise();
|
|
1298
|
+
|
|
1299
|
+
if (!open_) {
|
|
1300
|
+
deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
|
|
1301
|
+
return promise;
|
|
1302
|
+
}
|
|
1303
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
1304
|
+
deferred.Reject(Napi::TypeError::New(env, "evaluate(expr: string, opts?: object)").Value());
|
|
1305
|
+
return promise;
|
|
1306
|
+
}
|
|
1307
|
+
|
|
1308
|
+
std::string expr = info[0].As<Napi::String>().Utf8Value();
|
|
1309
|
+
|
|
1310
|
+
// Parse optional second argument: { onPrint?, onMessage?, onDialogBegin?,
|
|
1311
|
+
// onDialogPrint?, onDialogEnd?,
|
|
1312
|
+
// interactive?: boolean (override session default) }
|
|
1313
|
+
EvalOptions opts;
|
|
1314
|
+
int interactiveOverride = -1; // -1 = use session default
|
|
1315
|
+
if (info.Length() >= 2 && info[1].IsObject()) {
|
|
1316
|
+
auto optsObj = info[1].As<Napi::Object>();
|
|
1317
|
+
bool wantPrint = optsObj.Has("onPrint") && optsObj.Get("onPrint").IsFunction();
|
|
1318
|
+
bool wantMsg = optsObj.Has("onMessage") && optsObj.Get("onMessage").IsFunction();
|
|
1319
|
+
bool wantDBegin = optsObj.Has("onDialogBegin") && optsObj.Get("onDialogBegin").IsFunction();
|
|
1320
|
+
bool wantDPrint = optsObj.Has("onDialogPrint") && optsObj.Get("onDialogPrint").IsFunction();
|
|
1321
|
+
bool wantDEnd = optsObj.Has("onDialogEnd") && optsObj.Get("onDialogEnd").IsFunction();
|
|
1322
|
+
// Per-call interactive override: opts.interactive = true/false
|
|
1323
|
+
if (optsObj.Has("interactive") && optsObj.Get("interactive").IsBoolean())
|
|
1324
|
+
interactiveOverride = optsObj.Get("interactive").As<Napi::Boolean>().Value() ? 1 : 0;
|
|
1325
|
+
|
|
1326
|
+
// CompleteCtx: count = numTsfns + 1 (the extra slot is for OnOK/OnError).
|
|
1327
|
+
// This ensures the Promise resolves ONLY after every TSFN has been
|
|
1328
|
+
// finalized (= all queued callbacks delivered), regardless of whether
|
|
1329
|
+
// the TSFN finalizers or OnOK/OnError happen to run first.
|
|
1330
|
+
int tsfnCount = (wantPrint ? 1 : 0) + (wantMsg ? 1 : 0)
|
|
1331
|
+
+ (wantDBegin ? 1 : 0) + (wantDPrint ? 1 : 0)
|
|
1332
|
+
+ (wantDEnd ? 1 : 0);
|
|
1333
|
+
CompleteCtx* ctx = (tsfnCount > 0) ? new CompleteCtx{ tsfnCount + 1, {} } : nullptr;
|
|
1334
|
+
|
|
1335
|
+
auto makeTsfn = [&](const char* name, const char* key) {
|
|
1336
|
+
return Napi::ThreadSafeFunction::New(
|
|
1337
|
+
env, optsObj.Get(key).As<Napi::Function>(), name, 0, 1,
|
|
1338
|
+
ctx, [](Napi::Env, CompleteCtx* c) { c->done(); });
|
|
1339
|
+
};
|
|
1340
|
+
if (wantPrint) { opts.onPrint = makeTsfn("onPrint", "onPrint"); opts.hasOnPrint = true; }
|
|
1341
|
+
if (wantMsg) { opts.onMessage = makeTsfn("onMessage", "onMessage"); opts.hasOnMessage = true; }
|
|
1342
|
+
if (wantDBegin) { opts.onDialogBegin = makeTsfn("onDialogBegin", "onDialogBegin"); opts.hasOnDialogBegin = true; }
|
|
1343
|
+
if (wantDPrint) { opts.onDialogPrint = makeTsfn("onDialogPrint", "onDialogPrint"); opts.hasOnDialogPrint = true; }
|
|
1344
|
+
if (wantDEnd) { opts.onDialogEnd = makeTsfn("onDialogEnd", "onDialogEnd"); opts.hasOnDialogEnd = true; }
|
|
1345
|
+
opts.ctx = ctx;
|
|
1346
|
+
}
|
|
1347
|
+
// Wire up the dialog queue pointers so DrainToEvalResult can service
|
|
1348
|
+
// dialogEval() calls from the thread pool during a Dialog[] subsession.
|
|
1349
|
+
opts.dialogMutex = &dialogMutex_;
|
|
1350
|
+
opts.dialogQueue = &dialogQueue_;
|
|
1351
|
+
opts.dialogPending = &dialogPending_;
|
|
1352
|
+
opts.dialogOpen = &dialogOpen_;
|
|
1353
|
+
opts.abortFlag = &abortFlag_;
|
|
1354
|
+
|
|
1355
|
+
{
|
|
1356
|
+
std::lock_guard<std::mutex> lk(queueMutex_);
|
|
1357
|
+
queue_.push(QueuedEval{ std::move(expr), std::move(opts), std::move(deferred), interactiveOverride });
|
|
1358
|
+
}
|
|
1359
|
+
MaybeStartNext();
|
|
1360
|
+
return promise;
|
|
1361
|
+
}
|
|
1362
|
+
|
|
1363
|
+
// -----------------------------------------------------------------------
|
|
1364
|
+
// MaybeStartNext — pop the front of the queue and launch it, but only if
|
|
1365
|
+
// no evaluation is currently running (busy_ CAS ensures atomicity).
|
|
1366
|
+
// Called from: Evaluate() on main thread; completionCb_ on main thread.
|
|
1367
|
+
// Queue entry — one pending sub() call when the session is idle.
|
|
1368
|
+
struct QueuedSubIdle {
|
|
1369
|
+
std::string expr;
|
|
1370
|
+
Napi::Promise::Deferred deferred;
|
|
1371
|
+
};
|
|
1372
|
+
|
|
1373
|
+
// Sub-idle evals are preferred over normal evals so sub()-when-idle gets
|
|
1374
|
+
// a quick result without waiting for a queued evaluate().
|
|
1375
|
+
// -----------------------------------------------------------------------
|
|
1376
|
+
void MaybeStartNext() {
|
|
1377
|
+
bool expected = false;
|
|
1378
|
+
if (!busy_.compare_exchange_strong(expected, true))
|
|
1379
|
+
return; // already running
|
|
1380
|
+
|
|
1381
|
+
std::unique_lock<std::mutex> lk(queueMutex_);
|
|
1382
|
+
|
|
1383
|
+
// Check sub-idle queue first.
|
|
1384
|
+
if (!subIdleQueue_.empty()) {
|
|
1385
|
+
auto item = std::move(subIdleQueue_.front());
|
|
1386
|
+
subIdleQueue_.pop();
|
|
1387
|
+
lk.unlock();
|
|
1388
|
+
StartSubIdleWorker(std::move(item));
|
|
1389
|
+
return;
|
|
1390
|
+
}
|
|
1391
|
+
|
|
1392
|
+
if (queue_.empty()) {
|
|
1393
|
+
busy_.store(false);
|
|
1394
|
+
return;
|
|
1395
|
+
}
|
|
1396
|
+
auto item = std::move(queue_.front());
|
|
1397
|
+
queue_.pop();
|
|
1398
|
+
lk.unlock();
|
|
1399
|
+
|
|
1400
|
+
// Resolve interactive mode: per-call override takes precedence over session default.
|
|
1401
|
+
bool evalInteractive = (item.interactiveOverride == -1)
|
|
1402
|
+
? interactiveMode_
|
|
1403
|
+
: (item.interactiveOverride == 1);
|
|
1404
|
+
auto* worker = new EvaluateWorker(
|
|
1405
|
+
std::move(item.deferred),
|
|
1406
|
+
lp_,
|
|
1407
|
+
std::move(item.expr),
|
|
1408
|
+
std::move(item.opts),
|
|
1409
|
+
abortFlag_,
|
|
1410
|
+
[this]() { busy_.store(false); MaybeStartNext(); },
|
|
1411
|
+
nextLine_.fetch_add(1),
|
|
1412
|
+
evalInteractive
|
|
1413
|
+
);
|
|
1414
|
+
worker->Queue();
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1417
|
+
// Launch a lightweight EvaluateWorker that resolves with just the WExpr
|
|
1418
|
+
// (not a full EvalResult) — used by sub() when the session is idle.
|
|
1419
|
+
void StartSubIdleWorker(QueuedSubIdle item) {
|
|
1420
|
+
struct SubIdleWorker : public Napi::AsyncWorker {
|
|
1421
|
+
SubIdleWorker(Napi::Promise::Deferred d, WSLINK lp, std::string expr,
|
|
1422
|
+
std::function<void()> done)
|
|
1423
|
+
: Napi::AsyncWorker(d.Env()),
|
|
1424
|
+
deferred_(std::move(d)), lp_(lp), expr_(std::move(expr)),
|
|
1425
|
+
done_(std::move(done)) {}
|
|
1426
|
+
|
|
1427
|
+
void Execute() override {
|
|
1428
|
+
if (!WSPutFunction(lp_, "EvaluatePacket", 1) ||
|
|
1429
|
+
!WSPutFunction(lp_, "ToExpression", 1) ||
|
|
1430
|
+
!WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) ||
|
|
1431
|
+
!WSEndPacket(lp_) ||
|
|
1432
|
+
!WSFlush(lp_)) {
|
|
1433
|
+
SetError("sub (idle): failed to send EvaluatePacket");
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
result_ = DrainToEvalResult(lp_);
|
|
1437
|
+
}
|
|
1438
|
+
void OnOK() override {
|
|
1439
|
+
Napi::Env env = Env();
|
|
1440
|
+
if (result_.result.kind == WExpr::WError) {
|
|
1441
|
+
deferred_.Reject(Napi::Error::New(env, result_.result.strVal).Value());
|
|
1442
|
+
} else {
|
|
1443
|
+
Napi::Value v = WExprToNapi(env, result_.result);
|
|
1444
|
+
if (env.IsExceptionPending())
|
|
1445
|
+
deferred_.Reject(env.GetAndClearPendingException().Value());
|
|
1446
|
+
else
|
|
1447
|
+
deferred_.Resolve(v);
|
|
1448
|
+
}
|
|
1449
|
+
done_();
|
|
1450
|
+
}
|
|
1451
|
+
void OnError(const Napi::Error& e) override {
|
|
1452
|
+
deferred_.Reject(e.Value());
|
|
1453
|
+
done_();
|
|
1454
|
+
}
|
|
1455
|
+
private:
|
|
1456
|
+
Napi::Promise::Deferred deferred_;
|
|
1457
|
+
WSLINK lp_;
|
|
1458
|
+
std::string expr_;
|
|
1459
|
+
std::function<void()> done_;
|
|
1460
|
+
EvalResult result_;
|
|
1461
|
+
};
|
|
1462
|
+
|
|
1463
|
+
(new SubIdleWorker(std::move(item.deferred), lp_, std::move(item.expr),
|
|
1464
|
+
[this]() { busy_.store(false); MaybeStartNext(); }))->Queue();
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
// -----------------------------------------------------------------------
|
|
1468
|
+
// sub(expr) → Promise<WExpr>
|
|
1469
|
+
//
|
|
1470
|
+
// Queues a lightweight evaluation that resolves with just the WExpr result
|
|
1471
|
+
// (not a full EvalResult). If the session is busy the sub() is prioritised
|
|
1472
|
+
// over any pending evaluate() calls and starts as soon as the current eval
|
|
1473
|
+
// finishes. If idle it starts immediately.
|
|
1474
|
+
// -----------------------------------------------------------------------
|
|
1475
|
+
Napi::Value Sub(const Napi::CallbackInfo& info) {
|
|
1476
|
+
Napi::Env env = info.Env();
|
|
1477
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
1478
|
+
auto promise = deferred.Promise();
|
|
1479
|
+
|
|
1480
|
+
if (!open_) {
|
|
1481
|
+
deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
|
|
1482
|
+
return promise;
|
|
1483
|
+
}
|
|
1484
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
1485
|
+
deferred.Reject(Napi::TypeError::New(env, "sub(expr: string)").Value());
|
|
1486
|
+
return promise;
|
|
1487
|
+
}
|
|
1488
|
+
std::string expr = info[0].As<Napi::String>().Utf8Value();
|
|
1489
|
+
|
|
1490
|
+
{
|
|
1491
|
+
std::lock_guard<std::mutex> lk(queueMutex_);
|
|
1492
|
+
subIdleQueue_.push(QueuedSubIdle{ std::move(expr), std::move(deferred) });
|
|
1493
|
+
}
|
|
1494
|
+
MaybeStartNext();
|
|
1495
|
+
return promise;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
// -----------------------------------------------------------------------
|
|
1499
|
+
// exitDialog(retVal?) → Promise<void>
|
|
1500
|
+
//
|
|
1501
|
+
// Exits the currently-open Dialog[] subsession by entering
|
|
1502
|
+
// "Return[retVal]" as if the user typed it at the interactive prompt
|
|
1503
|
+
// (EnterTextPacket). This is different from plain dialogEval('Return[]')
|
|
1504
|
+
// which uses EvaluatePacket and does NOT exit the dialog because Return[]
|
|
1505
|
+
// at the top level of EvaluatePacket is unevaluated.
|
|
1506
|
+
//
|
|
1507
|
+
// Returns a Promise that resolves (with Null) when ENDDLGPKT is received,
|
|
1508
|
+
// or rejects immediately if no dialog is open.
|
|
1509
|
+
// -----------------------------------------------------------------------
|
|
1510
|
+
Napi::Value ExitDialog(const Napi::CallbackInfo& info) {
|
|
1511
|
+
Napi::Env env = info.Env();
|
|
1512
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
1513
|
+
auto promise = deferred.Promise();
|
|
1514
|
+
|
|
1515
|
+
if (!open_) {
|
|
1516
|
+
deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
|
|
1517
|
+
return promise;
|
|
1518
|
+
}
|
|
1519
|
+
if (!dialogOpen_.load()) {
|
|
1520
|
+
deferred.Reject(Napi::Error::New(env,
|
|
1521
|
+
"no dialog subsession is open").Value());
|
|
1522
|
+
return promise;
|
|
1523
|
+
}
|
|
1524
|
+
// Build "Return[]" or "Return[retVal]" as the exit expression.
|
|
1525
|
+
std::string exitExpr = "Return[]";
|
|
1526
|
+
if (info.Length() >= 1 && info[0].IsString())
|
|
1527
|
+
exitExpr = "Return[" + info[0].As<Napi::String>().Utf8Value() + "]";
|
|
1528
|
+
|
|
1529
|
+
auto tsfn = Napi::ThreadSafeFunction::New(
|
|
1530
|
+
env,
|
|
1531
|
+
Napi::Function::New(env, [deferred](const Napi::CallbackInfo& ci) mutable {
|
|
1532
|
+
Napi::Env e = ci.Env();
|
|
1533
|
+
if (ci.Length() > 0) {
|
|
1534
|
+
auto obj = ci[0];
|
|
1535
|
+
if (obj.IsObject()) {
|
|
1536
|
+
auto o = obj.As<Napi::Object>();
|
|
1537
|
+
if (o.Has("error") && o.Get("error").IsString()) {
|
|
1538
|
+
deferred.Reject(Napi::Error::New(e,
|
|
1539
|
+
o.Get("error").As<Napi::String>().Utf8Value()).Value());
|
|
1540
|
+
return;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
}
|
|
1544
|
+
deferred.Resolve(e.Null());
|
|
1545
|
+
}),
|
|
1546
|
+
"exitDialogResolve", 0, 1);
|
|
1547
|
+
|
|
1548
|
+
DialogRequest req;
|
|
1549
|
+
req.expr = std::move(exitExpr);
|
|
1550
|
+
req.useEnterText = true;
|
|
1551
|
+
req.resolve = std::move(tsfn);
|
|
1552
|
+
{
|
|
1553
|
+
std::lock_guard<std::mutex> lk(dialogMutex_);
|
|
1554
|
+
dialogQueue_.push(std::move(req));
|
|
1555
|
+
}
|
|
1556
|
+
dialogPending_.store(true);
|
|
1557
|
+
return promise;
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
// -----------------------------------------------------------------------
|
|
1561
|
+
// dialogEval(expr) → Promise<WExpr>
|
|
1562
|
+
// Rejects immediately if no dialog is open.
|
|
1563
|
+
// -----------------------------------------------------------------------
|
|
1564
|
+
Napi::Value DialogEval(const Napi::CallbackInfo& info) {
|
|
1565
|
+
Napi::Env env = info.Env();
|
|
1566
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
1567
|
+
auto promise = deferred.Promise();
|
|
1568
|
+
|
|
1569
|
+
if (!open_) {
|
|
1570
|
+
deferred.Reject(Napi::Error::New(env, "Session is closed").Value());
|
|
1571
|
+
return promise;
|
|
1572
|
+
}
|
|
1573
|
+
if (!dialogOpen_.load()) {
|
|
1574
|
+
deferred.Reject(Napi::Error::New(env,
|
|
1575
|
+
"no dialog subsession is open — call Dialog[] first").Value());
|
|
1576
|
+
return promise;
|
|
1577
|
+
}
|
|
1578
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
1579
|
+
deferred.Reject(Napi::TypeError::New(env, "dialogEval(expr: string)").Value());
|
|
1580
|
+
return promise;
|
|
1581
|
+
}
|
|
1582
|
+
std::string expr = info[0].As<Napi::String>().Utf8Value();
|
|
1583
|
+
|
|
1584
|
+
// Create a TSFN that resolves/rejects the deferred on the main thread.
|
|
1585
|
+
// The TSFN is Released by serviceDialogRequest() after the single call.
|
|
1586
|
+
auto tsfn = Napi::ThreadSafeFunction::New(
|
|
1587
|
+
env,
|
|
1588
|
+
Napi::Function::New(env, [deferred](const Napi::CallbackInfo& ci) mutable {
|
|
1589
|
+
Napi::Env e = ci.Env();
|
|
1590
|
+
if (ci.Length() > 0 && ci[0].IsObject()) {
|
|
1591
|
+
auto obj = ci[0].As<Napi::Object>();
|
|
1592
|
+
// Check for error sentinel: { error: string }
|
|
1593
|
+
if (obj.Has("error") && obj.Get("error").IsString()) {
|
|
1594
|
+
deferred.Reject(Napi::Error::New(e,
|
|
1595
|
+
obj.Get("error").As<Napi::String>().Utf8Value()).Value());
|
|
1596
|
+
} else {
|
|
1597
|
+
deferred.Resolve(ci[0]);
|
|
1598
|
+
}
|
|
1599
|
+
} else {
|
|
1600
|
+
deferred.Reject(Napi::Error::New(e, "dialogEval: bad result").Value());
|
|
1601
|
+
}
|
|
1602
|
+
}),
|
|
1603
|
+
"dialogResolve", 0, 1);
|
|
1604
|
+
|
|
1605
|
+
DialogRequest req;
|
|
1606
|
+
req.expr = std::move(expr);
|
|
1607
|
+
req.useEnterText = false;
|
|
1608
|
+
req.resolve = std::move(tsfn);
|
|
1609
|
+
{
|
|
1610
|
+
std::lock_guard<std::mutex> lk(dialogMutex_);
|
|
1611
|
+
dialogQueue_.push(std::move(req));
|
|
1612
|
+
}
|
|
1613
|
+
dialogPending_.store(true);
|
|
1614
|
+
return promise;
|
|
1615
|
+
}
|
|
1616
|
+
|
|
1617
|
+
// -----------------------------------------------------------------------
|
|
1618
|
+
// interrupt() — send WSInterruptMessage (best-effort).
|
|
1619
|
+
//
|
|
1620
|
+
// This is NOT abort() — it does not cancel the evaluation. Its effect
|
|
1621
|
+
// depends entirely on whether a Wolfram-side interrupt handler has been
|
|
1622
|
+
// installed (e.g. Internal`AddHandler["Interrupt", Function[Null, Dialog[]]]).
|
|
1623
|
+
// On kernels without such a handler this is a no-op.
|
|
1624
|
+
// Main-thread only — same thread-safety guarantee as abort().
|
|
1625
|
+
// -----------------------------------------------------------------------
|
|
1626
|
+
Napi::Value Interrupt(const Napi::CallbackInfo& info) {
|
|
1627
|
+
Napi::Env env = info.Env();
|
|
1628
|
+
if (!open_) return Napi::Boolean::New(env, false);
|
|
1629
|
+
int ok = WSPutMessage(lp_, WSInterruptMessage);
|
|
1630
|
+
return Napi::Boolean::New(env, ok != 0);
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
// -----------------------------------------------------------------------
|
|
1634
|
+
// abort() — interrupt the currently running evaluation.
|
|
1635
|
+
//
|
|
1636
|
+
// Sends WSAbortMessage on the link. Per the WSTP spec WSPutMessage() is
|
|
1637
|
+
// thread-safe and will cause WSNextPacket() on the thread-pool thread to
|
|
1638
|
+
// return ILLEGALPKT (link reset). The promise then rejects; a fresh
|
|
1639
|
+
// session must be created for further work after the kernel crashes/exits.
|
|
1640
|
+
//
|
|
1641
|
+
// For a softer (recoverable) interrupt use evaluate("Interrupt[]")
|
|
1642
|
+
// before the long computation, or wrap the computation in TimeConstrained.
|
|
1643
|
+
// -----------------------------------------------------------------------
|
|
1644
|
+
Napi::Value Abort(const Napi::CallbackInfo& info) {
|
|
1645
|
+
Napi::Env env = info.Env();
|
|
1646
|
+
if (!open_) return Napi::Boolean::New(env, false);
|
|
1647
|
+
// Only signal the kernel if an evaluation is actually in flight.
|
|
1648
|
+
// Sending WSAbortMessage to an idle kernel causes it to emit a
|
|
1649
|
+
// spurious RETURNPKT[$Aborted] that would corrupt the next evaluation.
|
|
1650
|
+
if (!busy_.load()) return Napi::Boolean::New(env, false);
|
|
1651
|
+
abortFlag_.store(true);
|
|
1652
|
+
int ok = WSPutMessage(lp_, WSAbortMessage);
|
|
1653
|
+
return Napi::Boolean::New(env, ok != 0);
|
|
1654
|
+
}
|
|
1655
|
+
|
|
1656
|
+
// -----------------------------------------------------------------------
|
|
1657
|
+
// createSubsession(kernelPath?) → WstpSession
|
|
1658
|
+
//
|
|
1659
|
+
// Spawns a completely independent WolframKernel process. Its entire
|
|
1660
|
+
// state (variables, definitions, memory) is isolated from the parent
|
|
1661
|
+
// and from every other session. Ideal for sandboxed or parallel work.
|
|
1662
|
+
// -----------------------------------------------------------------------
|
|
1663
|
+
Napi::Value CreateSubsession(const Napi::CallbackInfo& info) {
|
|
1664
|
+
Napi::Env env = info.Env();
|
|
1665
|
+
Napi::FunctionReference* ctor = env.GetInstanceData<Napi::FunctionReference>();
|
|
1666
|
+
if (info.Length() > 0 && info[0].IsString())
|
|
1667
|
+
return ctor->New({ info[0] });
|
|
1668
|
+
return ctor->New({});
|
|
1669
|
+
}
|
|
1670
|
+
|
|
1671
|
+
// -----------------------------------------------------------------------
|
|
1672
|
+
// close()
|
|
1673
|
+
// -----------------------------------------------------------------------
|
|
1674
|
+
Napi::Value Close(const Napi::CallbackInfo& info) {
|
|
1675
|
+
CleanUp();
|
|
1676
|
+
return info.Env().Undefined();
|
|
1677
|
+
}
|
|
1678
|
+
|
|
1679
|
+
// -----------------------------------------------------------------------
|
|
1680
|
+
// isOpen (read-only accessor)
|
|
1681
|
+
// -----------------------------------------------------------------------
|
|
1682
|
+
Napi::Value IsOpen(const Napi::CallbackInfo& info) {
|
|
1683
|
+
return Napi::Boolean::New(info.Env(), open_);
|
|
1684
|
+
}
|
|
1685
|
+
|
|
1686
|
+
// -----------------------------------------------------------------------
|
|
1687
|
+
// isDialogOpen (read-only accessor)
|
|
1688
|
+
// -----------------------------------------------------------------------
|
|
1689
|
+
Napi::Value IsDialogOpen(const Napi::CallbackInfo& info) {
|
|
1690
|
+
return Napi::Boolean::New(info.Env(), dialogOpen_.load());
|
|
1691
|
+
}
|
|
1692
|
+
|
|
1693
|
+
private:
|
|
1694
|
+
// Queue entry — one pending evaluate() call.
|
|
1695
|
+
// interactiveOverride: -1 = use session default, 0 = force batch, 1 = force interactive
|
|
1696
|
+
struct QueuedEval {
|
|
1697
|
+
std::string expr;
|
|
1698
|
+
EvalOptions opts;
|
|
1699
|
+
Napi::Promise::Deferred deferred;
|
|
1700
|
+
int interactiveOverride = -1;
|
|
1701
|
+
};
|
|
1702
|
+
|
|
1703
|
+
void CleanUp() {
|
|
1704
|
+
if (lp_) { WSClose(lp_); lp_ = nullptr; }
|
|
1705
|
+
if (wsEnv_) { WSDeinitialize(wsEnv_); wsEnv_ = nullptr; }
|
|
1706
|
+
// Kill the child kernel process so it doesn't become a zombie.
|
|
1707
|
+
// WSClose() closes the link but does not terminate the WolframKernel
|
|
1708
|
+
// child process — without this, each session leaks a kernel.
|
|
1709
|
+
if (kernelPid_ > 0) { kill(kernelPid_, SIGTERM); kernelPid_ = 0; }
|
|
1710
|
+
open_ = false;
|
|
1711
|
+
}
|
|
1712
|
+
|
|
1713
|
+
|
|
1714
|
+
// ---------------------------------------------------------------------------
|
|
1715
|
+
// Verify that $Output→TextPacket routing is live before the session is used.
|
|
1716
|
+
// Sends Print["$WARMUP$"] and looks for a TextPacket response, retrying up to
|
|
1717
|
+
// 5 times with a 100 ms pause between attempts. On some WolframKernel
|
|
1718
|
+
// launches (observed ~20% of consecutive runs) the kernel processes the
|
|
1719
|
+
// first EvaluatePacket before its internal $Output stream is wired to the
|
|
1720
|
+
// WSTP link, causing ALL subsequent Print[]/Message[] calls to silently
|
|
1721
|
+
// drop their output. One extra round-trip here forces the kernel to
|
|
1722
|
+
// complete its output-routing setup before any user code is evaluated.
|
|
1723
|
+
// Returns true if output routing is confirmed; false if it cannot be fixed.
|
|
1724
|
+
// ---------------------------------------------------------------------------
|
|
1725
|
+
static bool WarmUpOutputRouting(WSLINK lp) {
|
|
1726
|
+
// Brief initial pause after WSActivate: lets the kernel scheduler
|
|
1727
|
+
// fully wire $Output → WSTP TextPacket before the first probe.
|
|
1728
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
|
1729
|
+
for (int attempt = 0; attempt < 4; ++attempt) {
|
|
1730
|
+
if (attempt > 0)
|
|
1731
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
|
1732
|
+
|
|
1733
|
+
// Send EvaluatePacket[Print["$WARMUP$"]]
|
|
1734
|
+
if (!WSPutFunction(lp, "EvaluatePacket", 1) ||
|
|
1735
|
+
!WSPutFunction(lp, "Print", 1) ||
|
|
1736
|
+
!WSPutString (lp, "$WARMUP$") ||
|
|
1737
|
+
!WSEndPacket (lp) ||
|
|
1738
|
+
!WSFlush (lp))
|
|
1739
|
+
return false; // link error
|
|
1740
|
+
|
|
1741
|
+
bool gotText = false;
|
|
1742
|
+
while (true) {
|
|
1743
|
+
int pkt = WSNextPacket(lp);
|
|
1744
|
+
if (pkt == TEXTPKT) {
|
|
1745
|
+
WSNewPacket(lp);
|
|
1746
|
+
gotText = true;
|
|
1747
|
+
} else if (pkt == RETURNPKT) {
|
|
1748
|
+
WSNewPacket(lp);
|
|
1749
|
+
break;
|
|
1750
|
+
} else if (pkt == 0 || pkt == ILLEGALPKT) {
|
|
1751
|
+
WSClearError(lp);
|
|
1752
|
+
return false;
|
|
1753
|
+
} else {
|
|
1754
|
+
WSNewPacket(lp);
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
if (gotText) {
|
|
1758
|
+
DiagLog("[WarmUp] $Output routing verified on attempt " + std::to_string(attempt + 1));
|
|
1759
|
+
return true;
|
|
1760
|
+
}
|
|
1761
|
+
DiagLog("[WarmUp] no TextPacket on attempt " + std::to_string(attempt + 1) + ", retrying...");
|
|
1762
|
+
}
|
|
1763
|
+
DiagLog("[WarmUp] $Output routing NOT confirmed — kernel restart needed");
|
|
1764
|
+
return false;
|
|
1765
|
+
}
|
|
1766
|
+
|
|
1767
|
+
// ---------------------------------------------------------------------------
|
|
1768
|
+
// Synchronously evaluate $ProcessID and return the kernel's PID.
|
|
1769
|
+
// Called once from the constructor after WSActivate, while the link is idle.
|
|
1770
|
+
// Returns 0 on failure (non-fatal — cleanup falls back to no-kill).
|
|
1771
|
+
// ---------------------------------------------------------------------------
|
|
1772
|
+
static pid_t FetchKernelPid(WSLINK lp) {
|
|
1773
|
+
// Send: EvaluatePacket[ToExpression["$ProcessID"]]
|
|
1774
|
+
if (!WSPutFunction(lp, "EvaluatePacket", 1) ||
|
|
1775
|
+
!WSPutFunction(lp, "ToExpression", 1) ||
|
|
1776
|
+
!WSPutString (lp, "$ProcessID") ||
|
|
1777
|
+
!WSEndPacket (lp) ||
|
|
1778
|
+
!WSFlush (lp))
|
|
1779
|
+
return 0;
|
|
1780
|
+
|
|
1781
|
+
// Drain packets until we see ReturnPacket with an integer.
|
|
1782
|
+
pid_t pid = 0;
|
|
1783
|
+
while (true) {
|
|
1784
|
+
int pkt = WSNextPacket(lp);
|
|
1785
|
+
if (pkt == RETURNPKT) {
|
|
1786
|
+
if (WSGetType(lp) == WSTKINT) {
|
|
1787
|
+
wsint64 v = 0;
|
|
1788
|
+
WSGetInteger64(lp, &v);
|
|
1789
|
+
pid = static_cast<pid_t>(v);
|
|
1790
|
+
}
|
|
1791
|
+
WSNewPacket(lp);
|
|
1792
|
+
break;
|
|
1793
|
+
}
|
|
1794
|
+
if (pkt == 0 || pkt == ILLEGALPKT) { WSClearError(lp); break; }
|
|
1795
|
+
WSNewPacket(lp); // skip INPUTNAMEPKT, OUTPUTNAMEPKT, etc.
|
|
1796
|
+
}
|
|
1797
|
+
return pid;
|
|
1798
|
+
}
|
|
1799
|
+
|
|
1800
|
+
WSEnvironment wsEnv_;
|
|
1801
|
+
WSLINK lp_;
|
|
1802
|
+
bool open_;
|
|
1803
|
+
bool interactiveMode_ = false; // true → EnterTextPacket, populates In/Out
|
|
1804
|
+
pid_t kernelPid_ = 0; // child process — killed on CleanUp
|
|
1805
|
+
std::atomic<int64_t> nextLine_{1}; // 1-based In[n] counter for EvalResult.cellIndex
|
|
1806
|
+
std::atomic<bool> abortFlag_{false};
|
|
1807
|
+
std::atomic<bool> busy_{false};
|
|
1808
|
+
std::mutex queueMutex_;
|
|
1809
|
+
std::queue<QueuedEval> queue_;
|
|
1810
|
+
std::queue<QueuedSubIdle> subIdleQueue_; // sub() — runs before queue_ items
|
|
1811
|
+
// Dialog subsession state — written on main thread, consumed on thread pool
|
|
1812
|
+
std::mutex dialogMutex_;
|
|
1813
|
+
std::queue<DialogRequest> dialogQueue_; // dialogEval() requests
|
|
1814
|
+
std::atomic<bool> dialogPending_{false};
|
|
1815
|
+
std::atomic<bool> dialogOpen_{false};
|
|
1816
|
+
};
|
|
1817
|
+
|
|
1818
|
+
// ===========================================================================
|
|
1819
|
+
// ReadNextWorker — async "read one expression" for WstpReader
|
|
1820
|
+
// ===========================================================================
|
|
1821
|
+
class ReadNextWorker : public Napi::AsyncWorker {
|
|
1822
|
+
public:
|
|
1823
|
+
ReadNextWorker(Napi::Promise::Deferred deferred, WSLINK lp, std::atomic<bool>& activated)
|
|
1824
|
+
: Napi::AsyncWorker(deferred.Env()), deferred_(deferred), lp_(lp), activated_(activated) {}
|
|
1825
|
+
|
|
1826
|
+
// Thread-pool: activate on first call, then read next expression.
|
|
1827
|
+
void Execute() override {
|
|
1828
|
+
// WSActivate must happen on a thread-pool thread, never on the JS
|
|
1829
|
+
// main thread. The kernel's WSTP runtime can only complete the
|
|
1830
|
+
// handshake once it is inside the Do-loop calling LinkWrite.
|
|
1831
|
+
if (!activated_.load()) {
|
|
1832
|
+
DiagLog("[WstpReader] WSActivate starting...");
|
|
1833
|
+
if (!WSActivate(lp_)) {
|
|
1834
|
+
const char* msg = WSErrorMessage(lp_);
|
|
1835
|
+
std::string err = std::string("WstpReader: WSActivate failed: ") + (msg ? msg : "?");
|
|
1836
|
+
DiagLog("[WstpReader] " + err);
|
|
1837
|
+
SetError(err);
|
|
1838
|
+
WSClearError(lp_);
|
|
1839
|
+
return;
|
|
1840
|
+
}
|
|
1841
|
+
// DO NOT call WSGetType / WSNewPacket here.
|
|
1842
|
+
// On TCPIP connect-mode links WSGetType is non-blocking: it returns 0
|
|
1843
|
+
// for *both* "no data in buffer yet" and a genuine WSTKEND boundary
|
|
1844
|
+
// token. Calling WSNewPacket when there is no data in the buffer
|
|
1845
|
+
// corrupts the link's internal read state so that every subsequent
|
|
1846
|
+
// WSGetType also returns 0, making all future reads hang forever.
|
|
1847
|
+
// The WSTKEND / preamble case (if it occurs at all) is handled below
|
|
1848
|
+
// after WSWaitForLinkActivity returns and data is confirmed present.
|
|
1849
|
+
DiagLog("[WstpReader] activated.");
|
|
1850
|
+
activated_.store(true);
|
|
1851
|
+
}
|
|
1852
|
+
|
|
1853
|
+
// Spin on WSGetType() waiting for the link buffer to become non-empty.
|
|
1854
|
+
//
|
|
1855
|
+
// WSGetType() is non-blocking on TCPIP connect-mode links: it returns
|
|
1856
|
+
// 0 immediately if no data is buffered. We spin in 5 ms increments
|
|
1857
|
+
// rather than using WSWaitForLinkActivity(), which has been observed to
|
|
1858
|
+
// return WSWAITSUCCESS before the buffer is actually readable on fast
|
|
1859
|
+
// consecutive runs, leading to a ReadExprRaw(type=0) error.
|
|
1860
|
+
//
|
|
1861
|
+
// The first 500 ms are traced at each distinct type value so failures
|
|
1862
|
+
// can be diagnosed from the log. Hard timeout: 5 seconds.
|
|
1863
|
+
{
|
|
1864
|
+
auto spinStart = std::chrono::steady_clock::now();
|
|
1865
|
+
auto deadline = spinStart + std::chrono::seconds(5);
|
|
1866
|
+
auto traceWindow = spinStart + std::chrono::milliseconds(500);
|
|
1867
|
+
int iters = 0, lastLoggedType = -999;
|
|
1868
|
+
while (true) {
|
|
1869
|
+
int t = WSGetType(lp_);
|
|
1870
|
+
++iters;
|
|
1871
|
+
auto now = std::chrono::steady_clock::now();
|
|
1872
|
+
// Within the first 500 ms log every distinct type change.
|
|
1873
|
+
if (now < traceWindow && t != lastLoggedType) {
|
|
1874
|
+
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
1875
|
+
now - spinStart).count();
|
|
1876
|
+
DiagLog("[WstpReader] spin t=" + std::to_string(t)
|
|
1877
|
+
+ " +" + std::to_string(ms) + "ms iter=" + std::to_string(iters));
|
|
1878
|
+
lastLoggedType = t;
|
|
1879
|
+
}
|
|
1880
|
+
if (t != 0) {
|
|
1881
|
+
auto ms = std::chrono::duration_cast<std::chrono::milliseconds>(
|
|
1882
|
+
now - spinStart).count();
|
|
1883
|
+
DiagLog("[WstpReader] spin done: type=" + std::to_string(t)
|
|
1884
|
+
+ " after " + std::to_string(iters) + " iters +"
|
|
1885
|
+
+ std::to_string(ms) + "ms");
|
|
1886
|
+
break;
|
|
1887
|
+
}
|
|
1888
|
+
if (now >= deadline) {
|
|
1889
|
+
DiagLog("[WstpReader] spin TIMEOUT after " + std::to_string(iters)
|
|
1890
|
+
+ " iters (5s)");
|
|
1891
|
+
SetError("WstpReader: 5-second timeout — link dead or data never arrived");
|
|
1892
|
+
return;
|
|
1893
|
+
}
|
|
1894
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
result_ = ReadExprRaw(lp_);
|
|
1899
|
+
|
|
1900
|
+
// If ReadExprRaw still encountered WSTKEND (type=0) it means the spin
|
|
1901
|
+
// exited on a protocol boundary token, not an expression token. Skip
|
|
1902
|
+
// it once with WSNewPacket and re-spin for the real expression.
|
|
1903
|
+
if (result_.kind == WExpr::WError
|
|
1904
|
+
&& result_.strVal.find("unexpected token type: 0") != std::string::npos) {
|
|
1905
|
+
DiagLog("[WstpReader] got WSTKEND from ReadExprRaw — skipping preamble, re-spinning");
|
|
1906
|
+
WSNewPacket(lp_);
|
|
1907
|
+
auto spinStart2 = std::chrono::steady_clock::now();
|
|
1908
|
+
auto deadline2 = spinStart2 + std::chrono::seconds(5);
|
|
1909
|
+
int iters2 = 0;
|
|
1910
|
+
while (true) {
|
|
1911
|
+
int t = WSGetType(lp_);
|
|
1912
|
+
++iters2;
|
|
1913
|
+
if (t != 0) {
|
|
1914
|
+
DiagLog("[WstpReader] re-spin done: type=" + std::to_string(t)
|
|
1915
|
+
+ " after " + std::to_string(iters2) + " iters");
|
|
1916
|
+
break;
|
|
1917
|
+
}
|
|
1918
|
+
if (std::chrono::steady_clock::now() >= deadline2) {
|
|
1919
|
+
DiagLog("[WstpReader] re-spin TIMEOUT after " + std::to_string(iters2)
|
|
1920
|
+
+ " iters");
|
|
1921
|
+
result_ = WExpr::mkError("WstpReader: 5-second timeout after WSTKEND skip");
|
|
1922
|
+
return;
|
|
1923
|
+
}
|
|
1924
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
1925
|
+
}
|
|
1926
|
+
result_ = ReadExprRaw(lp_);
|
|
1927
|
+
}
|
|
1928
|
+
|
|
1929
|
+
// After a successful expression read, advance past the expression
|
|
1930
|
+
// boundary so the next WSGetType() call sees the next expression's
|
|
1931
|
+
// start marker rather than a residual WSTKEND.
|
|
1932
|
+
if (result_.kind != WExpr::WError)
|
|
1933
|
+
WSNewPacket(lp_);
|
|
1934
|
+
|
|
1935
|
+
DiagLog("[WstpReader] ReadExprRaw kind=" + std::to_string((int)result_.kind)
|
|
1936
|
+
+ (result_.kind == WExpr::WError ? " err=" + result_.strVal : ""));
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
void OnOK() override {
|
|
1940
|
+
Napi::Env env = Env();
|
|
1941
|
+
if (result_.kind == WExpr::WError) {
|
|
1942
|
+
deferred_.Reject(Napi::Error::New(env, result_.strVal).Value());
|
|
1943
|
+
return;
|
|
1944
|
+
}
|
|
1945
|
+
Napi::Value v = WExprToNapi(env, result_);
|
|
1946
|
+
if (env.IsExceptionPending())
|
|
1947
|
+
deferred_.Reject(env.GetAndClearPendingException().Value());
|
|
1948
|
+
else
|
|
1949
|
+
deferred_.Resolve(v);
|
|
1950
|
+
}
|
|
1951
|
+
|
|
1952
|
+
void OnError(const Napi::Error& e) override {
|
|
1953
|
+
deferred_.Reject(e.Value());
|
|
1954
|
+
}
|
|
1955
|
+
|
|
1956
|
+
private:
|
|
1957
|
+
Napi::Promise::Deferred deferred_;
|
|
1958
|
+
WSLINK lp_;
|
|
1959
|
+
std::atomic<bool>& activated_;
|
|
1960
|
+
WExpr result_;
|
|
1961
|
+
};
|
|
1962
|
+
|
|
1963
|
+
// ===========================================================================
|
|
1964
|
+
// WstpReader — connects to a named WSTP link created by a Wolfram kernel.
|
|
1965
|
+
//
|
|
1966
|
+
// Usage pattern:
|
|
1967
|
+
// 1. Main kernel: $link = LinkCreate[LinkProtocol->"TCPIP"]
|
|
1968
|
+
// linkName = LinkName[$link] // → "port@host,0@host"
|
|
1969
|
+
// 2. JS: const reader = new WstpReader(linkName)
|
|
1970
|
+
// 3. Loop: while (reader.isOpen) { const v = await reader.readNext() }
|
|
1971
|
+
//
|
|
1972
|
+
// Each call to readNext() blocks (on the thread pool) until the next
|
|
1973
|
+
// expression is available, then resolves with an ExprTree.
|
|
1974
|
+
// When the kernel closes the link (LinkClose[$link]), readNext() rejects
|
|
1975
|
+
// with a "link closed" error.
|
|
1976
|
+
// ===========================================================================
|
|
1977
|
+
class WstpReader : public Napi::ObjectWrap<WstpReader> {
|
|
1978
|
+
public:
|
|
1979
|
+
static Napi::Object Init(Napi::Env env, Napi::Object exports) {
|
|
1980
|
+
Napi::Function func = DefineClass(env, "WstpReader", {
|
|
1981
|
+
InstanceMethod<&WstpReader::ReadNext>("readNext"),
|
|
1982
|
+
InstanceMethod<&WstpReader::Close> ("close"),
|
|
1983
|
+
InstanceAccessor<&WstpReader::IsOpen>("isOpen"),
|
|
1984
|
+
});
|
|
1985
|
+
exports.Set("WstpReader", func);
|
|
1986
|
+
return exports;
|
|
1987
|
+
}
|
|
1988
|
+
|
|
1989
|
+
// -----------------------------------------------------------------------
|
|
1990
|
+
// Constructor new WstpReader(linkName, protocol?)
|
|
1991
|
+
// linkName — value returned by Wolfram's LinkName[$link]
|
|
1992
|
+
// protocol — link protocol string, default "TCPIP"
|
|
1993
|
+
// -----------------------------------------------------------------------
|
|
1994
|
+
WstpReader(const Napi::CallbackInfo& info)
|
|
1995
|
+
: Napi::ObjectWrap<WstpReader>(info), wsEnv_(nullptr), lp_(nullptr), open_(false)
|
|
1996
|
+
{
|
|
1997
|
+
Napi::Env env = info.Env();
|
|
1998
|
+
if (info.Length() < 1 || !info[0].IsString()) {
|
|
1999
|
+
Napi::TypeError::New(env, "WstpReader(linkName: string, protocol?: string)")
|
|
2000
|
+
.ThrowAsJavaScriptException();
|
|
2001
|
+
return;
|
|
2002
|
+
}
|
|
2003
|
+
std::string linkName = info[0].As<Napi::String>().Utf8Value();
|
|
2004
|
+
std::string protocol = "TCPIP";
|
|
2005
|
+
if (info.Length() >= 2 && info[1].IsString())
|
|
2006
|
+
protocol = info[1].As<Napi::String>().Utf8Value();
|
|
2007
|
+
|
|
2008
|
+
WSEnvironmentParameter params = WSNewParameters(WSREVISION, WSAPIREVISION);
|
|
2009
|
+
wsEnv_ = WSInitialize(params);
|
|
2010
|
+
WSReleaseParameters(params);
|
|
2011
|
+
if (!wsEnv_) {
|
|
2012
|
+
Napi::Error::New(env, "WSInitialize failed (WstpReader)")
|
|
2013
|
+
.ThrowAsJavaScriptException();
|
|
2014
|
+
return;
|
|
2015
|
+
}
|
|
2016
|
+
|
|
2017
|
+
// Connect to the already-listening link.
|
|
2018
|
+
const char* argv[] = {
|
|
2019
|
+
"reader",
|
|
2020
|
+
"-linkname", linkName.c_str(),
|
|
2021
|
+
"-linkmode", "connect",
|
|
2022
|
+
"-linkprotocol", protocol.c_str()
|
|
2023
|
+
};
|
|
2024
|
+
int err = 0;
|
|
2025
|
+
lp_ = WSOpenArgcArgv(wsEnv_, 7, const_cast<char**>(argv), &err);
|
|
2026
|
+
if (!lp_ || err != WSEOK) {
|
|
2027
|
+
std::string msg = "WstpReader: WSOpenArgcArgv failed (code "
|
|
2028
|
+
+ std::to_string(err) + ") for link \"" + linkName + "\"";
|
|
2029
|
+
WSDeinitialize(wsEnv_); wsEnv_ = nullptr;
|
|
2030
|
+
Napi::Error::New(env, msg).ThrowAsJavaScriptException();
|
|
2031
|
+
return;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
// Do NOT call WSActivate here — it would block the JS main thread.
|
|
2035
|
+
// Activation is deferred to the first ReadNextWorker::Execute() call,
|
|
2036
|
+
// which runs on the libuv thread pool. The kernel's WSTP runtime
|
|
2037
|
+
// completes the handshake once it enters the Do-loop and calls LinkWrite.
|
|
2038
|
+
open_ = true;
|
|
2039
|
+
activated_.store(false);
|
|
2040
|
+
}
|
|
2041
|
+
|
|
2042
|
+
~WstpReader() { CleanUp(); }
|
|
2043
|
+
|
|
2044
|
+
// readNext() → Promise<ExprTree>
|
|
2045
|
+
Napi::Value ReadNext(const Napi::CallbackInfo& info) {
|
|
2046
|
+
Napi::Env env = info.Env();
|
|
2047
|
+
auto deferred = Napi::Promise::Deferred::New(env);
|
|
2048
|
+
if (!open_) {
|
|
2049
|
+
deferred.Reject(Napi::Error::New(env, "WstpReader is closed").Value());
|
|
2050
|
+
return deferred.Promise();
|
|
2051
|
+
}
|
|
2052
|
+
(new ReadNextWorker(deferred, lp_, activated_))->Queue();
|
|
2053
|
+
return deferred.Promise();
|
|
2054
|
+
}
|
|
2055
|
+
|
|
2056
|
+
// close()
|
|
2057
|
+
Napi::Value Close(const Napi::CallbackInfo& info) {
|
|
2058
|
+
CleanUp();
|
|
2059
|
+
return info.Env().Undefined();
|
|
2060
|
+
}
|
|
2061
|
+
|
|
2062
|
+
// isOpen
|
|
2063
|
+
Napi::Value IsOpen(const Napi::CallbackInfo& info) {
|
|
2064
|
+
return Napi::Boolean::New(info.Env(), open_);
|
|
2065
|
+
}
|
|
2066
|
+
|
|
2067
|
+
private:
|
|
2068
|
+
void CleanUp() {
|
|
2069
|
+
if (lp_) { WSClose(lp_); lp_ = nullptr; }
|
|
2070
|
+
if (wsEnv_) { WSDeinitialize(wsEnv_); wsEnv_ = nullptr; }
|
|
2071
|
+
open_ = false;
|
|
2072
|
+
}
|
|
2073
|
+
|
|
2074
|
+
WSEnvironment wsEnv_;
|
|
2075
|
+
WSLINK lp_;
|
|
2076
|
+
bool open_;
|
|
2077
|
+
std::atomic<bool> activated_{false};
|
|
2078
|
+
};
|
|
2079
|
+
|
|
2080
|
+
// ===========================================================================
|
|
2081
|
+
// setDiagHandler(fn) — JS-callable; registers the global diagnostic callback.
|
|
2082
|
+
// Pass null / no argument to clear. The TSFN is Unref()'d so it does not
|
|
2083
|
+
// hold the Node.js event loop open.
|
|
2084
|
+
// ===========================================================================
|
|
2085
|
+
static Napi::Value SetDiagHandler(const Napi::CallbackInfo& info) {
|
|
2086
|
+
Napi::Env env = info.Env();
|
|
2087
|
+
std::lock_guard<std::mutex> lk(g_diagMutex);
|
|
2088
|
+
if (g_diagActive) {
|
|
2089
|
+
g_diagTsfn.Release();
|
|
2090
|
+
g_diagActive = false;
|
|
2091
|
+
}
|
|
2092
|
+
if (info.Length() >= 1 && info[0].IsFunction()) {
|
|
2093
|
+
g_diagTsfn = Napi::ThreadSafeFunction::New(
|
|
2094
|
+
env,
|
|
2095
|
+
info[0].As<Napi::Function>(),
|
|
2096
|
+
"diagHandler",
|
|
2097
|
+
/*maxQueueSize=*/0,
|
|
2098
|
+
/*initialThreadCount=*/1);
|
|
2099
|
+
g_diagTsfn.Unref(env); // do not prevent process exit
|
|
2100
|
+
g_diagActive = true;
|
|
2101
|
+
}
|
|
2102
|
+
return env.Undefined();
|
|
2103
|
+
}
|
|
2104
|
+
|
|
2105
|
+
// ===========================================================================
|
|
2106
|
+
// Module entry point
|
|
2107
|
+
// ===========================================================================
|
|
2108
|
+
Napi::Object InitModule(Napi::Env env, Napi::Object exports) {
|
|
2109
|
+
WstpSession::Init(env, exports);
|
|
2110
|
+
WstpReader::Init(env, exports);
|
|
2111
|
+
exports.Set("setDiagHandler",
|
|
2112
|
+
Napi::Function::New(env, SetDiagHandler, "setDiagHandler"));
|
|
2113
|
+
return exports;
|
|
2114
|
+
}
|
|
2115
|
+
|
|
2116
|
+
NODE_API_MODULE(wstp, InitModule)
|