wstp-node 0.3.0 → 0.4.1
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/InstallationWindows.md +171 -0
- package/README.md +48 -3
- package/binding.gyp +7 -9
- package/build/Release/wstp.node +0 -0
- package/index.d.ts +13 -0
- package/package.json +7 -4
- package/scripts/wstp_dir.js +78 -0
- package/src/addon.cc +187 -12
- package/test.js +378 -3
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
# Building wstp-node on Windows
|
|
2
|
+
|
|
3
|
+
This guide walks through compiling the native WSTP addon (`wstp.node`) on a
|
|
4
|
+
Windows 10 / 11 x64 machine so that the
|
|
5
|
+
[Wolfbook VS Code extension](https://github.com/vanbaalon/wolfbook) can use it
|
|
6
|
+
without any build step on the end-user's machine.
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Prerequisites
|
|
11
|
+
|
|
12
|
+
### 1. Wolfram Engine (or Mathematica)
|
|
13
|
+
|
|
14
|
+
Download the free **Wolfram Engine** from:
|
|
15
|
+
https://wolfram.com/engine/
|
|
16
|
+
|
|
17
|
+
Default installation path after setup:
|
|
18
|
+
```
|
|
19
|
+
C:\Program Files\Wolfram Research\Wolfram Engine\14.x\
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
Verify the WSTP DeveloperKit is present:
|
|
23
|
+
```
|
|
24
|
+
C:\Program Files\Wolfram Research\Wolfram Engine\14.x\SystemFiles\Links\WSTP\DeveloperKit\Windows-x86-64\CompilerAdditions\
|
|
25
|
+
```
|
|
26
|
+
The key files needed are `wstp64i4s.lib` and `wstp.h` inside that folder.
|
|
27
|
+
|
|
28
|
+
> If your Wolfram version or install path differs, set the `WSTP_DIR` environment
|
|
29
|
+
> variable to the `CompilerAdditions` folder before building:
|
|
30
|
+
> ```powershell
|
|
31
|
+
> $env:WSTP_DIR = "C:\Program Files\Wolfram Research\Wolfram Engine\14.x\SystemFiles\Links\WSTP\DeveloperKit\Windows-x86-64\CompilerAdditions"
|
|
32
|
+
> ```
|
|
33
|
+
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
### 2. Visual Studio Build Tools (C++ compiler)
|
|
37
|
+
|
|
38
|
+
Run the following in **PowerShell as Administrator**:
|
|
39
|
+
|
|
40
|
+
```powershell
|
|
41
|
+
winget install Microsoft.VisualStudio.2022.BuildTools
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Then open **Visual Studio Installer** → **Modify** → select the
|
|
45
|
+
**"Desktop development with C++"** workload. Make sure these components
|
|
46
|
+
are checked:
|
|
47
|
+
|
|
48
|
+
- ✅ MSVC v143 – VS 2022 C++ x64/x86 build tools
|
|
49
|
+
- ✅ Windows 11 SDK (or Windows 10 SDK)
|
|
50
|
+
- ✅ C++ CMake tools (optional but useful)
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
### 3. Node.js ≥ 18 (LTS)
|
|
55
|
+
|
|
56
|
+
```powershell
|
|
57
|
+
winget install OpenJS.NodeJS.LTS
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
Restart your shell after installation, then verify:
|
|
61
|
+
|
|
62
|
+
```powershell
|
|
63
|
+
node --version # 20.x or 22.x
|
|
64
|
+
npm --version
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
### 4. Git
|
|
70
|
+
|
|
71
|
+
```powershell
|
|
72
|
+
winget install Git.Git
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Build
|
|
78
|
+
|
|
79
|
+
Open a **Developer PowerShell for VS 2022** (or a regular PowerShell — `node-gyp`
|
|
80
|
+
finds MSVC automatically if the Build Tools are installed correctly).
|
|
81
|
+
|
|
82
|
+
```powershell
|
|
83
|
+
git clone https://github.com/vanbaalon/mathematica-wstp-node.git
|
|
84
|
+
cd mathematica-wstp-node
|
|
85
|
+
|
|
86
|
+
npm install
|
|
87
|
+
npm run build
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
A successful build prints:
|
|
91
|
+
```
|
|
92
|
+
Build succeeded: build\Release\wstp.node
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
### Verify the build
|
|
96
|
+
|
|
97
|
+
```powershell
|
|
98
|
+
node -e "const {WstpSession}=require('./build/Release/wstp.node'); const s=new WstpSession(); s.evaluate('1+1').then(r=>{console.log(r.result.value); s.close()})"
|
|
99
|
+
```
|
|
100
|
+
Expected output: `2`
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
## Contributing the binary to the Wolfbook extension
|
|
105
|
+
|
|
106
|
+
After building, copy the binary to the extension's `wstp/prebuilt/` folder and
|
|
107
|
+
commit it so CI can package the Windows VSIX:
|
|
108
|
+
|
|
109
|
+
```powershell
|
|
110
|
+
# from inside the mathematica-wstp-node directory:
|
|
111
|
+
copy build\Release\wstp.node ..\wolfbook\wstp\prebuilt\wstp-win32-x64.node
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
Then on macOS / Linux, run the deploy script to build both platform VSIXs:
|
|
115
|
+
|
|
116
|
+
```bash
|
|
117
|
+
bash deploy-extension.sh package
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Setting up a self-hosted GitHub Actions runner (optional)
|
|
123
|
+
|
|
124
|
+
If you want CI to build the Windows binary automatically on every release tag:
|
|
125
|
+
|
|
126
|
+
**In your browser:** go to
|
|
127
|
+
`https://github.com/vanbaalon/mathematica-wstp-node` →
|
|
128
|
+
Settings → Actions → Runners → **New self-hosted runner** → Windows x64
|
|
129
|
+
|
|
130
|
+
GitHub shows you a unique token and the exact download URL. Run those commands
|
|
131
|
+
in **PowerShell as Administrator**:
|
|
132
|
+
|
|
133
|
+
```powershell
|
|
134
|
+
mkdir C:\actions-runner; cd C:\actions-runner
|
|
135
|
+
|
|
136
|
+
# Replace the URL and token with what GitHub shows you:
|
|
137
|
+
Invoke-WebRequest -Uri https://github.com/actions/runner/releases/download/v2.x.x/actions-runner-win-x64-2.x.x.zip -OutFile runner.zip
|
|
138
|
+
Add-Type -AssemblyName System.IO.Compression.FileSystem
|
|
139
|
+
[System.IO.Compression.ZipFile]::ExtractToDirectory("$PWD\runner.zip", "$PWD")
|
|
140
|
+
|
|
141
|
+
.\config.cmd --url https://github.com/vanbaalon/mathematica-wstp-node --token YOUR_TOKEN_HERE
|
|
142
|
+
|
|
143
|
+
# Install as a Windows service so it runs even when you are logged out:
|
|
144
|
+
.\svc.cmd install
|
|
145
|
+
.\svc.cmd start
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
Verify the runner is online: GitHub → Settings → Actions → Runners → green dot.
|
|
149
|
+
|
|
150
|
+
Once both the macOS and Windows runners are registered, push a version tag from
|
|
151
|
+
your Mac to trigger a full build:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
git tag v0.5.0 && git push origin v0.5.0
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
The workflow builds both binaries and publishes them as assets on the
|
|
158
|
+
`mathematica-wstp-node` GitHub Release. The `wolfbook` workflow then downloads
|
|
159
|
+
those assets and packages the platform-specific VSIXs automatically.
|
|
160
|
+
|
|
161
|
+
---
|
|
162
|
+
|
|
163
|
+
## Troubleshooting
|
|
164
|
+
|
|
165
|
+
| Problem | Solution |
|
|
166
|
+
|---------|----------|
|
|
167
|
+
| `gyp ERR! find VS` | Open Developer PowerShell for VS 2022, or run `npm config set msvs_version 2022` |
|
|
168
|
+
| `LINK : fatal error LNK1181: cannot open input file 'wstp64i4s.lib'` | WSTP DeveloperKit not found — set `WSTP_DIR` (see Prerequisites) |
|
|
169
|
+
| `node.exe` crashes on `require('./build/Release/wstp.node')` | Node.js architecture mismatch — ensure you installed 64-bit Node |
|
|
170
|
+
| Runner shows offline | `cd C:\actions-runner && .\svc.cmd status`, then `.\svc.cmd start` |
|
|
171
|
+
| `error MSB8036: The Windows SDK version X was not found` | Install the matching Windows SDK via Visual Studio Installer |
|
package/README.md
CHANGED
|
@@ -23,6 +23,7 @@ const { WstpSession, WstpReader, setDiagHandler } = require('./build/Release/wst
|
|
|
23
23
|
- [`evaluate(expr, opts?)`](#evaluateexpr-opts) — queue an expression for evaluation; supports streaming `Print` callbacks
|
|
24
24
|
- [`sub(expr)`](#subexpr) — priority evaluation that jumps ahead of the queue, for quick queries during a long computation
|
|
25
25
|
- [`abort()`](#abort) — interrupt the currently running evaluation
|
|
26
|
+
- [`closeAllDialogs()`](#closealldialogs) — immediately reject all pending dialog promises and reset dialog state
|
|
26
27
|
- [`dialogEval(expr)`](#dialogevalexpr) — evaluate inside an active `Dialog[]` subsession
|
|
27
28
|
- [`exitDialog(retVal?)`](#exitdialogretval) — exit the current dialog and resume the main evaluation
|
|
28
29
|
- [`interrupt()`](#interrupt) — send a low-level interrupt signal to the kernel
|
|
@@ -53,11 +54,16 @@ const { WstpSession, WstpReader, setDiagHandler } = require('./build/Release/wst
|
|
|
53
54
|
|
|
54
55
|
| Requirement | Notes |
|
|
55
56
|
|-------------|-------|
|
|
56
|
-
| macOS | Tested on macOS 13
|
|
57
|
+
| macOS | Tested on macOS 13+ (ARM64 and x86-64) |
|
|
58
|
+
| Windows 10/11 x64 | See [InstallationWindows.md](InstallationWindows.md) for the full Windows guide |
|
|
59
|
+
| Linux x86-64 / ARM64 | Should work with standard `node-gyp` toolchain |
|
|
57
60
|
| Node.js ≥ 18 | Earlier versions may work but are untested |
|
|
58
|
-
| Clang / Xcode Command Line Tools | `xcode-select --install` |
|
|
61
|
+
| Clang / Xcode Command Line Tools (macOS) | `xcode-select --install` |
|
|
62
|
+
| MSVC Build Tools with C++ workload (Windows) | Visual Studio 2019+ or Build Tools |
|
|
59
63
|
| Wolfram Mathematica or Wolfram Engine | Provides `WolframKernel` and the WSTP SDK headers/libraries |
|
|
60
64
|
|
|
65
|
+
> **Windows users:** follow [InstallationWindows.md](InstallationWindows.md) instead of the steps below.
|
|
66
|
+
|
|
61
67
|
### 1. Clone
|
|
62
68
|
|
|
63
69
|
```bash
|
|
@@ -91,7 +97,7 @@ The script automatically locates the WSTP SDK inside the default Wolfram install
|
|
|
91
97
|
node test.js
|
|
92
98
|
```
|
|
93
99
|
|
|
94
|
-
Expected last line: `All
|
|
100
|
+
Expected last line: `All 36 tests passed.`
|
|
95
101
|
|
|
96
102
|
A more comprehensive suite (both modes + In/Out + comparison) lives in `tmp/tests_all.js`:
|
|
97
103
|
|
|
@@ -317,6 +323,43 @@ const r = await p;
|
|
|
317
323
|
|
|
318
324
|
---
|
|
319
325
|
|
|
326
|
+
### `closeAllDialogs()`
|
|
327
|
+
|
|
328
|
+
```ts
|
|
329
|
+
session.closeAllDialogs(): boolean
|
|
330
|
+
```
|
|
331
|
+
|
|
332
|
+
Unconditionally reset all dialog state on the Node.js side.
|
|
333
|
+
|
|
334
|
+
- Drains the internal dialog queue, immediately **rejecting** every pending `dialogEval()` and
|
|
335
|
+
`exitDialog()` promise with an error — no callers are left waiting forever.
|
|
336
|
+
- Clears `isDialogOpen` to `false`.
|
|
337
|
+
|
|
338
|
+
This does **not** send any packet to the kernel — it only fixes Node-side bookkeeping. Use
|
|
339
|
+
it in error-recovery paths, before `abort()`, or whenever you need to guarantee clean dialog
|
|
340
|
+
state without knowing whether a dialog is actually still running.
|
|
341
|
+
|
|
342
|
+
Returns `true` if `isDialogOpen` was `true` before the call (something was cleaned up),
|
|
343
|
+
`false` if it was already clear.
|
|
344
|
+
|
|
345
|
+
```js
|
|
346
|
+
// Safe no-op when there is no open dialog:
|
|
347
|
+
const cleaned = session.closeAllDialogs(); // false
|
|
348
|
+
|
|
349
|
+
// Reliable recovery pattern before abort:
|
|
350
|
+
session.closeAllDialogs(); // reject any hanging dialog promises immediately
|
|
351
|
+
session.abort();
|
|
352
|
+
|
|
353
|
+
// Queued dialogEval() promises reject with a descriptive error:
|
|
354
|
+
const p = session.evaluate('Dialog[]');
|
|
355
|
+
await pollUntil(() => session.isDialogOpen);
|
|
356
|
+
const pe = session.dialogEval('1 + 1').catch(e => e.message);
|
|
357
|
+
session.closeAllDialogs(); // pe rejects → "dialog closed by closeAllDialogs"
|
|
358
|
+
session.abort();
|
|
359
|
+
```
|
|
360
|
+
|
|
361
|
+
---
|
|
362
|
+
|
|
320
363
|
### `dialogEval(expr)`
|
|
321
364
|
|
|
322
365
|
```ts
|
|
@@ -990,6 +1033,8 @@ kb.close();
|
|
|
990
1033
|
| Abort | `evaluate()` **resolves** with `aborted: true`, `result.value === '$Aborted'` |
|
|
991
1034
|
| Kernel crashes | `evaluate()` rejects with a link error message — create a new `WstpSession` |
|
|
992
1035
|
| `dialogEval()` / `exitDialog()` when no dialog open | Rejects with `"no dialog subsession is open"` |
|
|
1036
|
+
| `dialogEval()` / `exitDialog()` when flushed by `closeAllDialogs()` | Rejects with `"dialog closed by closeAllDialogs"` |
|
|
1037
|
+
| `abort()` / `closeAllDialogs()` flushes dialog queue | Pending `dialogEval()`/`exitDialog()` promises reject immediately |
|
|
993
1038
|
| `evaluate()` after `close()` | Rejects with `"Session is closed"` |
|
|
994
1039
|
| `WstpReader.readNext()` after link closes | Rejects with a link-closed error |
|
|
995
1040
|
|
package/binding.gyp
CHANGED
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
{
|
|
2
2
|
"variables": {
|
|
3
|
-
#
|
|
4
|
-
# Override
|
|
5
|
-
|
|
3
|
+
# Cross-platform WSTP DeveloperKit path.
|
|
4
|
+
# Override by setting WSTP_DIR in the environment.
|
|
5
|
+
# See scripts/wstp_dir.js for auto-detection logic.
|
|
6
|
+
"wstp_dir%": "<!@(node scripts/wstp_dir.js)"
|
|
6
7
|
},
|
|
7
8
|
"targets": [
|
|
8
9
|
{
|
|
@@ -16,13 +17,9 @@
|
|
|
16
17
|
|
|
17
18
|
"defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"],
|
|
18
19
|
|
|
19
|
-
"libraries": [
|
|
20
|
-
# The static library ships as libWSTPi4.a in the macOS DeveloperKit.
|
|
21
|
-
"<(wstp_dir)/libWSTPi4.a"
|
|
22
|
-
],
|
|
23
|
-
|
|
24
20
|
"conditions": [
|
|
25
21
|
["OS=='mac'", {
|
|
22
|
+
"libraries": ["<(wstp_dir)/libWSTPi4.a"],
|
|
26
23
|
"xcode_settings": {
|
|
27
24
|
"GCC_ENABLE_CPP_EXCEPTIONS": "YES",
|
|
28
25
|
"MACOSX_DEPLOYMENT_TARGET": "11.0",
|
|
@@ -37,8 +34,9 @@
|
|
|
37
34
|
}
|
|
38
35
|
}],
|
|
39
36
|
["OS=='linux'", {
|
|
37
|
+
"libraries": ["<(wstp_dir)/libWSTPi4.a"],
|
|
40
38
|
"cflags_cc": ["-std=c++17", "-Wall", "-Wextra"],
|
|
41
|
-
"libraries": ["-lrt", "-lpthread", "-ldl", "-lm"]
|
|
39
|
+
"link_settings": { "libraries": ["-lrt", "-lpthread", "-ldl", "-lm"] }
|
|
42
40
|
}],
|
|
43
41
|
["OS=='win'", {
|
|
44
42
|
"msvs_settings": {
|
package/build/Release/wstp.node
CHANGED
|
Binary file
|
package/index.d.ts
CHANGED
|
@@ -158,6 +158,19 @@ export class WstpSession {
|
|
|
158
158
|
/** True while a Dialog[] subsession is open on this link. */
|
|
159
159
|
readonly isDialogOpen: boolean;
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* True when the session is fully ready to accept a new evaluation:
|
|
163
|
+
* the kernel is running (`isOpen`), no evaluation is currently executing
|
|
164
|
+
* or queued, and no Dialog[] subsession is open.
|
|
165
|
+
*
|
|
166
|
+
* Equivalent to:
|
|
167
|
+
* `isOpen && !busy && !isDialogOpen && queue.empty() && subQueue.empty()`
|
|
168
|
+
*
|
|
169
|
+
* This is a synchronous snapshot; the value may change on the next tick
|
|
170
|
+
* if an async operation starts or finishes concurrently.
|
|
171
|
+
*/
|
|
172
|
+
readonly isReady: boolean;
|
|
173
|
+
|
|
161
174
|
/**
|
|
162
175
|
* Interrupt the currently running evaluate() call.
|
|
163
176
|
*
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "wstp-node",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.4.1",
|
|
4
4
|
"description": "Native Node.js addon for Wolfram/Mathematica WSTP — kernel sessions with evaluation queue, streaming Print/messages, Dialog subsessions, and side-channel WstpReader",
|
|
5
5
|
"main": "build/Release/wstp.node",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -20,16 +20,19 @@
|
|
|
20
20
|
"license": "MIT",
|
|
21
21
|
"os": [
|
|
22
22
|
"darwin",
|
|
23
|
-
"linux"
|
|
23
|
+
"linux",
|
|
24
|
+
"win32"
|
|
24
25
|
],
|
|
25
26
|
"engines": {
|
|
26
27
|
"node": ">=18"
|
|
27
28
|
},
|
|
28
29
|
"files": [
|
|
29
30
|
"src/addon.cc",
|
|
31
|
+
"scripts/wstp_dir.js",
|
|
30
32
|
"build.sh",
|
|
31
33
|
"binding.gyp",
|
|
32
34
|
"index.d.ts",
|
|
35
|
+
"InstallationWindows.md",
|
|
33
36
|
"examples/",
|
|
34
37
|
"test.js",
|
|
35
38
|
"test_interrupt_dialog.js"
|
|
@@ -43,9 +46,9 @@
|
|
|
43
46
|
"demo": "node examples/demo.js"
|
|
44
47
|
},
|
|
45
48
|
"dependencies": {
|
|
46
|
-
"node-addon-api": "^8.
|
|
49
|
+
"node-addon-api": "^8.6.0"
|
|
47
50
|
},
|
|
48
51
|
"devDependencies": {
|
|
49
|
-
"node-gyp": "^
|
|
52
|
+
"node-gyp": "^12.2.0"
|
|
50
53
|
}
|
|
51
54
|
}
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* wstp_dir.js
|
|
3
|
+
* Cross-platform helper used by binding.gyp to locate the WSTP DeveloperKit.
|
|
4
|
+
*
|
|
5
|
+
* Usage in binding.gyp: "wstp_dir%": "<!@(node scripts/wstp_dir.js)"
|
|
6
|
+
*
|
|
7
|
+
* Precedence:
|
|
8
|
+
* 1. WSTP_DIR environment variable (always wins)
|
|
9
|
+
* 2. Platform-specific auto-detect below
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
'use strict';
|
|
13
|
+
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const fs = require('fs');
|
|
16
|
+
|
|
17
|
+
function resolve () {
|
|
18
|
+
// ── 1. Explicit override ────────────────────────────────────────────────
|
|
19
|
+
if (process.env.WSTP_DIR) {
|
|
20
|
+
return process.env.WSTP_DIR;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// ── 2. Windows ──────────────────────────────────────────────────────────
|
|
24
|
+
if (process.platform === 'win32') {
|
|
25
|
+
const base = 'C:\\Program Files\\Wolfram Research\\Wolfram Engine';
|
|
26
|
+
let versions;
|
|
27
|
+
try {
|
|
28
|
+
versions = fs.readdirSync(base)
|
|
29
|
+
.filter(d => /^\d/.test(d))
|
|
30
|
+
.sort()
|
|
31
|
+
.reverse();
|
|
32
|
+
} catch (e) {
|
|
33
|
+
throw new Error(
|
|
34
|
+
`WSTP DeveloperKit not found at "${base}".\n` +
|
|
35
|
+
`Install Wolfram Engine or set WSTP_DIR to the CompilerAdditions folder.`
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
if (!versions.length) throw new Error(`No Wolfram Engine version found under "${base}"`);
|
|
39
|
+
return path.join(
|
|
40
|
+
base, versions[0],
|
|
41
|
+
'SystemFiles', 'Links', 'WSTP', 'DeveloperKit',
|
|
42
|
+
'Windows-x86-64', 'CompilerAdditions'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// ── 3. macOS ────────────────────────────────────────────────────────────
|
|
47
|
+
if (process.platform === 'darwin') {
|
|
48
|
+
const { execSync } = require('child_process');
|
|
49
|
+
const machine = execSync('uname -m').toString().trim();
|
|
50
|
+
const archDir = machine === 'arm64' ? 'MacOSX-ARM64' : 'MacOSX-x86-64';
|
|
51
|
+
return path.join(
|
|
52
|
+
'/Applications/Wolfram 3.app/Contents/SystemFiles/Links/WSTP/DeveloperKit',
|
|
53
|
+
archDir, 'CompilerAdditions'
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
// ── 4. Linux ────────────────────────────────────────────────────────────
|
|
58
|
+
{
|
|
59
|
+
const { execSync } = require('child_process');
|
|
60
|
+
const machine = execSync('uname -m').toString().trim();
|
|
61
|
+
const archDir = machine === 'aarch64' ? 'Linux-ARM64' : 'Linux-x86-64';
|
|
62
|
+
const candidates = [
|
|
63
|
+
path.join(process.env.HOME || '/root', 'Wolfram', 'WolframEngine',
|
|
64
|
+
'SystemFiles', 'Links', 'WSTP', 'DeveloperKit', archDir, 'CompilerAdditions'),
|
|
65
|
+
path.join('/usr/local/Wolfram/WolframEngine',
|
|
66
|
+
'SystemFiles', 'Links', 'WSTP', 'DeveloperKit', archDir, 'CompilerAdditions'),
|
|
67
|
+
];
|
|
68
|
+
for (const c of candidates) {
|
|
69
|
+
if (fs.existsSync(c)) return c;
|
|
70
|
+
}
|
|
71
|
+
throw new Error(
|
|
72
|
+
'WSTP DeveloperKit not found for Linux.\n' +
|
|
73
|
+
'Set WSTP_DIR to the CompilerAdditions directory.'
|
|
74
|
+
);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
process.stdout.write(resolve());
|
package/src/addon.cc
CHANGED
|
@@ -222,6 +222,46 @@ static WExpr ReadExprRaw(WSLINK lp, int depth = 0) {
|
|
|
222
222
|
// Forward declaration — WExprToNapi is defined after WstpSession.
|
|
223
223
|
static Napi::Value WExprToNapi(Napi::Env env, const WExpr& e);
|
|
224
224
|
|
|
225
|
+
// ---------------------------------------------------------------------------
|
|
226
|
+
// drainDialogAbortResponse — drain the WSTP link after aborting out of a
|
|
227
|
+
// dialog inner loop.
|
|
228
|
+
//
|
|
229
|
+
// When abort() fires proactively (abortFlag_ is true before the kernel has
|
|
230
|
+
// sent its response), WSAbortMessage has already been posted but the kernel's
|
|
231
|
+
// abort response — RETURNPKT[$Aborted] or ILLEGALPKT — has not yet been read.
|
|
232
|
+
// Leaving it on the link corrupts the next evaluation: it becomes the first
|
|
233
|
+
// packet seen by the next DrainToEvalResult call, which resolves immediately
|
|
234
|
+
// with $Aborted and leaves the real response on the link — permanently
|
|
235
|
+
// degrading the session and disabling subsequent interrupts.
|
|
236
|
+
//
|
|
237
|
+
// Reads and discards packets until RETURNPKT, RETURNEXPRPKT, ILLEGALPKT, or a
|
|
238
|
+
// 10-second wall-clock deadline. Intermediate packets (ENDDLGPKT, TEXTPKT,
|
|
239
|
+
// MESSAGEPKT, MENUPKT, etc.) are silently consumed.
|
|
240
|
+
// ---------------------------------------------------------------------------
|
|
241
|
+
static void drainDialogAbortResponse(WSLINK lp) {
|
|
242
|
+
const auto deadline =
|
|
243
|
+
std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
|
244
|
+
while (std::chrono::steady_clock::now() < deadline) {
|
|
245
|
+
// Poll until data is available or the deadline passes.
|
|
246
|
+
while (std::chrono::steady_clock::now() < deadline) {
|
|
247
|
+
if (WSReady(lp)) break;
|
|
248
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
|
249
|
+
}
|
|
250
|
+
if (!WSReady(lp)) return; // timed out — give up
|
|
251
|
+
int pkt = WSNextPacket(lp);
|
|
252
|
+
if (pkt == RETURNPKT || pkt == RETURNEXPRPKT) {
|
|
253
|
+
WSNewPacket(lp);
|
|
254
|
+
return; // outer-eval abort response consumed — link is clean
|
|
255
|
+
}
|
|
256
|
+
if (pkt == ILLEGALPKT || pkt == 0) {
|
|
257
|
+
WSClearError(lp);
|
|
258
|
+
WSNewPacket(lp);
|
|
259
|
+
return; // link reset — clean
|
|
260
|
+
}
|
|
261
|
+
WSNewPacket(lp); // discard intermediate packet (ENDDLGPKT, MENUPKT, …)
|
|
262
|
+
}
|
|
263
|
+
}
|
|
264
|
+
|
|
225
265
|
// ---------------------------------------------------------------------------
|
|
226
266
|
// DrainToEvalResult — consume all packets for one cell, capturing Print[]
|
|
227
267
|
// output and messages. Blocks until RETURNPKT. Thread-pool thread only.
|
|
@@ -273,19 +313,32 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
273
313
|
WSReleaseString(lp, s);
|
|
274
314
|
static const std::string NL = "\\012";
|
|
275
315
|
std::string msg;
|
|
316
|
+
// Extract the message starting from the Symbol::tag position.
|
|
317
|
+
// Wolfram's TEXTPKT for MESSAGEPKT may contain multiple \012-separated
|
|
318
|
+
// sections (e.g. the tag on one line, the body on subsequent lines).
|
|
319
|
+
// Old code stopped at the first \012 after ::, which truncated long
|
|
320
|
+
// messages like NIntegrate::ncvb to just their tag.
|
|
321
|
+
// Fixed: take the whole text from the start of the Symbol name,
|
|
322
|
+
// stripping only leading/trailing \012 groups, and replacing
|
|
323
|
+
// internal \012 with spaces so the full message is one readable line.
|
|
276
324
|
auto dc = text.find("::");
|
|
325
|
+
size_t raw_start = 0;
|
|
277
326
|
if (dc != std::string::npos) {
|
|
278
327
|
auto nl_before = text.rfind(NL, dc);
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
328
|
+
raw_start = (nl_before != std::string::npos) ? nl_before + 4 : 0;
|
|
329
|
+
}
|
|
330
|
+
// Strip trailing \012 sequences from the raw text
|
|
331
|
+
std::string raw = text.substr(raw_start);
|
|
332
|
+
while (raw.size() >= 4 && raw.compare(raw.size() - 4, 4, NL) == 0)
|
|
333
|
+
raw.resize(raw.size() - 4);
|
|
334
|
+
// Strip leading spaces
|
|
335
|
+
size_t sp = 0;
|
|
336
|
+
while (sp < raw.size() && raw[sp] == ' ') ++sp;
|
|
337
|
+
raw = raw.substr(sp);
|
|
338
|
+
// Replace all remaining \012 with a single space
|
|
339
|
+
for (size_t i = 0; i < raw.size(); ) {
|
|
340
|
+
if (raw.compare(i, 4, NL) == 0) { msg += ' '; i += 4; }
|
|
341
|
+
else { msg += raw[i++]; }
|
|
289
342
|
}
|
|
290
343
|
if (!forDialog) {
|
|
291
344
|
r.messages.push_back(msg);
|
|
@@ -680,10 +733,14 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
680
733
|
// The kernel may be slow to respond (or send RETURNPKT[$Aborted]
|
|
681
734
|
// without a preceding ENDDLGPKT); bail out proactively to avoid
|
|
682
735
|
// spinning forever.
|
|
736
|
+
// CRITICAL: always drain the link before returning. If we exit
|
|
737
|
+
// here before the kernel sends its RETURNPKT[$Aborted], that
|
|
738
|
+
// response stays on the link and corrupts the next evaluation.
|
|
683
739
|
if (opts && opts->abortFlag && opts->abortFlag->load()) {
|
|
684
740
|
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
685
741
|
r.result = WExpr::mkSymbol("System`$Aborted");
|
|
686
742
|
r.aborted = true;
|
|
743
|
+
drainDialogAbortResponse(lp); // consume pending abort response
|
|
687
744
|
return r;
|
|
688
745
|
}
|
|
689
746
|
|
|
@@ -821,6 +878,7 @@ static EvalResult DrainToEvalResult(WSLINK lp, EvalOptions* opts = nullptr) {
|
|
|
821
878
|
if (opts->dialogOpen) opts->dialogOpen->store(false);
|
|
822
879
|
r.result = WExpr::mkSymbol("System`$Aborted");
|
|
823
880
|
r.aborted = true;
|
|
881
|
+
drainDialogAbortResponse(lp); // consume pending abort response
|
|
824
882
|
return r;
|
|
825
883
|
}
|
|
826
884
|
if (opts && opts->dialogPending && opts->dialogPending->load()) {
|
|
@@ -1037,6 +1095,7 @@ public:
|
|
|
1037
1095
|
std::string expr,
|
|
1038
1096
|
EvalOptions opts,
|
|
1039
1097
|
std::atomic<bool>& abortFlag,
|
|
1098
|
+
std::atomic<bool>& workerReadingLink,
|
|
1040
1099
|
std::function<void()> completionCb,
|
|
1041
1100
|
int64_t cellIndex,
|
|
1042
1101
|
bool interactive = false)
|
|
@@ -1047,6 +1106,7 @@ public:
|
|
|
1047
1106
|
interactive_(interactive),
|
|
1048
1107
|
opts_(std::move(opts)),
|
|
1049
1108
|
abortFlag_(abortFlag),
|
|
1109
|
+
workerReadingLink_(workerReadingLink),
|
|
1050
1110
|
completionCb_(std::move(completionCb)),
|
|
1051
1111
|
cellIndex_(cellIndex)
|
|
1052
1112
|
{}
|
|
@@ -1072,10 +1132,12 @@ public:
|
|
|
1072
1132
|
WSFlush (lp_);
|
|
1073
1133
|
}
|
|
1074
1134
|
if (!sent) {
|
|
1135
|
+
workerReadingLink_.store(false, std::memory_order_release);
|
|
1075
1136
|
SetError("Failed to send packet to kernel");
|
|
1076
1137
|
} else {
|
|
1077
1138
|
opts_.interactive = interactive_;
|
|
1078
1139
|
result_ = DrainToEvalResult(lp_, &opts_);
|
|
1140
|
+
workerReadingLink_.store(false, std::memory_order_release); // lp_ no longer in use
|
|
1079
1141
|
if (!interactive_) {
|
|
1080
1142
|
// EvaluatePacket mode: kernel never sends INPUTNAMEPKT/OUTPUTNAMEPKT,
|
|
1081
1143
|
// so stamp the pre-captured counter and derive outputName manually.
|
|
@@ -1160,6 +1222,7 @@ private:
|
|
|
1160
1222
|
std::string expr_;
|
|
1161
1223
|
EvalOptions opts_;
|
|
1162
1224
|
std::atomic<bool>& abortFlag_;
|
|
1225
|
+
std::atomic<bool>& workerReadingLink_;
|
|
1163
1226
|
std::function<void()> completionCb_;
|
|
1164
1227
|
int64_t cellIndex_;
|
|
1165
1228
|
bool interactive_;
|
|
@@ -1182,10 +1245,12 @@ public:
|
|
|
1182
1245
|
InstanceMethod<&WstpSession::ExitDialog> ("exitDialog"),
|
|
1183
1246
|
InstanceMethod<&WstpSession::Interrupt> ("interrupt"),
|
|
1184
1247
|
InstanceMethod<&WstpSession::Abort> ("abort"),
|
|
1248
|
+
InstanceMethod<&WstpSession::CloseAllDialogs> ("closeAllDialogs"),
|
|
1185
1249
|
InstanceMethod<&WstpSession::CreateSubsession>("createSubsession"),
|
|
1186
1250
|
InstanceMethod<&WstpSession::Close> ("close"),
|
|
1187
1251
|
InstanceAccessor<&WstpSession::IsOpen> ("isOpen"),
|
|
1188
1252
|
InstanceAccessor<&WstpSession::IsDialogOpen> ("isDialogOpen"),
|
|
1253
|
+
InstanceAccessor<&WstpSession::IsReady> ("isReady"),
|
|
1189
1254
|
});
|
|
1190
1255
|
|
|
1191
1256
|
Napi::FunctionReference* ctor = new Napi::FunctionReference();
|
|
@@ -1401,12 +1466,14 @@ public:
|
|
|
1401
1466
|
bool evalInteractive = (item.interactiveOverride == -1)
|
|
1402
1467
|
? interactiveMode_
|
|
1403
1468
|
: (item.interactiveOverride == 1);
|
|
1469
|
+
workerReadingLink_.store(true, std::memory_order_release);
|
|
1404
1470
|
auto* worker = new EvaluateWorker(
|
|
1405
1471
|
std::move(item.deferred),
|
|
1406
1472
|
lp_,
|
|
1407
1473
|
std::move(item.expr),
|
|
1408
1474
|
std::move(item.opts),
|
|
1409
1475
|
abortFlag_,
|
|
1476
|
+
workerReadingLink_,
|
|
1410
1477
|
[this]() { busy_.store(false); MaybeStartNext(); },
|
|
1411
1478
|
nextLine_.fetch_add(1),
|
|
1412
1479
|
evalInteractive
|
|
@@ -1419,10 +1486,11 @@ public:
|
|
|
1419
1486
|
void StartSubIdleWorker(QueuedSubIdle item) {
|
|
1420
1487
|
struct SubIdleWorker : public Napi::AsyncWorker {
|
|
1421
1488
|
SubIdleWorker(Napi::Promise::Deferred d, WSLINK lp, std::string expr,
|
|
1489
|
+
std::atomic<bool>& workerReadingLink,
|
|
1422
1490
|
std::function<void()> done)
|
|
1423
1491
|
: Napi::AsyncWorker(d.Env()),
|
|
1424
1492
|
deferred_(std::move(d)), lp_(lp), expr_(std::move(expr)),
|
|
1425
|
-
done_(std::move(done)) {}
|
|
1493
|
+
workerReadingLink_(workerReadingLink), done_(std::move(done)) {}
|
|
1426
1494
|
|
|
1427
1495
|
void Execute() override {
|
|
1428
1496
|
if (!WSPutFunction(lp_, "EvaluatePacket", 1) ||
|
|
@@ -1430,10 +1498,12 @@ public:
|
|
|
1430
1498
|
!WSPutUTF8String(lp_, (const unsigned char *)expr_.c_str(), (int)expr_.size()) ||
|
|
1431
1499
|
!WSEndPacket(lp_) ||
|
|
1432
1500
|
!WSFlush(lp_)) {
|
|
1501
|
+
workerReadingLink_.store(false, std::memory_order_release);
|
|
1433
1502
|
SetError("sub (idle): failed to send EvaluatePacket");
|
|
1434
1503
|
return;
|
|
1435
1504
|
}
|
|
1436
1505
|
result_ = DrainToEvalResult(lp_);
|
|
1506
|
+
workerReadingLink_.store(false, std::memory_order_release); // lp_ no longer in use
|
|
1437
1507
|
}
|
|
1438
1508
|
void OnOK() override {
|
|
1439
1509
|
Napi::Env env = Env();
|
|
@@ -1456,11 +1526,14 @@ public:
|
|
|
1456
1526
|
Napi::Promise::Deferred deferred_;
|
|
1457
1527
|
WSLINK lp_;
|
|
1458
1528
|
std::string expr_;
|
|
1529
|
+
std::atomic<bool>& workerReadingLink_;
|
|
1459
1530
|
std::function<void()> done_;
|
|
1460
1531
|
EvalResult result_;
|
|
1461
1532
|
};
|
|
1462
1533
|
|
|
1534
|
+
workerReadingLink_.store(true, std::memory_order_release);
|
|
1463
1535
|
(new SubIdleWorker(std::move(item.deferred), lp_, std::move(item.expr),
|
|
1536
|
+
workerReadingLink_,
|
|
1464
1537
|
[this]() { busy_.store(false); MaybeStartNext(); }))->Queue();
|
|
1465
1538
|
}
|
|
1466
1539
|
|
|
@@ -1521,6 +1594,15 @@ public:
|
|
|
1521
1594
|
"no dialog subsession is open").Value());
|
|
1522
1595
|
return promise;
|
|
1523
1596
|
}
|
|
1597
|
+
// Stale-state guard: dialogOpen_=true but the drain loop has already
|
|
1598
|
+
// exited (busy_=false). Nobody will service the queue, so resolve
|
|
1599
|
+
// immediately and clean up rather than enqueuing a hanging request.
|
|
1600
|
+
if (!busy_.load()) {
|
|
1601
|
+
FlushDialogQueueWithError("dialog closed: session idle");
|
|
1602
|
+
dialogOpen_.store(false);
|
|
1603
|
+
deferred.Resolve(env.Null());
|
|
1604
|
+
return promise;
|
|
1605
|
+
}
|
|
1524
1606
|
// Build "Return[]" or "Return[retVal]" as the exit expression.
|
|
1525
1607
|
std::string exitExpr = "Return[]";
|
|
1526
1608
|
if (info.Length() >= 1 && info[0].IsString())
|
|
@@ -1649,10 +1731,37 @@ public:
|
|
|
1649
1731
|
// spurious RETURNPKT[$Aborted] that would corrupt the next evaluation.
|
|
1650
1732
|
if (!busy_.load()) return Napi::Boolean::New(env, false);
|
|
1651
1733
|
abortFlag_.store(true);
|
|
1734
|
+
// Flush any queued dialogEval/exitDialog requests so their promises
|
|
1735
|
+
// reject immediately instead of hanging forever.
|
|
1736
|
+
FlushDialogQueueWithError("abort");
|
|
1737
|
+
dialogOpen_.store(false);
|
|
1652
1738
|
int ok = WSPutMessage(lp_, WSAbortMessage);
|
|
1653
1739
|
return Napi::Boolean::New(env, ok != 0);
|
|
1654
1740
|
}
|
|
1655
1741
|
|
|
1742
|
+
// -----------------------------------------------------------------------
|
|
1743
|
+
// closeAllDialogs() → boolean
|
|
1744
|
+
//
|
|
1745
|
+
// Unconditionally resets all dialog state on the JS side:
|
|
1746
|
+
// • Drains dialogQueue_, rejecting every pending dialogEval/exitDialog
|
|
1747
|
+
// promise with an error (so callers don't hang).
|
|
1748
|
+
// • Clears dialogOpen_ so isDialogOpen returns false.
|
|
1749
|
+
//
|
|
1750
|
+
// This does NOT send any packet to the kernel — it only fixes the Node
|
|
1751
|
+
// side bookkeeping. Use it in error-recovery paths (before abort() or
|
|
1752
|
+
// after an unexpected kernel state change) to guarantee clean state.
|
|
1753
|
+
//
|
|
1754
|
+
// Returns true if dialogOpen_ was set before the call (i.e. something
|
|
1755
|
+
// was actually cleaned up), false if it was already clear.
|
|
1756
|
+
// -----------------------------------------------------------------------
|
|
1757
|
+
Napi::Value CloseAllDialogs(const Napi::CallbackInfo& info) {
|
|
1758
|
+
Napi::Env env = info.Env();
|
|
1759
|
+
bool wasOpen = dialogOpen_.load();
|
|
1760
|
+
FlushDialogQueueWithError("dialog closed by closeAllDialogs");
|
|
1761
|
+
dialogOpen_.store(false);
|
|
1762
|
+
return Napi::Boolean::New(env, wasOpen);
|
|
1763
|
+
}
|
|
1764
|
+
|
|
1656
1765
|
// -----------------------------------------------------------------------
|
|
1657
1766
|
// createSubsession(kernelPath?) → WstpSession
|
|
1658
1767
|
//
|
|
@@ -1690,6 +1799,23 @@ public:
|
|
|
1690
1799
|
return Napi::Boolean::New(info.Env(), dialogOpen_.load());
|
|
1691
1800
|
}
|
|
1692
1801
|
|
|
1802
|
+
// -----------------------------------------------------------------------
|
|
1803
|
+
// isReady (read-only accessor)
|
|
1804
|
+
// true iff the session is open, the kernel has no active evaluation
|
|
1805
|
+
// (busy_ is false), no dialog is open, and the eval + sub queues are
|
|
1806
|
+
// both empty — i.e. the next evaluate() will start immediately.
|
|
1807
|
+
// All reads are on the JS main thread (same thread that writes open_ and
|
|
1808
|
+
// the queues), so no extra locking is needed.
|
|
1809
|
+
// -----------------------------------------------------------------------
|
|
1810
|
+
Napi::Value IsReady(const Napi::CallbackInfo& info) {
|
|
1811
|
+
return Napi::Boolean::New(info.Env(),
|
|
1812
|
+
open_
|
|
1813
|
+
&& !busy_.load(std::memory_order_relaxed)
|
|
1814
|
+
&& !dialogOpen_.load(std::memory_order_relaxed)
|
|
1815
|
+
&& queue_.empty()
|
|
1816
|
+
&& subIdleQueue_.empty());
|
|
1817
|
+
}
|
|
1818
|
+
|
|
1693
1819
|
private:
|
|
1694
1820
|
// Queue entry — one pending evaluate() call.
|
|
1695
1821
|
// interactiveOverride: -1 = use session default, 0 = force batch, 1 = force interactive
|
|
@@ -1700,14 +1826,58 @@ private:
|
|
|
1700
1826
|
int interactiveOverride = -1;
|
|
1701
1827
|
};
|
|
1702
1828
|
|
|
1829
|
+
// -----------------------------------------------------------------------
|
|
1830
|
+
// FlushDialogQueueWithError — drain dialogQueue_, rejecting every pending
|
|
1831
|
+
// promise with errMsg. Caller must hold no locks; this acquires
|
|
1832
|
+
// dialogMutex_ internally. Resets dialogPending_.
|
|
1833
|
+
// -----------------------------------------------------------------------
|
|
1834
|
+
void FlushDialogQueueWithError(const std::string& errMsg) {
|
|
1835
|
+
std::queue<DialogRequest> toFlush;
|
|
1836
|
+
{
|
|
1837
|
+
std::lock_guard<std::mutex> lk(dialogMutex_);
|
|
1838
|
+
std::swap(toFlush, dialogQueue_);
|
|
1839
|
+
dialogPending_.store(false);
|
|
1840
|
+
}
|
|
1841
|
+
while (!toFlush.empty()) {
|
|
1842
|
+
DialogRequest req = std::move(toFlush.front());
|
|
1843
|
+
toFlush.pop();
|
|
1844
|
+
std::string msg = errMsg;
|
|
1845
|
+
req.resolve.NonBlockingCall([msg](Napi::Env e, Napi::Function cb) {
|
|
1846
|
+
auto obj = Napi::Object::New(e);
|
|
1847
|
+
obj.Set("error", Napi::String::New(e, msg));
|
|
1848
|
+
cb.Call({obj});
|
|
1849
|
+
});
|
|
1850
|
+
req.resolve.Release();
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1703
1854
|
void CleanUp() {
|
|
1855
|
+
// If a worker thread is currently reading from lp_, calling WSClose()
|
|
1856
|
+
// on it from the JS main thread causes a concurrent-access crash
|
|
1857
|
+
// (heap-use-after-free / SIGSEGV).
|
|
1858
|
+
//
|
|
1859
|
+
// We spin on workerReadingLink_ (set false by Execute() on the background
|
|
1860
|
+
// thread) rather than busy_ (set false by OnOK/OnError on the main thread).
|
|
1861
|
+
// Spinning on busy_ from the main thread would deadlock because the main
|
|
1862
|
+
// thread's event loop is blocked — NAPI never gets to call OnOK.
|
|
1863
|
+
open_ = false; // prevent new evals from queuing during shutdown
|
|
1864
|
+
if (workerReadingLink_.load(std::memory_order_acquire) && lp_) {
|
|
1865
|
+
abortFlag_.store(true);
|
|
1866
|
+
FlushDialogQueueWithError("session closed");
|
|
1867
|
+
dialogOpen_.store(false);
|
|
1868
|
+
WSPutMessage(lp_, WSAbortMessage);
|
|
1869
|
+
const auto deadline =
|
|
1870
|
+
std::chrono::steady_clock::now() + std::chrono::seconds(10);
|
|
1871
|
+
while (workerReadingLink_.load(std::memory_order_acquire) &&
|
|
1872
|
+
std::chrono::steady_clock::now() < deadline)
|
|
1873
|
+
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
|
1874
|
+
}
|
|
1704
1875
|
if (lp_) { WSClose(lp_); lp_ = nullptr; }
|
|
1705
1876
|
if (wsEnv_) { WSDeinitialize(wsEnv_); wsEnv_ = nullptr; }
|
|
1706
1877
|
// Kill the child kernel process so it doesn't become a zombie.
|
|
1707
1878
|
// WSClose() closes the link but does not terminate the WolframKernel
|
|
1708
1879
|
// child process — without this, each session leaks a kernel.
|
|
1709
1880
|
if (kernelPid_ > 0) { kill(kernelPid_, SIGTERM); kernelPid_ = 0; }
|
|
1710
|
-
open_ = false;
|
|
1711
1881
|
}
|
|
1712
1882
|
|
|
1713
1883
|
|
|
@@ -1805,6 +1975,11 @@ private:
|
|
|
1805
1975
|
std::atomic<int64_t> nextLine_{1}; // 1-based In[n] counter for EvalResult.cellIndex
|
|
1806
1976
|
std::atomic<bool> abortFlag_{false};
|
|
1807
1977
|
std::atomic<bool> busy_{false};
|
|
1978
|
+
// Set true before queuing a worker, set false from within Execute() (background
|
|
1979
|
+
// thread) right after DrainToEvalResult returns. CleanUp() spins on this flag
|
|
1980
|
+
// rather than busy_ (which is cleared on the main thread and cannot be polled
|
|
1981
|
+
// from a main-thread spin loop).
|
|
1982
|
+
std::atomic<bool> workerReadingLink_{false};
|
|
1808
1983
|
std::mutex queueMutex_;
|
|
1809
1984
|
std::queue<QueuedEval> queue_;
|
|
1810
1985
|
std::queue<QueuedSubIdle> subIdleQueue_; // sub() — runs before queue_ items
|
package/test.js
CHANGED
|
@@ -39,6 +39,30 @@ function pollUntil(condition, timeoutMs = 3000, intervalMs = 50) {
|
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
// withTimeout — race a promise against a named deadline.
|
|
43
|
+
function withTimeout(p, ms, label) {
|
|
44
|
+
return Promise.race([
|
|
45
|
+
p,
|
|
46
|
+
new Promise((_, rej) =>
|
|
47
|
+
setTimeout(() => rej(new Error(`TIMEOUT(${ms}ms): ${label}`)), ms)),
|
|
48
|
+
]);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// mkSession — open a fresh WstpSession.
|
|
52
|
+
function mkSession() {
|
|
53
|
+
return new WstpSession(KERNEL_PATH);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// installHandler — install Interrupt→Dialog[] handler on a session.
|
|
57
|
+
// Must be done via evaluate() (EnterExpressionPacket context) so the handler
|
|
58
|
+
// fires on WSInterruptMessage; EvaluatePacket context does not receive it.
|
|
59
|
+
async function installHandler(s) {
|
|
60
|
+
await s.evaluate(
|
|
61
|
+
'Quiet[Internal`AddHandler["Interrupt", Function[{}, Dialog[]]]]',
|
|
62
|
+
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
42
66
|
// Per-test timeout (ms). Any test that does not complete within this window
|
|
43
67
|
// is failed immediately with a "TIMED OUT" error. Prevents indefinite hangs.
|
|
44
68
|
const TEST_TIMEOUT_MS = 30_000;
|
|
@@ -56,11 +80,11 @@ suiteWatchdog.unref(); // does not prevent normal exit
|
|
|
56
80
|
let passed = 0;
|
|
57
81
|
let failed = 0;
|
|
58
82
|
|
|
59
|
-
async function run(name, fn) {
|
|
83
|
+
async function run(name, fn, timeoutMs = TEST_TIMEOUT_MS) {
|
|
60
84
|
// Race the test body against a per-test timeout.
|
|
61
85
|
const timeout = new Promise((_, reject) =>
|
|
62
|
-
setTimeout(() => reject(new Error(`TIMED OUT after ${
|
|
63
|
-
|
|
86
|
+
setTimeout(() => reject(new Error(`TIMED OUT after ${timeoutMs} ms`)),
|
|
87
|
+
timeoutMs));
|
|
64
88
|
try {
|
|
65
89
|
await Promise.race([fn(), timeout]);
|
|
66
90
|
console.log(` ✓ ${name}`);
|
|
@@ -616,6 +640,353 @@ async function main() {
|
|
|
616
640
|
);
|
|
617
641
|
});
|
|
618
642
|
|
|
643
|
+
// ── 28. closeAllDialogs() is a no-op when no dialog is open ───────────
|
|
644
|
+
await run('28. closeAllDialogs() no-op when idle', async () => {
|
|
645
|
+
assert(!session.isDialogOpen, 'precondition: no dialog open');
|
|
646
|
+
const closed = session.closeAllDialogs();
|
|
647
|
+
assert(closed === false,
|
|
648
|
+
`closeAllDialogs() should return false when idle, got: ${closed}`);
|
|
649
|
+
assert(!session.isDialogOpen, 'isDialogOpen should stay false');
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
// ── 29. closeAllDialogs() rejects all queued dialogEval promises ───────
|
|
653
|
+
// Uses a subsession so the main session stays intact for tests 25 and 18.
|
|
654
|
+
// Verifies: return value = true, isDialogOpen cleared, queued promises rejected.
|
|
655
|
+
await run('29. closeAllDialogs() rejects queued dialogEval promises', async () => {
|
|
656
|
+
const sub = session.createSubsession();
|
|
657
|
+
try {
|
|
658
|
+
// Open a Dialog[] on the subsession.
|
|
659
|
+
const pEval = sub.evaluate('Dialog[]');
|
|
660
|
+
await pollUntil(() => sub.isDialogOpen);
|
|
661
|
+
assert(sub.isDialogOpen, 'dialog should be open');
|
|
662
|
+
|
|
663
|
+
// Queue two dialogEval() calls — neither will be serviced before
|
|
664
|
+
// closeAllDialogs() runs (the JS event loop hasn't yielded yet).
|
|
665
|
+
const pe1 = sub.dialogEval('"expr-1"')
|
|
666
|
+
.then(() => 'resolved').catch(e => 'rejected:' + e.message);
|
|
667
|
+
const pe2 = sub.dialogEval('"expr-2"')
|
|
668
|
+
.then(() => 'resolved').catch(e => 'rejected:' + e.message);
|
|
669
|
+
|
|
670
|
+
// closeAllDialogs() should flush both and return true.
|
|
671
|
+
const closed = sub.closeAllDialogs();
|
|
672
|
+
assert(closed === true,
|
|
673
|
+
`closeAllDialogs() should return true when dialog was open, got: ${closed}`);
|
|
674
|
+
assert(!sub.isDialogOpen,
|
|
675
|
+
'isDialogOpen should be false after closeAllDialogs()');
|
|
676
|
+
|
|
677
|
+
// Both queued promises must reject immediately.
|
|
678
|
+
const [r1, r2] = await Promise.all([pe1, pe2]);
|
|
679
|
+
assert(r1.startsWith('rejected:'),
|
|
680
|
+
`pe1 should have rejected, got: ${r1}`);
|
|
681
|
+
assert(r2.startsWith('rejected:'),
|
|
682
|
+
`pe2 should have rejected, got: ${r2}`);
|
|
683
|
+
|
|
684
|
+
// Abort the subsession to unstick the kernel (still inside Dialog[]).
|
|
685
|
+
sub.abort();
|
|
686
|
+
const ra = await pEval;
|
|
687
|
+
assert(ra.aborted === true,
|
|
688
|
+
`subsession evaluate should resolve with aborted=true, got: ${JSON.stringify(ra.aborted)}`);
|
|
689
|
+
} finally {
|
|
690
|
+
sub.close();
|
|
691
|
+
}
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
// ── P1: Pause[8] ignores interrupt ────────────────────────────────────
|
|
695
|
+
// Expected: interrupt() returns true but isDialogOpen stays false within
|
|
696
|
+
// 2500ms because Pause[] ignores WSInterruptMessage during a sleep.
|
|
697
|
+
// This test documents the fundamental limitation: Dynamic cannot read a
|
|
698
|
+
// live variable while Pause[N] is running.
|
|
699
|
+
await run('P1: Pause[8] ignores interrupt within 2500ms', async () => {
|
|
700
|
+
const s = mkSession();
|
|
701
|
+
try {
|
|
702
|
+
await installHandler(s);
|
|
703
|
+
|
|
704
|
+
let evalDone = false;
|
|
705
|
+
const mainProm = s.evaluate(
|
|
706
|
+
'pP1 = 0; Pause[8]; pP1 = 1; "p1-done"',
|
|
707
|
+
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
708
|
+
).then(() => { evalDone = true; });
|
|
709
|
+
|
|
710
|
+
await sleep(300);
|
|
711
|
+
|
|
712
|
+
const sent = s.interrupt();
|
|
713
|
+
assert(sent === true, 'interrupt() should return true mid-eval');
|
|
714
|
+
|
|
715
|
+
const t0 = Date.now();
|
|
716
|
+
while (!s.isDialogOpen && Date.now() - t0 < 2500) await sleep(25);
|
|
717
|
+
|
|
718
|
+
const dlgOpened = s.isDialogOpen;
|
|
719
|
+
assert(!dlgOpened, 'Pause[8] should NOT open a dialog within 2500ms');
|
|
720
|
+
|
|
721
|
+
// After the 2500ms window the interrupt may still be queued — the kernel
|
|
722
|
+
// will fire Dialog[] once Pause[] releases. Abort to unstick the eval
|
|
723
|
+
// rather than waiting for it to return on its own (which could take forever
|
|
724
|
+
// if Dialog[] opens and nobody services it).
|
|
725
|
+
s.abort();
|
|
726
|
+
await mainProm; // resolves immediately after abort()
|
|
727
|
+
} finally {
|
|
728
|
+
s.close();
|
|
729
|
+
}
|
|
730
|
+
}, 20_000);
|
|
731
|
+
|
|
732
|
+
// ── P2: Pause[0.3] loop — interrupt opens Dialog and dialogEval works ──
|
|
733
|
+
// Expected: interrupt during a short Pause[] loop opens a Dialog[],
|
|
734
|
+
// dialogEval can read the live variable, and exitDialog resumes the loop.
|
|
735
|
+
await run('P2: Pause[0.3] loop — interrupt opens Dialog and dialogEval succeeds', async () => {
|
|
736
|
+
const s = mkSession();
|
|
737
|
+
try {
|
|
738
|
+
await installHandler(s);
|
|
739
|
+
|
|
740
|
+
let evalDone = false;
|
|
741
|
+
const mainProm = s.evaluate(
|
|
742
|
+
'Do[nP2 = k; Pause[0.3], {k, 1, 30}]; "p2-done"',
|
|
743
|
+
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
744
|
+
).then(() => { evalDone = true; });
|
|
745
|
+
|
|
746
|
+
await sleep(500);
|
|
747
|
+
|
|
748
|
+
s.interrupt();
|
|
749
|
+
try { await pollUntil(() => s.isDialogOpen, 3000); }
|
|
750
|
+
catch (_) { throw new Error('Dialog never opened — interrupt not working with Pause[0.3]'); }
|
|
751
|
+
|
|
752
|
+
const val = await withTimeout(s.dialogEval('nP2'), 5000, 'dialogEval nP2');
|
|
753
|
+
assert(val && typeof val.value === 'number' && val.value >= 1,
|
|
754
|
+
`expected nP2 >= 1, got ${JSON.stringify(val)}`);
|
|
755
|
+
|
|
756
|
+
await s.exitDialog();
|
|
757
|
+
await withTimeout(mainProm, 15_000, 'P2 main eval');
|
|
758
|
+
} finally {
|
|
759
|
+
try { s.abort(); } catch (_) {}
|
|
760
|
+
s.close();
|
|
761
|
+
}
|
|
762
|
+
}, 30_000);
|
|
763
|
+
|
|
764
|
+
// ── P3: dialogEval timeout — kernel state after (diagnostic) ───────────
|
|
765
|
+
// Simulates the extension failure: dialogEval times out without exitDialog.
|
|
766
|
+
// Verifies the kernel is NOT permanently broken by a timeout alone.
|
|
767
|
+
// Always passes — records the observed behaviour.
|
|
768
|
+
await run('P3: dialogEval timeout — kernel still recovers via exitDialog', async () => {
|
|
769
|
+
const s = mkSession();
|
|
770
|
+
try {
|
|
771
|
+
await installHandler(s);
|
|
772
|
+
|
|
773
|
+
let evalDone = false;
|
|
774
|
+
const mainProm = s.evaluate(
|
|
775
|
+
'Do[nP3 = k; Pause[0.3], {k, 1, 200}]; "p3-done"',
|
|
776
|
+
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
777
|
+
).then(() => { evalDone = true; });
|
|
778
|
+
|
|
779
|
+
await sleep(500);
|
|
780
|
+
|
|
781
|
+
s.interrupt();
|
|
782
|
+
try { await pollUntil(() => s.isDialogOpen, 3000); }
|
|
783
|
+
catch (_) { throw new Error('Dialog #1 never opened'); }
|
|
784
|
+
|
|
785
|
+
// Simulate a timed-out dialogEval (abandon it at 200ms)
|
|
786
|
+
try { await withTimeout(s.dialogEval('nP3'), 200, 'deliberate-timeout'); } catch (_) {}
|
|
787
|
+
|
|
788
|
+
// Attempt exitDialog — should succeed
|
|
789
|
+
let exitOk = false;
|
|
790
|
+
try { await withTimeout(s.exitDialog(), 2000, 'exitDialog after timeout'); exitOk = true; }
|
|
791
|
+
catch (_) {}
|
|
792
|
+
|
|
793
|
+
// Attempt a second interrupt to confirm kernel state
|
|
794
|
+
await sleep(400);
|
|
795
|
+
s.interrupt();
|
|
796
|
+
let dlg2 = false;
|
|
797
|
+
const t2 = Date.now();
|
|
798
|
+
while (!s.isDialogOpen && Date.now() - t2 < 3000) await sleep(30);
|
|
799
|
+
dlg2 = s.isDialogOpen;
|
|
800
|
+
|
|
801
|
+
if (dlg2) {
|
|
802
|
+
await withTimeout(s.dialogEval('nP3'), 4000, 'dialogEval #2').catch(() => {});
|
|
803
|
+
await s.exitDialog().catch(() => {});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
if (!evalDone) { try { await withTimeout(mainProm, 20_000, 'P3 loop'); } catch (_) {} }
|
|
807
|
+
|
|
808
|
+
// Diagnostic: document observed outcome but do not hard-fail on dlg2
|
|
809
|
+
if (!exitOk) {
|
|
810
|
+
console.log(` P3 note: exitDialog failed, dlg2=${dlg2} (expected=false for unfixed build)`);
|
|
811
|
+
}
|
|
812
|
+
// Test passes unconditionally.
|
|
813
|
+
} finally {
|
|
814
|
+
try { s.abort(); } catch (_) {}
|
|
815
|
+
s.close();
|
|
816
|
+
}
|
|
817
|
+
}, 45_000);
|
|
818
|
+
|
|
819
|
+
// ── P4: abort() after stuck dialog — session stays alive ────────────────
|
|
820
|
+
// Note: abort() sends WSAbortMessage which resets the Wolfram kernel's
|
|
821
|
+
// Internal`AddHandler["Interrupt", ...] registration. Subsequent interrupts
|
|
822
|
+
// therefore do not open a new Dialog[]; that is expected, not a bug.
|
|
823
|
+
// What this test verifies: after abort() the session is NOT dead —
|
|
824
|
+
// evaluate() still works so the extension can queue more cells.
|
|
825
|
+
await run('P4: abort() after stuck dialog — session can still evaluate', async () => {
|
|
826
|
+
const s = mkSession();
|
|
827
|
+
try {
|
|
828
|
+
await installHandler(s);
|
|
829
|
+
|
|
830
|
+
const mainProm = s.evaluate(
|
|
831
|
+
'Do[nP4=k; Pause[0.3], {k,1,200}]; "p4-done"',
|
|
832
|
+
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
833
|
+
).catch(() => {});
|
|
834
|
+
|
|
835
|
+
await sleep(500);
|
|
836
|
+
|
|
837
|
+
// Trigger a dialog, force dialogEval timeout, then abort
|
|
838
|
+
s.interrupt();
|
|
839
|
+
try { await pollUntil(() => s.isDialogOpen, 3000); }
|
|
840
|
+
catch (_) { throw new Error('Dialog #1 never opened'); }
|
|
841
|
+
|
|
842
|
+
try { await withTimeout(s.dialogEval('nP4'), 300, 'deliberate'); } catch (_) {}
|
|
843
|
+
try { s.abort(); } catch (_) {}
|
|
844
|
+
|
|
845
|
+
// Let execute() drain the abort response and OnOK fire
|
|
846
|
+
const tAbort = Date.now();
|
|
847
|
+
while (s.isDialogOpen && Date.now() - tAbort < 5000) await sleep(50);
|
|
848
|
+
try { await withTimeout(mainProm, 5000, 'P4 abort settle'); } catch (_) {}
|
|
849
|
+
await sleep(300);
|
|
850
|
+
|
|
851
|
+
// KEY assertion: the session is still functional for evaluate()
|
|
852
|
+
// (abort() must NOT permanently close the link)
|
|
853
|
+
const r = await withTimeout(
|
|
854
|
+
s.evaluate('"session-alive-after-abort"'),
|
|
855
|
+
8000, 'post-abort evaluate'
|
|
856
|
+
);
|
|
857
|
+
assert(r && r.result && r.result.value === 'session-alive-after-abort',
|
|
858
|
+
`evaluate() after abort returned unexpected result: ${JSON.stringify(r)}`);
|
|
859
|
+
} finally {
|
|
860
|
+
try { s.abort(); } catch (_) {}
|
|
861
|
+
s.close();
|
|
862
|
+
}
|
|
863
|
+
}, 30_000);
|
|
864
|
+
|
|
865
|
+
// ── P5: closeAllDialogs()+abort() recovery → reinstall handler → new dialog works
|
|
866
|
+
// closeAllDialogs() is designed to be paired with abort(). It rejects all
|
|
867
|
+
// pending dialogEval() promises (JS-side), while abort() signals the kernel.
|
|
868
|
+
// After recovery the interrupt handler must be reinstalled because abort()
|
|
869
|
+
// clears Wolfram's Internal`AddHandler["Interrupt",...] registration.
|
|
870
|
+
await run('P5: closeAllDialogs()+abort() recovery — new dialog works after reinstallHandler', async () => {
|
|
871
|
+
const s = mkSession();
|
|
872
|
+
try {
|
|
873
|
+
await installHandler(s);
|
|
874
|
+
|
|
875
|
+
const mainProm = s.evaluate(
|
|
876
|
+
'Do[nP5 = k; Pause[0.3], {k, 1, 200}]; "p5-done"',
|
|
877
|
+
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
878
|
+
).catch(() => {});
|
|
879
|
+
|
|
880
|
+
await sleep(500);
|
|
881
|
+
|
|
882
|
+
// ── Phase 1: open dialog #1, call closeAllDialogs()+abort() ──────
|
|
883
|
+
s.interrupt();
|
|
884
|
+
try { await pollUntil(() => s.isDialogOpen, 3000); }
|
|
885
|
+
catch (_) { throw new Error('Dialog #1 never opened'); }
|
|
886
|
+
|
|
887
|
+
// Start a dialogEval — closeAllDialogs() will reject it synchronously.
|
|
888
|
+
const de1 = s.dialogEval('nP5').catch(() => {});
|
|
889
|
+
try { s.closeAllDialogs(); } catch (_) {}
|
|
890
|
+
await de1; // resolves immediately (rejected by closeAllDialogs)
|
|
891
|
+
await sleep(100);
|
|
892
|
+
try { s.abort(); } catch (_) {}
|
|
893
|
+
await mainProm; // should resolve promptly after abort()
|
|
894
|
+
|
|
895
|
+
// ── Phase 2: reinstall handler, start new loop, interrupt → dialog #2 ──
|
|
896
|
+
// abort() clears Internal`AddHandler["Interrupt",...] in the kernel,
|
|
897
|
+
// so we must reinstall before the next interrupt cycle.
|
|
898
|
+
await withTimeout(installHandler(s), 8000, 'reinstall handler after abort');
|
|
899
|
+
|
|
900
|
+
let dlg2 = false;
|
|
901
|
+
const mainProm2 = s.evaluate(
|
|
902
|
+
'Do[nP5b = k; Pause[0.3], {k, 1, 200}]; "p5b-done"',
|
|
903
|
+
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
904
|
+
).catch(() => {});
|
|
905
|
+
|
|
906
|
+
await sleep(500);
|
|
907
|
+
s.interrupt();
|
|
908
|
+
try { await pollUntil(() => s.isDialogOpen, 3000); }
|
|
909
|
+
catch (_) { throw new Error('Dialog #2 never opened after closeAllDialogs()+abort() recovery'); }
|
|
910
|
+
dlg2 = true;
|
|
911
|
+
|
|
912
|
+
// Read a live variable inside the dialog.
|
|
913
|
+
const val2 = await withTimeout(s.dialogEval('nP5b'), 4000, 'dialogEval #2');
|
|
914
|
+
assert(val2 !== null && val2.value !== undefined, 'dialogEval #2 should return nP5b value');
|
|
915
|
+
|
|
916
|
+
assert(dlg2, 'after closeAllDialogs()+abort()+reinstallHandler, interrupt must open a new dialog');
|
|
917
|
+
// Abort the dialog and remaining Do loop promptly (no exitDialog needed).
|
|
918
|
+
try { s.abort(); } catch (_) {}
|
|
919
|
+
await mainProm2.catch(() => {}); // drains immediately after abort
|
|
920
|
+
} finally {
|
|
921
|
+
try { s.abort(); } catch (_) {}
|
|
922
|
+
s.close();
|
|
923
|
+
}
|
|
924
|
+
}, 60_000);
|
|
925
|
+
|
|
926
|
+
// ── P6: Simulate Dynamic + Pause[5] full scenario (diagnostic) ─────────
|
|
927
|
+
// Reproduces: n=RandomInteger[100]; Pause[5] with interrupt every 2.5s.
|
|
928
|
+
// Pause[5] is expected to block all interrupts. Test always passes —
|
|
929
|
+
// it documents whether reads succeed despite long Pause.
|
|
930
|
+
await run('P6: Pause[5] + interrupt cycle — dynamic read diagnostic', async () => {
|
|
931
|
+
const s = mkSession();
|
|
932
|
+
try {
|
|
933
|
+
await installHandler(s);
|
|
934
|
+
|
|
935
|
+
let evalDone = false;
|
|
936
|
+
const mainProm = s.evaluate(
|
|
937
|
+
'n = RandomInteger[100]; Pause[5]; "p6-done"',
|
|
938
|
+
{ onDialogBegin: () => {}, onDialogEnd: () => {} }
|
|
939
|
+
).then(() => { evalDone = true; });
|
|
940
|
+
|
|
941
|
+
await sleep(200);
|
|
942
|
+
|
|
943
|
+
const INTERRUPT_WAIT_MS = 2500;
|
|
944
|
+
const DIALOG_EVAL_TIMEOUT_MS = 8000;
|
|
945
|
+
let dialogReadSucceeded = false;
|
|
946
|
+
let pauseIgnoredInterrupt = false;
|
|
947
|
+
|
|
948
|
+
for (let cycle = 1; cycle <= 5 && !evalDone; cycle++) {
|
|
949
|
+
const t0 = Date.now();
|
|
950
|
+
s.interrupt();
|
|
951
|
+
while (!s.isDialogOpen && Date.now() - t0 < INTERRUPT_WAIT_MS) await sleep(25);
|
|
952
|
+
const dlg = s.isDialogOpen;
|
|
953
|
+
|
|
954
|
+
if (!dlg) {
|
|
955
|
+
pauseIgnoredInterrupt = true;
|
|
956
|
+
await sleep(300);
|
|
957
|
+
continue;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
try {
|
|
961
|
+
const val = await withTimeout(
|
|
962
|
+
s.dialogEval('n'), DIALOG_EVAL_TIMEOUT_MS, `P6 dialogEval cycle ${cycle}`);
|
|
963
|
+
dialogReadSucceeded = true;
|
|
964
|
+
await s.exitDialog();
|
|
965
|
+
break;
|
|
966
|
+
} catch (e) {
|
|
967
|
+
let exitOk = false;
|
|
968
|
+
for (let a = 0; a < 3 && !exitOk; a++) {
|
|
969
|
+
try { await withTimeout(s.exitDialog(), 2000, `exitDialog ${a+1}`); exitOk = true; }
|
|
970
|
+
catch (_) {}
|
|
971
|
+
}
|
|
972
|
+
if (!exitOk) { try { s.abort(); } catch (_) {} await sleep(1000); break; }
|
|
973
|
+
}
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
try { await withTimeout(mainProm, 12_000, 'P6 main eval'); } catch (_) {}
|
|
977
|
+
|
|
978
|
+
// Diagnostic: always passes — log observed outcome
|
|
979
|
+
if (pauseIgnoredInterrupt && !dialogReadSucceeded) {
|
|
980
|
+
console.log(' P6 note: Pause[5] blocked all interrupts — expected behaviour');
|
|
981
|
+
} else if (dialogReadSucceeded) {
|
|
982
|
+
console.log(' P6 note: at least one read succeeded despite Pause');
|
|
983
|
+
}
|
|
984
|
+
} finally {
|
|
985
|
+
try { s.abort(); } catch (_) {}
|
|
986
|
+
s.close();
|
|
987
|
+
}
|
|
988
|
+
}, 60_000);
|
|
989
|
+
|
|
619
990
|
// ── 25. abort() while dialog is open ──────────────────────────────────
|
|
620
991
|
// Must run AFTER all other dialog tests — abort() sends WSAbortMessage
|
|
621
992
|
// which resets the WSTP link, leaving the session unusable for further
|
|
@@ -649,6 +1020,10 @@ async function main() {
|
|
|
649
1020
|
console.log();
|
|
650
1021
|
if (failed === 0) {
|
|
651
1022
|
console.log(`All ${passed} tests passed.`);
|
|
1023
|
+
// Force-exit: WSTP library may keep libuv handles alive after WSClose,
|
|
1024
|
+
// preventing the event loop from draining naturally. All assertions are
|
|
1025
|
+
// done; a clean exit(0) is safe.
|
|
1026
|
+
process.exit(0);
|
|
652
1027
|
} else {
|
|
653
1028
|
console.log(`${passed} passed, ${failed} failed.`);
|
|
654
1029
|
}
|