@nataliapc/mcp-openmsx 1.2.9 → 1.2.11

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.
Files changed (52) hide show
  1. package/README.md +41 -2
  2. package/bin/win-x64/mcp-openmsx-sspi-proxy.exe +0 -0
  3. package/dist/chunker.js +187 -0
  4. package/dist/embedder.js +250 -0
  5. package/dist/openmsx.js +113 -248
  6. package/dist/openmsx_windows.js +316 -0
  7. package/dist/server.js +6 -1
  8. package/dist/server_tools.js +6 -5
  9. package/dist/vectordb.js +94 -35
  10. package/package.json +16 -18
  11. package/resources/audio/chipsfmpacpr1_en.md +209 -0
  12. package/resources/audio/chipsfmpacpr2_en.md +170 -0
  13. package/resources/audio/toc.json +12 -0
  14. package/resources/book--msx-top-secret-3/MTS3-Appendix-English-Upd2.pdf +0 -0
  15. package/resources/book--msx-top-secret-3/MTS3-Complete-English.pdf +0 -0
  16. package/resources/book--msx2-technical-handbook/toc.json +1 -1
  17. package/resources/book--the-msx-red-book/Chapter1_Programmable_Peripheral_Interface.md +112 -0
  18. package/resources/book--the-msx-red-book/Chapter2_Video_Display_Processor.md +308 -0
  19. package/resources/book--the-msx-red-book/Chapter3_Programmable_Sound_Generator.md +168 -0
  20. package/resources/book--the-msx-red-book/Chapter4_ROM_BIOS.md +2528 -0
  21. package/resources/book--the-msx-red-book/Chapter5_ROM_BASIC_Interpreter.md +3975 -0
  22. package/resources/book--the-msx-red-book/Chapter6_Memory_Map.md +1963 -0
  23. package/resources/book--the-msx-red-book/Chapter7_Machine_Code_Programs.md +1238 -0
  24. package/resources/book--the-msx-red-book/Introduction.md +104 -0
  25. package/resources/book--the-msx-red-book/toc.json +38 -3
  26. package/resources/processors/toc.json +3 -3
  27. package/resources/processors/z80-undocumented.md +141 -0
  28. package/resources/sdcc/1_Introduction.md +199 -0
  29. package/resources/sdcc/2_Installing_SDCC.md +533 -0
  30. package/resources/sdcc/3_Using_SDCC.md +1758 -0
  31. package/resources/sdcc/4_Notes_on_supported_Processors.md +1638 -0
  32. package/resources/sdcc/5_Debugging.md +210 -0
  33. package/resources/sdcc/6_Tips_and_Support.md +258 -0
  34. package/resources/sdcc/7_SDCC_Technical_Data.md +489 -0
  35. package/resources/sdcc/8_Compiler_internals.md +477 -0
  36. package/resources/sdcc/toc.json +44 -2
  37. package/vector-db/msxdocs.lance/_indices/4d3bd360-e3c6-408d-b0ff-a4d6bd9580cb/metadata.lance +0 -0
  38. package/vector-db/msxdocs.lance/_indices/4d3bd360-e3c6-408d-b0ff-a4d6bd9580cb/part_0_docs.lance +0 -0
  39. package/vector-db/msxdocs.lance/_indices/4d3bd360-e3c6-408d-b0ff-a4d6bd9580cb/part_0_invert.lance +0 -0
  40. package/vector-db/msxdocs.lance/_indices/4d3bd360-e3c6-408d-b0ff-a4d6bd9580cb/part_0_tokens.lance +0 -0
  41. package/vector-db/msxdocs.lance/_transactions/0-6f47c9fc-3657-40f0-9dd4-c7226b2a4805.txn +0 -0
  42. package/vector-db/msxdocs.lance/_transactions/1-2bb7426e-a4b0-40ea-9a58-00c4985fc6a9.txn +0 -0
  43. package/vector-db/msxdocs.lance/_versions/18446744073709551613.manifest +0 -0
  44. package/vector-db/msxdocs.lance/_versions/18446744073709551614.manifest +0 -0
  45. package/vector-db/msxdocs.lance/_versions/latest_version_hint.json +1 -0
  46. package/vector-db/msxdocs.lance/data/110001110001011010001000876c134b8296fbc47762d1e1ab.lance +0 -0
  47. package/resources/book--the-msx-red-book/the_msx_red_book.md +0 -10349
  48. package/resources/processors/z80-undocumented.tex +0 -5617
  49. package/resources/sdcc/lyx2md.py +0 -745
  50. package/resources/sdcc/sdccman.lyx +0 -81574
  51. package/resources/sdcc/sdccman.md +0 -5557
  52. package/vector-db/index.json +0 -1
