@alotop/dsh-matlab-bridge 0.1.6 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -6
- package/README.zh-CN.md +32 -5
- package/package.json +1 -1
- package/python/ml_driver.py +28 -6
- package/python/selftest.py +30 -0
- package/src/plugin.mjs +174 -49
package/README.md
CHANGED
|
@@ -119,10 +119,11 @@ All optional; the defaults need no configuration on a normal machine.
|
|
|
119
119
|
- id: matlab-bridge
|
|
120
120
|
name: '@alotop/dsh-matlab-bridge'
|
|
121
121
|
config:
|
|
122
|
-
pythonPath: python3.12
|
|
123
|
-
workDir: /path/to/project
|
|
124
|
-
figureDir: /tmp/figures
|
|
125
|
-
timeoutMs: 180000
|
|
122
|
+
pythonPath: python3.12 # default: first of python3, python on PATH
|
|
123
|
+
workDir: /path/to/project # default: the calling session's cwd
|
|
124
|
+
figureDir: /tmp/figures # default: <workDir>/.matlab-figures
|
|
125
|
+
timeoutMs: 180000 # default: 180000
|
|
126
|
+
shutdownEngineOnExit: true # default: true
|
|
126
127
|
```
|
|
127
128
|
|
|
128
129
|
## Tools
|
|
@@ -172,8 +173,47 @@ nothing.
|
|
|
172
173
|
### `matlab_session`
|
|
173
174
|
|
|
174
175
|
`start`, `status`, `stop`. MATLAB takes tens of seconds to start, so the session
|
|
175
|
-
stays warm and is reused.
|
|
176
|
-
|
|
176
|
+
stays warm and is reused.
|
|
177
|
+
|
|
178
|
+
`status` reports the package path, **the Python interpreter the driver runs on**,
|
|
179
|
+
whether the driver is running, whether the engine runtime has been laid out, and
|
|
180
|
+
whether MATLAB is quit on exit. The interpreter is worth showing on a machine
|
|
181
|
+
that carries several: the one that resolves first is not necessarily the one
|
|
182
|
+
setup verified, and `pythonPath` is the fix when they disagree.
|
|
183
|
+
|
|
184
|
+
## Commands
|
|
185
|
+
|
|
186
|
+
Four human entry points, for the operations that do not belong on the model's
|
|
187
|
+
tool surface:
|
|
188
|
+
|
|
189
|
+
| Command | Does |
|
|
190
|
+
|---|---|
|
|
191
|
+
| `/matlab-status` | The same report as `matlab_session status`. |
|
|
192
|
+
| `/matlab-start` | Start the persistent engine session. |
|
|
193
|
+
| `/matlab-stop` | Quit MATLAB and stop the driver. |
|
|
194
|
+
| `/matlab-setup` | Lay out the engine runtime from the local MATLAB install. |
|
|
195
|
+
|
|
196
|
+
`/matlab-setup` runs the layout script through the same unconfined subprocess
|
|
197
|
+
seam as everything else, so it can write into the installed package directory —
|
|
198
|
+
which lies outside every session workspace — without you opening a separate
|
|
199
|
+
terminal.
|
|
200
|
+
|
|
201
|
+
### Quitting MATLAB when DSH exits
|
|
202
|
+
|
|
203
|
+
`shutdownEngineOnExit` (default `true`) decides whether shutting DSH down also
|
|
204
|
+
quits MATLAB. It is on by default because the driver is a child process: killing
|
|
205
|
+
it outright leaves the MATLAB engine it started with no owner.
|
|
206
|
+
|
|
207
|
+
Two mechanisms cover both ways DSH can go away:
|
|
208
|
+
|
|
209
|
+
- a normal plugin disposal writes the shutdown request and closes the driver's
|
|
210
|
+
stdin, so the driver quits MATLAB and exits on its own;
|
|
211
|
+
- if DSH exits without disposing the plugin at all, that closed pipe reaches the
|
|
212
|
+
driver as EOF, and the driver quits MATLAB and exits anyway.
|
|
213
|
+
|
|
214
|
+
Set it to `false` to leave MATLAB running when DSH exits. Without a shared engine
|
|
215
|
+
session nothing can reattach to it, so it becomes an orphan process — useful for
|
|
216
|
+
inspecting a live session, not a way to keep one.
|
|
177
217
|
|
|
178
218
|
## How it works
|
|
179
219
|
|
package/README.zh-CN.md
CHANGED
|
@@ -94,10 +94,11 @@ npm install --no-save ./alotop-dsh-matlab-bridge-<版本>.tgz # 或装进某
|
|
|
94
94
|
- id: matlab-bridge
|
|
95
95
|
name: '@alotop/dsh-matlab-bridge'
|
|
96
96
|
config:
|
|
97
|
-
pythonPath: python3.12
|
|
98
|
-
workDir: /path/to/project
|
|
99
|
-
figureDir: /tmp/figures
|
|
100
|
-
timeoutMs: 180000
|
|
97
|
+
pythonPath: python3.12 # 默认:PATH 上第一个 python3 / python
|
|
98
|
+
workDir: /path/to/project # 默认:调用方会话的 cwd
|
|
99
|
+
figureDir: /tmp/figures # 默认:<workDir>/.matlab-figures
|
|
100
|
+
timeoutMs: 180000 # 默认:180000
|
|
101
|
+
shutdownEngineOnExit: true # 默认:true
|
|
101
102
|
```
|
|
102
103
|
|
|
103
104
|
## 工具
|
|
@@ -132,7 +133,33 @@ matlab_debug action=continue → state: completed
|
|
|
132
133
|
|
|
133
134
|
### `matlab_session`
|
|
134
135
|
|
|
135
|
-
`start`、`status`、`stop`。MATLAB
|
|
136
|
+
`start`、`status`、`stop`。MATLAB 启动要几十秒,所以会话保持热态并复用。
|
|
137
|
+
|
|
138
|
+
`status` 会报告包路径、**驱动实际使用的 Python 解释器**、驱动是否在跑、引擎运行库是否已铺开、以及退出 DSH 时是否关闭 MATLAB。解释器值得显示出来——在有多个 Python 的机器上,PATH 上先解析到的那个不一定是 setup 验证过的那个,两者不一致时 `pythonPath` 就是解药。
|
|
139
|
+
|
|
140
|
+
## 命令
|
|
141
|
+
|
|
142
|
+
四个人工入口,用于那些不该出现在模型工具面上的操作:
|
|
143
|
+
|
|
144
|
+
| 命令 | 作用 |
|
|
145
|
+
|---|---|
|
|
146
|
+
| `/matlab-status` | 与 `matlab_session status` 同一份报告 |
|
|
147
|
+
| `/matlab-start` | 启动常驻引擎会话 |
|
|
148
|
+
| `/matlab-stop` | 关闭 MATLAB 并停止驱动 |
|
|
149
|
+
| `/matlab-setup` | 从本地 MATLAB 铺开引擎运行库 |
|
|
150
|
+
|
|
151
|
+
`/matlab-setup` 走的是与其它操作相同的非受限 subprocess 通道,因此它能写入**已安装的包目录**(位于任何会话工作区之外),不需要你另开终端。
|
|
152
|
+
|
|
153
|
+
### DSH 退出时是否关闭 MATLAB
|
|
154
|
+
|
|
155
|
+
`shutdownEngineOnExit`(默认 `true`)决定关闭 DSH 时是否一并关闭 MATLAB。默认开启,因为驱动是子进程:直接杀掉它会留下一个**无人认领**的 MATLAB 引擎。
|
|
156
|
+
|
|
157
|
+
两条机制覆盖 DSH 两种退出方式:
|
|
158
|
+
|
|
159
|
+
- 正常的插件卸载会写入关闭请求并关闭驱动的 stdin,驱动自己跑完 `engine.quit()` 后退出;
|
|
160
|
+
- 如果 DSH 根本没卸载插件就退出,那条管道关闭会以 **EOF** 形式到达驱动,驱动同样退出 MATLAB 后再结束。
|
|
161
|
+
|
|
162
|
+
设为 `false` 可让 DSH 退出时保留 MATLAB。但在没有 shared engine 会话的前提下,没有任何东西能重接上去,它只会变成一个孤儿进程——适合临时查看一个活会话,不是"保留会话"的办法。
|
|
136
163
|
|
|
137
164
|
## 工作原理
|
|
138
165
|
|
package/package.json
CHANGED
package/python/ml_driver.py
CHANGED
|
@@ -386,6 +386,23 @@ def op_figure(req):
|
|
|
386
386
|
return {"ok": False, "err": "unknown figure action: %s" % action}
|
|
387
387
|
|
|
388
388
|
|
|
389
|
+
def shutdown_engine():
|
|
390
|
+
"""Quit the MATLAB engine if one is running, and forget it.
|
|
391
|
+
|
|
392
|
+
Quitting is the only thing that stops the MATLAB process: it was started by
|
|
393
|
+
this driver, and letting the driver die without this leaves MATLAB running
|
|
394
|
+
with no owner.
|
|
395
|
+
"""
|
|
396
|
+
global _engine
|
|
397
|
+
if _engine is None:
|
|
398
|
+
return
|
|
399
|
+
try:
|
|
400
|
+
_engine.quit()
|
|
401
|
+
except Exception: # noqa: BLE001
|
|
402
|
+
pass
|
|
403
|
+
_engine = None
|
|
404
|
+
|
|
405
|
+
|
|
389
406
|
def dispatch(req):
|
|
390
407
|
global _engine
|
|
391
408
|
op = req.get("op", "")
|
|
@@ -402,12 +419,7 @@ def dispatch(req):
|
|
|
402
419
|
if op == "figure":
|
|
403
420
|
return op_figure(req)
|
|
404
421
|
if op == "shutdown":
|
|
405
|
-
|
|
406
|
-
try:
|
|
407
|
-
_engine.quit()
|
|
408
|
-
except Exception: # noqa: BLE001
|
|
409
|
-
pass
|
|
410
|
-
_engine = None
|
|
422
|
+
shutdown_engine()
|
|
411
423
|
emit({"id": req.get("id"), "ok": True, "state": "shutdown"})
|
|
412
424
|
raise SystemExit(0)
|
|
413
425
|
return {"ok": False, "err": "unknown op: %s" % op}
|
|
@@ -432,6 +444,16 @@ def main():
|
|
|
432
444
|
response["id"] = req.get("id")
|
|
433
445
|
emit(response)
|
|
434
446
|
|
|
447
|
+
# stdin reached EOF: the caller is gone. This is the second half of the
|
|
448
|
+
# clean-exit story -- a parent that exits without disposing us closes this
|
|
449
|
+
# pipe, so quitting MATLAB here is what keeps a closed DSH from leaving an
|
|
450
|
+
# orphaned MATLAB behind. The notice goes to stderr because that is the
|
|
451
|
+
# diagnostic channel, and it is what a self-test can observe: the process
|
|
452
|
+
# exits either way, so only this distinguishes "quit MATLAB first" from
|
|
453
|
+
# "died and left MATLAB running".
|
|
454
|
+
print("dsh-matlab-bridge: stdin closed, quitting MATLAB", file=sys.stderr, flush=True)
|
|
455
|
+
shutdown_engine()
|
|
456
|
+
|
|
435
457
|
|
|
436
458
|
if __name__ == "__main__":
|
|
437
459
|
main()
|
package/python/selftest.py
CHANGED
|
@@ -248,6 +248,8 @@ def main():
|
|
|
248
248
|
run_figure_checks(driver, tmp)
|
|
249
249
|
if not args.no_debug:
|
|
250
250
|
run_debug_checks(driver, tmp)
|
|
251
|
+
# Last, because it ends the driver's life.
|
|
252
|
+
run_exit_checks(driver)
|
|
251
253
|
finally:
|
|
252
254
|
driver.close()
|
|
253
255
|
|
|
@@ -259,5 +261,33 @@ def main():
|
|
|
259
261
|
return 0
|
|
260
262
|
|
|
261
263
|
|
|
264
|
+
def run_exit_checks(driver):
|
|
265
|
+
"""
|
|
266
|
+
Closing the driver's stdin is what a DSH shutdown looks like from inside the
|
|
267
|
+
driver, so it must quit MATLAB and then exit on its own.
|
|
268
|
+
|
|
269
|
+
The process exits either way -- EOF ends the read loop whether or not the
|
|
270
|
+
cleanup runs -- so the assertion is the cleanup notice on stderr, not the
|
|
271
|
+
exit itself. Exiting without that notice is exactly how a MATLAB process
|
|
272
|
+
gets orphaned.
|
|
273
|
+
"""
|
|
274
|
+
driver.proc.stdin.close()
|
|
275
|
+
deadline = time.time() + 90
|
|
276
|
+
saw_cleanup = False
|
|
277
|
+
while time.time() < deadline:
|
|
278
|
+
line = driver.proc.stdout.readline()
|
|
279
|
+
if line == "":
|
|
280
|
+
break
|
|
281
|
+
if "stdin closed, quitting MATLAB" in line:
|
|
282
|
+
saw_cleanup = True
|
|
283
|
+
try:
|
|
284
|
+
code = driver.proc.wait(timeout=60)
|
|
285
|
+
except Exception as exc: # noqa: BLE001
|
|
286
|
+
check("driver quits MATLAB when its stdin closes", False, "did not exit: %r" % (exc,))
|
|
287
|
+
return
|
|
288
|
+
check("driver quits MATLAB when its stdin closes", saw_cleanup,
|
|
289
|
+
"cleanup notice=%s exit=%s" % (saw_cleanup, code))
|
|
290
|
+
|
|
291
|
+
|
|
262
292
|
if __name__ == "__main__":
|
|
263
293
|
sys.exit(main())
|
package/src/plugin.mjs
CHANGED
|
@@ -193,6 +193,123 @@ export function apply(ctx, config) {
|
|
|
193
193
|
if (typeof cwd === 'string' && cwd.length > 0) sessionCwd = cwd
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
/**
|
|
197
|
+
* Whether DSH shutting down also quits MATLAB. On by default: the driver is
|
|
198
|
+
* a child process, and killing it hard leaves the MATLAB engine it started
|
|
199
|
+
* with no owner.
|
|
200
|
+
*/
|
|
201
|
+
const shutdownOnExit = config?.shutdownEngineOnExit !== false
|
|
202
|
+
|
|
203
|
+
function setupScriptPath() {
|
|
204
|
+
return join(PACKAGE_ROOT, 'scripts', 'setup-engine.mjs')
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* Report the interpreter the driver actually runs on. A machine can carry
|
|
209
|
+
* several (a system Python and a conda one, say), and the one that resolves
|
|
210
|
+
* first is not necessarily the one that was verified during setup, so seeing
|
|
211
|
+
* it is the difference between a five-second diagnosis and a long one.
|
|
212
|
+
*/
|
|
213
|
+
async function pythonLine() {
|
|
214
|
+
try {
|
|
215
|
+
return 'python: ' + (await resolvePython())
|
|
216
|
+
} catch (error) {
|
|
217
|
+
return 'python: NOT FOUND - ' + String((error && error.message) || error)
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Shared by the `matlab_session` tool and the /matlab-status command. */
|
|
222
|
+
async function statusReport() {
|
|
223
|
+
const runtimePresent = existsSync(join(PYLIBS_PATH, 'matlab'))
|
|
224
|
+
const lines = [
|
|
225
|
+
'package: ' + PACKAGE_ROOT,
|
|
226
|
+
await pythonLine(),
|
|
227
|
+
'driver: ' + (handle === null ? 'not running' : 'running'),
|
|
228
|
+
'engine runtime: ' + (runtimePresent ? 'present' : 'MISSING'),
|
|
229
|
+
'quit MATLAB on exit: ' + (shutdownOnExit ? 'yes' : 'no'),
|
|
230
|
+
]
|
|
231
|
+
if (!runtimePresent) {
|
|
232
|
+
lines.push('', 'Lay it out with /matlab-setup, or: node ' + JSON.stringify(setupScriptPath()))
|
|
233
|
+
}
|
|
234
|
+
if (handle === null) {
|
|
235
|
+
lines.push('matlab: not started')
|
|
236
|
+
return lines.join('\n')
|
|
237
|
+
}
|
|
238
|
+
const resp = await rpc('ping', {}, 30000)
|
|
239
|
+
lines.push('matlab: ' + (resp.engineRunning ? 'running' : 'not started'))
|
|
240
|
+
return lines.join('\n')
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Shared by the `matlab_session` tool and the /matlab-stop command. */
|
|
244
|
+
async function stopEngine() {
|
|
245
|
+
if (handle === null) return 'no MATLAB session is running'
|
|
246
|
+
try {
|
|
247
|
+
await rpc('shutdown', {}, 60000)
|
|
248
|
+
} catch {
|
|
249
|
+
// The driver exits as part of shutdown, so a dropped reply is expected.
|
|
250
|
+
}
|
|
251
|
+
try {
|
|
252
|
+
handle.terminate()
|
|
253
|
+
} catch {
|
|
254
|
+
// Already gone.
|
|
255
|
+
}
|
|
256
|
+
handle = null
|
|
257
|
+
pending.clear()
|
|
258
|
+
return 'MATLAB session stopped'
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/** Shared by the `matlab_session` tool and the /matlab-start command. */
|
|
262
|
+
async function startEngine() {
|
|
263
|
+
const resp = await rpc('start', {}, timeoutMs)
|
|
264
|
+
if (resp.ok === false) return 'MATLAB failed to start: ' + resp.err
|
|
265
|
+
return 'MATLAB ' + (resp.version || '') + ' started'
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
/** Keep a command reply readable; setup output is long and mostly progress. */
|
|
269
|
+
function tailLines(text, max) {
|
|
270
|
+
const lines = text.split('\n').filter((line) => line.trim() !== '')
|
|
271
|
+
return lines.length <= max ? lines.join('\n') : lines.slice(-max).join('\n')
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Run the engine-layout script. Spawning it through `ctx.subprocess` matters:
|
|
276
|
+
* the script writes into this package's own directory, which lies outside the
|
|
277
|
+
* session workspace, and only this unconfined seam can write there — so
|
|
278
|
+
* /matlab-setup works without the user opening a separate terminal.
|
|
279
|
+
* `process.execPath` is the Node already running DSH, so no PATH lookup.
|
|
280
|
+
*/
|
|
281
|
+
async function runSetup() {
|
|
282
|
+
const script = setupScriptPath()
|
|
283
|
+
if (!existsSync(script)) throw new Error('setup script not found at ' + script)
|
|
284
|
+
const spawned = ctx.subprocess.spawn({
|
|
285
|
+
argv: [process.execPath, script],
|
|
286
|
+
cwd: PACKAGE_ROOT,
|
|
287
|
+
stdio: {
|
|
288
|
+
stdin: 'ignore',
|
|
289
|
+
stdout: { maxBytes: 64000 },
|
|
290
|
+
stderr: { maxBytes: 64000 },
|
|
291
|
+
},
|
|
292
|
+
graceMs: 10000,
|
|
293
|
+
})
|
|
294
|
+
const outcome = await spawned.done
|
|
295
|
+
const collected = spawned.collected ?? {}
|
|
296
|
+
const read = (reader) => {
|
|
297
|
+
try {
|
|
298
|
+
return reader === undefined ? '' : reader.readFrom(0).text
|
|
299
|
+
} catch {
|
|
300
|
+
return ''
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const text = [read(collected.stdout), read(collected.stderr)]
|
|
304
|
+
.filter((part) => part.length > 0)
|
|
305
|
+
.join('\n')
|
|
306
|
+
const tail = tailLines(text, 20)
|
|
307
|
+
if (outcome.exitCode !== 0) {
|
|
308
|
+
return 'setup-engine failed (exit code ' + outcome.exitCode + ')\n' + (tail || '(no output)')
|
|
309
|
+
}
|
|
310
|
+
return tail || 'setup-engine finished with no output'
|
|
311
|
+
}
|
|
312
|
+
|
|
196
313
|
function pushChatter(line) {
|
|
197
314
|
if (line.trim() === '') return
|
|
198
315
|
chatter.push(line)
|
|
@@ -451,61 +568,69 @@ export function apply(ctx, config) {
|
|
|
451
568
|
async execute(args, exec) {
|
|
452
569
|
rememberSession(exec)
|
|
453
570
|
const action = String(args.action)
|
|
454
|
-
if (action === 'stop') {
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
await rpc('shutdown', {}, 60000)
|
|
458
|
-
} catch {
|
|
459
|
-
// The driver exits as part of shutdown, so a dropped reply is expected.
|
|
460
|
-
}
|
|
461
|
-
try {
|
|
462
|
-
handle.terminate()
|
|
463
|
-
} catch {
|
|
464
|
-
// Already gone.
|
|
465
|
-
}
|
|
466
|
-
handle = null
|
|
467
|
-
pending.clear()
|
|
468
|
-
return { text: 'MATLAB session stopped' }
|
|
469
|
-
}
|
|
470
|
-
if (action === 'status') {
|
|
471
|
-
const runtimePresent = existsSync(join(PYLIBS_PATH, 'matlab'))
|
|
472
|
-
const lines = [
|
|
473
|
-
'package: ' + PACKAGE_ROOT,
|
|
474
|
-
'driver: ' + (handle === null ? 'not running' : 'running'),
|
|
475
|
-
// Name the exact command rather than a bare bin name: this package is
|
|
476
|
-
// normally installed into a DSH profile, where its bin is not on
|
|
477
|
-
// PATH, so "run dsh-matlab-bridge-setup" is not actionable.
|
|
478
|
-
'engine runtime: ' + (runtimePresent
|
|
479
|
-
? 'present'
|
|
480
|
-
: 'MISSING - run: node ' + JSON.stringify(join(PACKAGE_ROOT, 'scripts', 'setup-engine.mjs'))),
|
|
481
|
-
]
|
|
482
|
-
if (handle === null) {
|
|
483
|
-
lines.push('matlab: not started')
|
|
484
|
-
return { text: lines.join('\n') }
|
|
485
|
-
}
|
|
486
|
-
const resp = await rpc('ping', {}, 30000)
|
|
487
|
-
lines.push('matlab: ' + (resp.engineRunning ? 'running' : 'not started'))
|
|
488
|
-
return { text: lines.join('\n') }
|
|
489
|
-
}
|
|
490
|
-
if (action === 'start') {
|
|
491
|
-
const resp = await rpc('start', {}, timeoutMs)
|
|
492
|
-
if (resp.ok === false) return { text: 'MATLAB failed to start: ' + resp.err }
|
|
493
|
-
return { text: 'MATLAB ' + (resp.version || '') + ' started' }
|
|
494
|
-
}
|
|
571
|
+
if (action === 'stop') return { text: await stopEngine() }
|
|
572
|
+
if (action === 'status') return { text: await statusReport() }
|
|
573
|
+
if (action === 'start') return { text: await startEngine() }
|
|
495
574
|
return { text: 'unknown action: ' + action + ' (use start, status or stop)' }
|
|
496
575
|
},
|
|
497
576
|
})
|
|
498
577
|
|
|
578
|
+
/**
|
|
579
|
+
* Ask the driver to quit MATLAB, then close its stdin.
|
|
580
|
+
*
|
|
581
|
+
* The disposer cannot await, and killing the driver outright would orphan the
|
|
582
|
+
* MATLAB process it started, because a hard kill never runs Python's atexit.
|
|
583
|
+
* Writing the shutdown request and closing the pipe hands the clean exit to
|
|
584
|
+
* the driver instead. Returns false when there is no usable stdin, so the
|
|
585
|
+
* caller can fall back to terminate().
|
|
586
|
+
*/
|
|
587
|
+
function requestGracefulStop(active) {
|
|
588
|
+
try {
|
|
589
|
+
active.stdin.write(JSON.stringify({ id: nextId, op: 'shutdown' }) + '\n')
|
|
590
|
+
nextId += 1
|
|
591
|
+
active.stdin.end()
|
|
592
|
+
return true
|
|
593
|
+
} catch {
|
|
594
|
+
return false
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
|
|
598
|
+
/**
|
|
599
|
+
* Human entry points for the operations that do not belong on the model's
|
|
600
|
+
* tool surface. `commands` is read optionally rather than injected: a
|
|
601
|
+
* deployment without a command registry must still get the tools, which are
|
|
602
|
+
* this plugin's whole value.
|
|
603
|
+
*/
|
|
604
|
+
const commands = ctx.get('commands')
|
|
605
|
+
if (commands !== undefined) {
|
|
606
|
+
const command = (name, description, run) => ctx.effect(() => commands.register({
|
|
607
|
+
name,
|
|
608
|
+
description,
|
|
609
|
+
handler: async () => {
|
|
610
|
+
try {
|
|
611
|
+
return { kind: 'success', text: await run() }
|
|
612
|
+
} catch (error) {
|
|
613
|
+
return { kind: 'error', text: String((error && error.message) || error) }
|
|
614
|
+
}
|
|
615
|
+
},
|
|
616
|
+
}))
|
|
617
|
+
command('matlab-status', 'Show MATLAB bridge status: package, interpreter, driver, engine runtime.', statusReport)
|
|
618
|
+
command('matlab-start', 'Start the persistent MATLAB engine session.', startEngine)
|
|
619
|
+
command('matlab-stop', 'Shut down the persistent MATLAB engine session and its driver.', stopEngine)
|
|
620
|
+
command('matlab-setup', 'Lay out the MATLAB Engine runtime from the local MATLAB installation.', runSetup)
|
|
621
|
+
}
|
|
622
|
+
|
|
499
623
|
// The driver owns a MATLAB process; the mount must not leak it.
|
|
500
624
|
ctx.effect(() => () => {
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
handle.terminate()
|
|
504
|
-
} catch {
|
|
505
|
-
// Nothing left to release.
|
|
506
|
-
}
|
|
507
|
-
handle = null
|
|
508
|
-
}
|
|
625
|
+
const active = handle
|
|
626
|
+
handle = null
|
|
509
627
|
failAll('matlab-bridge plugin was stopped')
|
|
628
|
+
if (active === null) return
|
|
629
|
+
if (shutdownOnExit && requestGracefulStop(active)) return
|
|
630
|
+
try {
|
|
631
|
+
active.terminate()
|
|
632
|
+
} catch {
|
|
633
|
+
// Nothing left to release.
|
|
634
|
+
}
|
|
510
635
|
})
|
|
511
636
|
}
|