package/dist/openmsx.js CHANGED
@@ -5,21 +5,23 @@
5
5
  * @license GPL2
6
6
  */
7
7
  import fs from "fs/promises";
8
- import fsSync from "fs";
9
8
  import { extractDescriptionFromXML, decodeHtmlEntities, encodeHtmlEntities } from "./utils.js";
10
9
  import { spawn } from 'child_process';
11
- import net from 'net';
12
- import os from 'os';
13
10
  import path from 'path';
11
+ import { OpenMsxWindowsConnector } from "./openmsx_windows.js";
14
12
  /** True when running on Windows. Evaluated once at module load. */
15
13
  const IS_WINDOWS = process.platform === 'win32';
16
14
  export class OpenMSX {
17
15
  lastMachine = null;
18
16
  process = null;
19
17
  isConnected = false;
20
- // Windows TCP socket bidirectional after SSPI auth
21
- tcpSocket = null;
22
- // Accumulated I/O data for readData() — shared by both platforms (never coexist)
18
+ // Active writable control channel: process.stdin (Linux/macOS) or the
19
+ // Windows connection's input (TCP socket / SSPI proxy stdin).
20
+ controlWritable = null;
21
+ // Windows-only control connection (TCP+SSPI or stdio proxy). Owns the
22
+ // transport and its teardown; null on Linux/macOS. See openmsx_windows.ts.
23
+ controlConnection = null;
24
+ // Accumulated I/O data for readData() — shared by all transports (never coexist)
23
25
  ioBuffer = '';
24
26
  // Notify callback: fired when new I/O data arrives — shared by both platforms
25
27
  ioNotify = null;
@@ -51,6 +53,23 @@ export class OpenMSX {
51
53
  }
52
54
  this.resetIO();
53
55
  this.commandQueue = Promise.resolve(''); // reset queue for new session
56
+ // Resolve the Windows control mode up front so an invalid value
57
+ // fails before we spawn openMSX (avoids orphaned GUI processes).
58
+ let windowsMode = null;
59
+ if (IS_WINDOWS) {
60
+ try {
61
+ windowsMode = OpenMsxWindowsConnector.getControlMode();
62
+ }
63
+ catch (e) {
64
+ safeResolve(`Error: ${e instanceof Error ? e.message : e}`);
65
+ return;
66
+ }
67
+ if (windowsMode === 'pipe') {
68
+ safeResolve('Error: OPENMSX_WINDOWS_CONTROL=pipe is reserved but not implemented yet');
69
+ return;
70
+ }
71
+ diag(`Windows control mode: ${windowsMode}`);
72
+ }
54
73
  // Build args
55
74
  const args = [];
56
75
  if (!IS_WINDOWS) {
@@ -169,85 +188,88 @@ export class OpenMSX {
169
188
  });
170
189
  }
171
190
  /**
172
- * Windows connection: TCP socket + SSPI authentication.
173
- * Polls for the socket file, connects, authenticates, and starts the XML session.
191
+ * Windows connection (both modes). All transport — socket-file polling, TCP
192
+ * connect, SSPI, or proxy launch is delegated to {@link OpenMsxWindowsConnector},
193
+ * which returns a uniform connection with SSPI already done. Here we only
194
+ * attach the generic XML stream handlers and open the session. Both Windows
195
+ * modes use the TCP semantics: we send `<openmsx-control>` and openMSX replies
196
+ * with `<openmsx-output>`.
174
197
  */
175
198
  launchConnectWindows(ctx) {
176
- const tmpDir = process.env.TEMP ?? process.env.TMP ??
177
- path.join(os.homedir(), 'AppData', 'Local', 'Temp');
178
- const socketFile = path.join(tmpDir, 'openmsx-default', `socket.${this.process.pid}`);
179
- ctx.diag(`waiting for socket file: ${socketFile}`);
180
- this.connectWindowsTCP(socketFile).then(async ({ socket, port }) => {
181
- ctx.diag(`TCP connected on port ${port}`);
182
- this.tcpSocket = socket;
199
+ const connector = new OpenMsxWindowsConnector({
200
+ openmsxProcess: this.process,
201
+ diag: ctx.diag,
202
+ });
203
+ connector.connect().then((connection) => {
204
+ this.controlConnection = connection;
205
+ this.controlWritable = connection.input;
183
206
  this.ioBuffer = '';
184
- socket.on('error', e => {
207
+ // Diagnostics from the SSPI proxy (stderr only never stdout).
208
+ connection.errorOutput?.on('data', (data) => {
209
+ const msg = data.toString().trim();
210
+ if (msg && !this.isConnected)
211
+ ctx.diag(`control stderr: ${msg.substring(0, 300)}`);
212
+ });
213
+ // stdio-proxy: proxy exiting before we connect means SSPI/connect failed.
214
+ connection.controlProcess?.on('exit', (code, signal) => {
215
+ ctx.diag(`control process exit: code=${code} signal=${signal} isConnected=${this.isConnected}`);
216
+ if (!this.isConnected && !ctx.isResolved()) {
217
+ ctx.safeResolve(`Error: control process exited before connecting (code=${code}, signal=${signal}).`);
218
+ }
219
+ });
220
+ // direct-sspi: TCP socket errors / close.
221
+ connection.tcpSocket?.on('error', e => {
185
222
  ctx.diag(`TCP socket error: ${e.message}`);
186
223
  if (!ctx.isResolved())
187
224
  ctx.safeResolve(`Error: TCP socket error: ${e.message}`);
188
225
  });
189
- socket.on('close', () => {
226
+ connection.tcpSocket?.on('close', () => {
190
227
  ctx.diag('TCP socket closed');
191
- this.tcpSocket = null;
192
228
  this.isConnected = false;
193
229
  });
194
- // Register the main data handler BEFORE performSspiAuth.
195
- // This prevents any gap where data could be discarded:
196
- // performSspiAuth adds its own 'data' listener that runs in
197
- // parallel both handlers receive data, but:
198
- // - during SSPI: ioBuffer accumulates binary SSPI tokens
199
- // (harmless, won't contain '<openmsx-output>')
200
- // - after SSPI: ioBuffer is cleared, then <openmsx-control>
201
- // is sent, and <openmsx-output> is detected normally.
202
- socket.on('data', (data) => {
203
- const chunk = data.toString();
204
- // Accumulate all incoming data
205
- this.ioBuffer += chunk;
206
- // Signal readData() that new data arrived (notify pattern)
207
- if (this.ioNotify) {
208
- const notify = this.ioNotify;
209
- this.ioNotify = null;
210
- notify();
211
- }
212
- // During launch: detect <openmsx-output> in the accumulated stream
213
- if (!this.isConnected && this.ioBuffer.includes('<openmsx-output>')) {
214
- this.isConnected = true;
215
- // Discard everything up to and including <openmsx-output>
216
- this.ioBuffer = this.ioBuffer.substring(this.ioBuffer.indexOf('<openmsx-output>') + '<openmsx-output>'.length);
217
- setTimeout(async () => {
218
- if (!ctx.isResolved()) {
219
- try {
220
- await ctx.onReady(false);
221
- } // Windows: don't resend <openmsx-control>
222
- catch (e) {
223
- ctx.safeResolve(`Error: Failed to send control commands - ${e instanceof Error ? e.message : e}`);
224
- }
225
- }
226
- }, 300);
227
- }
228
- });
229
- // SSPI authentication (adds its own parallel 'data' listener internally)
230
- try {
231
- ctx.diag('starting SSPI authentication...');
232
- await this.performSspiAuth(socket);
233
- ctx.diag('SSPI authentication successful');
234
- }
235
- catch (e) {
236
- ctx.safeResolve(`Error: SSPI authentication failed: ${e instanceof Error ? e.message : e}. ` +
237
- `Make sure 'node-expose-sspi' is installed: npm install node-expose-sspi`);
238
- return;
239
- }
240
- // Clear SSPI binary garbage from ioBuffer before XML session
241
- this.ioBuffer = '';
242
- // Send <openmsx-control> to initiate the XML session.
243
- // On Windows/TCP, openMSX sends <openmsx-output> in response.
230
+ connection.output.on('error', (e) => ctx.diag(`control stream error: ${e.message}`));
231
+ this.attachOutputHandler(connection.output, ctx);
232
+ // Open the XML session: we send <openmsx-control>; openMSX replies with
233
+ // <openmsx-output>. (In stdio-proxy the proxy forwards it after SSPI.)
244
234
  ctx.diag('sending <openmsx-control> to start XML session');
245
- socket.write('<openmsx-control>\n');
246
- }).catch(err => {
247
- ctx.diag(`connectWindowsTCP failed: ${err.message}`);
235
+ connection.input.write('<openmsx-control>\n');
236
+ }).catch((err) => {
237
+ ctx.diag(`windows connect failed: ${err.message}`);
248
238
  ctx.safeResolve(`Error: ${err.message}`);
249
239
  });
250
240
  }
241
+ /**
242
+ * Shared output handler for both Windows modes (proxy stdout or TCP socket):
243
+ * accumulate into `ioBuffer`, wake any pending `readData()`, and on the first
244
+ * `<openmsx-output>` mark connected and trigger the initial command sequence
245
+ * with `onReady(false)` (we already sent `<openmsx-control>`). Linux/macOS uses
246
+ * its own variant in {@link launchConnectLinux} (openMSX emits the tag unprompted).
247
+ */
248
+ attachOutputHandler(output, ctx) {
249
+ output.on('data', (data) => {
250
+ const chunk = data.toString();
251
+ this.ioBuffer += chunk;
252
+ if (this.ioNotify) {
253
+ const notify = this.ioNotify;
254
+ this.ioNotify = null;
255
+ notify();
256
+ }
257
+ if (!this.isConnected && this.ioBuffer.includes('<openmsx-output>')) {
258
+ this.isConnected = true;
259
+ this.ioBuffer = this.ioBuffer.substring(this.ioBuffer.indexOf('<openmsx-output>') + '<openmsx-output>'.length);
260
+ setTimeout(async () => {
261
+ if (!ctx.isResolved()) {
262
+ try {
263
+ await ctx.onReady(false);
264
+ } // <openmsx-control> already sent
265
+ catch (e) {
266
+ ctx.safeResolve(`Error: Failed to send control commands - ${e instanceof Error ? e.message : e}`);
267
+ }
268
+ }
269
+ }, 300);
270
+ }
271
+ });
272
+ }
251
273
  /**
252
274
  * Linux/macOS connection: stdio pipes.
253
275
  * Registers stdout handler for ioBuffer accumulation and <openmsx-output> detection.
@@ -255,6 +277,7 @@ export class OpenMSX {
255
277
  launchConnectLinux(ctx) {
256
278
  const FATAL_ERROR_GRACE_PERIOD = 500;
257
279
  let connectionTime = null;
280
+ this.controlWritable = this.process.stdin;
258
281
  this.process.stdout.on('data', (data) => {
259
282
  const output = data.toString();
260
283
  if (!this.isConnected) {
@@ -294,164 +317,17 @@ export class OpenMSX {
294
317
  }
295
318
  });
296
319
  }
297
- /**
298
- * SSPI (Negotiate/NTLM) authentication handshake — Windows only.
299
- * Required since openMSX 0.7.1 for TCP socket connections.
300
- * Uses `node-expose-sspi` v0.1.x optional npm package.
301
- *
302
- * Reference C++ implementation: openMSX debugger SspiNegotiateClient.cpp
303
- * Protocol: loop until SEC_E_OK — each round:
304
- * client → [4-byte BE length][SSPI token]
305
- * server → [4-byte BE length][SSPI response] (if SEC_I_CONTINUE_NEEDED)
306
- * After SEC_E_OK: no server read — proceed directly with XML protocol.
307
- */
308
- async performSspiAuth(socket) {
309
- let nes;
310
- try {
311
- const { createRequire } = await import('module');
312
- const req = createRequire(import.meta.url);
313
- nes = req('node-expose-sspi');
314
- }
315
- catch (e) {
316
- throw new Error(`node-expose-sspi not available (${e instanceof Error ? e.message : e}). ` +
317
- `Install with: npm install node-expose-sspi`);
318
- }
319
- // Accumulate TCP data for length-prefixed reads during SSPI phase.
320
- // Pattern: onSspiData appends to buffer and NOTIFIES (does not clear).
321
- // readLengthPrefixed checks the buffer size after each notification.
322
- let sspiBuffer = Buffer.alloc(0);
323
- let sspiNotify = null;
324
- const onSspiData = (chunk) => {
325
- sspiBuffer = Buffer.concat([sspiBuffer, chunk]);
326
- if (sspiNotify) {
327
- const notify = sspiNotify;
328
- sspiNotify = null;
329
- notify(); // just signal — buffer stays intact
330
- }
331
- };
332
- socket.on('data', onSspiData);
333
- // Wait until new data arrives (doesn't clear the buffer)
334
- const waitMore = () => new Promise(resolve => {
335
- sspiNotify = resolve;
336
- });
337
- const readLengthPrefixed = async () => {
338
- // Wait until we have at least the 4-byte length prefix
339
- while (sspiBuffer.length < 4)
340
- await waitMore();
341
- const len = sspiBuffer.readUInt32BE(0);
342
- const total = 4 + len;
343
- // Wait until the full payload has arrived
344
- while (sspiBuffer.length < total)
345
- await waitMore();
346
- const result = sspiBuffer.slice(4, total);
347
- sspiBuffer = sspiBuffer.slice(total); // consume only what we read
348
- return result;
349
- };
350
- const sendToken = (token) => {
351
- const buf = Buffer.from(token);
352
- const lenBuf = Buffer.alloc(4);
353
- lenBuf.writeUInt32BE(buf.length, 0);
354
- socket.write(lenBuf);
355
- socket.write(buf);
356
- };
357
- try {
358
- // node-expose-sspi v0.1.x API:
359
- // AcquireCredentialsHandle → CredentialWithExpiry { credential, tsExpiry }
360
- // InitializeSecurityContextInput.credential = CredHandle (the .credential property)
361
- // InitializeSecurityContextInput.SecBufferDesc = server's token (NOT serverSecurityContext)
362
- // InitializeSecurityContextInput.contextReq = string[] of ISC_REQ_* flags
363
- // Flags from openMSX C++ ref: ISC_REQ_ALLOCATE_MEMORY | ISC_REQ_CONNECTION | ISC_REQ_STREAM
364
- const credWithExpiry = nes.sspi.AcquireCredentialsHandle({
365
- packageName: 'Negotiate',
366
- credentialUse: 'SECPKG_CRED_OUTBOUND',
367
- });
368
- const credential = credWithExpiry.credential;
369
- const packageInfo = nes.sspi.QuerySecurityPackageInfo('Negotiate');
370
- const ISC_FLAGS = ['ISC_REQ_ALLOCATE_MEMORY', 'ISC_REQ_CONNECTION', 'ISC_REQ_STREAM'];
371
- let contextHandle = undefined;
372
- let serverSecBufDesc = undefined;
373
- // Loop until SEC_E_OK (mirrors C++ reference implementation)
374
- while (true) {
375
- const ctxInput = {
376
- credential,
377
- targetName: '',
378
- cbMaxToken: packageInfo.cbMaxToken,
379
- contextReq: ISC_FLAGS,
380
- targetDataRep: 'SECURITY_NETWORK_DREP',
381
- };
382
- if (contextHandle !== undefined)
383
- ctxInput.contextHandle = contextHandle;
384
- if (serverSecBufDesc !== undefined)
385
- ctxInput.SecBufferDesc = serverSecBufDesc;
386
- const clientCtx = nes.sspi.InitializeSecurityContext(ctxInput);
387
- contextHandle = clientCtx.contextHandle;
388
- // Send our token to the server (if non-empty)
389
- const tokenBuf = clientCtx.SecBufferDesc?.buffers?.[0];
390
- if (tokenBuf && tokenBuf.byteLength > 0) {
391
- sendToken(tokenBuf);
392
- }
393
- if (clientCtx.SECURITY_STATUS === 'SEC_E_OK') {
394
- // Auth complete — no final read from server, proceed to XML
395
- break;
396
- }
397
- if (clientCtx.SECURITY_STATUS !== 'SEC_I_CONTINUE_NEEDED') {
398
- throw new Error(`SSPI error: ${clientCtx.SECURITY_STATUS}`);
399
- }
400
- // Read server's response token
401
- const response = await readLengthPrefixed();
402
- const responseAB = response.buffer.slice(response.byteOffset, response.byteOffset + response.byteLength);
403
- serverSecBufDesc = { ulVersion: 0, buffers: [responseAB] };
404
- }
405
- }
406
- finally {
407
- // Remove SSPI data handler — main handler will be added after this returns
408
- socket.removeListener('data', onSspiData);
409
- }
410
- }
411
- /**
412
- * Poll for the TCP socket file, read the port, connect.
413
- */
414
- connectWindowsTCP(socketFile) {
415
- return new Promise((resolve, reject) => {
416
- const maxWaitMs = 8000;
417
- const pollMs = 200;
418
- let elapsed = 0;
419
- const poll = () => {
420
- if (fsSync.existsSync(socketFile)) {
421
- let port;
422
- try {
423
- port = parseInt(fsSync.readFileSync(socketFile, 'utf8').trim(), 10);
424
- }
425
- catch (e) {
426
- return reject(new Error(`Cannot read socket file: ${e}`));
427
- }
428
- if (!port || isNaN(port))
429
- return reject(new Error(`Invalid port in socket file`));
430
- const sock = net.createConnection(port, '127.0.0.1');
431
- sock.once('connect', () => resolve({ socket: sock, port }));
432
- sock.once('error', err => { sock.destroy(); reject(new Error(`TCP connect to ${port} failed: ${err.message}`)); });
433
- }
434
- else {
435
- elapsed += pollMs;
436
- if (elapsed >= maxWaitMs) {
437
- reject(new Error(`openMSX socket file not found after ${maxWaitMs}ms: ${socketFile}`));
438
- }
439
- else {
440
- setTimeout(poll, pollMs);
441
- }
442
- }
443
- };
444
- poll();
445
- });
446
- }
447
320
  resetIO() {
448
- if (this.tcpSocket) {
321
+ // Tear down the Windows control connection (TCP socket / SSPI proxy).
322
+ // No-op on Linux/macOS where controlConnection is null.
323
+ if (this.controlConnection) {
449
324
  try {
450
- this.tcpSocket.destroy();
325
+ this.controlConnection.forceClose();
451
326
  }
452
327
  catch (_) { /* ignore */ }
453
- this.tcpSocket = null;
328
+ this.controlConnection = null;
454
329
  }
330
+ this.controlWritable = null;
455
331
  this.ioBuffer = '';
456
332
  this.ioNotify = null;
457
333
  }
@@ -472,6 +348,7 @@ export class OpenMSX {
472
348
  this.lastMachine = null;
473
349
  this.isConnected = false;
474
350
  this.process = null;
351
+ // resetIO() also tears down the Windows control connection (proxy/socket).
475
352
  this.resetIO();
476
353
  safeResolve("Ok: Emulator process closed successfully");
477
354
  });
@@ -572,16 +449,10 @@ export class OpenMSX {
572
449
  writeData(data) {
573
450
  if (!this.process || !this.isConnected)
574
451
  throw new Error('openMSX process not running or not connected');
575
- if (IS_WINDOWS) {
576
- if (!this.tcpSocket || this.tcpSocket.destroyed)
577
- throw new Error('Windows TCP socket not open');
578
- this.tcpSocket.write(data);
579
- }
580
- else {
581
- if (!this.process.stdin)
582
- throw new Error('openMSX stdin not available');
583
- this.process.stdin.write(data);
584
- }
452
+ const channel = this.controlWritable;
453
+ if (!channel || channel.destroyed)
454
+ throw new Error('openMSX control channel not available');
455
+ channel.write(data);
585
456
  }
586
457
  readData() {
587
458
  return new Promise((resolve, reject) => {
@@ -589,19 +460,11 @@ export class OpenMSX {
589
460
  reject(new Error('openMSX process not running or not connected'));
590
461
  return;
591
462
  }
592
- if (IS_WINDOWS) {
593
- if (!this.tcpSocket || this.tcpSocket.destroyed) {
594
- reject(new Error('Windows TCP socket not open'));
595
- return;
596
- }
597
- }
598
- else {
599
- if (!this.process?.stdout) {
600
- reject(new Error('openMSX stdout not available'));
601
- return;
602
- }
463
+ if (!this.controlWritable) {
464
+ reject(new Error('openMSX control channel not available'));
465
+ return;
603
466
  }
604
- // Unified for both platforms: accumulate in ioBuffer until a complete
467
+ // Unified for all transports: accumulate in ioBuffer until a complete
605
468
  // <reply>…</reply> block, then extract and return it.
606
469
  const RESPONSE_TIMEOUT = 10000;
607
470
  const timer = setTimeout(() => {
@@ -629,14 +492,16 @@ export class OpenMSX {
629
492
  await this.emu_close();
630
493
  }
631
494
  forceClose() {
495
+ // Kill the openMSX emulator; resetIO() force-closes the control connection
496
+ // (TCP socket / SSPI proxy) on Windows.
632
497
  if (this.process && !this.process.killed) {
633
498
  try {
634
499
  this.process.kill('SIGKILL');
635
500
  }
636
501
  catch (_) { /* ignore */ }
637
- this.process = null;
638
- this.isConnected = false;
639
502
  }
503
+ this.process = null;
504
+ this.isConnected = false;
640
505
  this.resetIO();
641
506
  }
642
507
  }