c64-debug-mcp 0.1.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 +287 -0
- package/dist/http.cjs +3603 -0
- package/dist/http.d.cts +1 -0
- package/dist/http.d.ts +1 -0
- package/dist/http.js +3580 -0
- package/dist/stdio.cjs +3553 -0
- package/dist/stdio.d.cts +1 -0
- package/dist/stdio.d.ts +1 -0
- package/dist/stdio.js +3530 -0
- package/package.json +61 -0
package/dist/http.cjs
ADDED
|
@@ -0,0 +1,3603 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
"use strict";
|
|
3
|
+
var __create = Object.create;
|
|
4
|
+
var __defProp = Object.defineProperty;
|
|
5
|
+
var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
|
|
6
|
+
var __getOwnPropNames = Object.getOwnPropertyNames;
|
|
7
|
+
var __getProtoOf = Object.getPrototypeOf;
|
|
8
|
+
var __hasOwnProp = Object.prototype.hasOwnProperty;
|
|
9
|
+
var __copyProps = (to, from, except, desc) => {
|
|
10
|
+
if (from && typeof from === "object" || typeof from === "function") {
|
|
11
|
+
for (let key of __getOwnPropNames(from))
|
|
12
|
+
if (!__hasOwnProp.call(to, key) && key !== except)
|
|
13
|
+
__defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
|
|
14
|
+
}
|
|
15
|
+
return to;
|
|
16
|
+
};
|
|
17
|
+
var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
|
|
18
|
+
// If the importer is in node compatibility mode or this is not an ESM
|
|
19
|
+
// file that has been converted to a CommonJS file using a Babel-
|
|
20
|
+
// compatible transform (i.e. "__esModule" has not been set), then set
|
|
21
|
+
// "default" to the CommonJS "module.exports" for node compatibility.
|
|
22
|
+
isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
|
|
23
|
+
mod
|
|
24
|
+
));
|
|
25
|
+
|
|
26
|
+
// src/http.ts
|
|
27
|
+
var import_node_http = __toESM(require("http"), 1);
|
|
28
|
+
|
|
29
|
+
// src/server.ts
|
|
30
|
+
var import_tools = require("@mastra/core/tools");
|
|
31
|
+
var import_mcp = require("@mastra/mcp");
|
|
32
|
+
var import_zod4 = require("zod");
|
|
33
|
+
|
|
34
|
+
// src/contracts.ts
|
|
35
|
+
var import_zod = require("zod");
|
|
36
|
+
var VICE_API_VERSION = 2;
|
|
37
|
+
var VICE_STX = 2;
|
|
38
|
+
var VICE_BROADCAST_REQUEST_ID = 4294967295;
|
|
39
|
+
var DEFAULT_MONITOR_HOST = "127.0.0.1";
|
|
40
|
+
var DEFAULT_C64_BINARY = "x64sc";
|
|
41
|
+
var DEFAULT_FORBIDDEN_PORTS = /* @__PURE__ */ new Set([6502]);
|
|
42
|
+
var transportStateSchema = import_zod.z.enum([
|
|
43
|
+
"not_started",
|
|
44
|
+
"starting",
|
|
45
|
+
"waiting_for_monitor",
|
|
46
|
+
"connecting",
|
|
47
|
+
"connected",
|
|
48
|
+
"reconnecting",
|
|
49
|
+
"disconnected",
|
|
50
|
+
"stopped",
|
|
51
|
+
"faulted"
|
|
52
|
+
]);
|
|
53
|
+
var processStateSchema = import_zod.z.enum(["not_applicable", "launching", "running", "exited", "crashed"]);
|
|
54
|
+
var executionStateSchema = import_zod.z.enum(["unknown", "running", "stopped"]);
|
|
55
|
+
var stopReasonSchema = import_zod.z.enum([
|
|
56
|
+
"none",
|
|
57
|
+
"breakpoint",
|
|
58
|
+
"watchpoint_read",
|
|
59
|
+
"watchpoint_write",
|
|
60
|
+
"step_complete",
|
|
61
|
+
"reset",
|
|
62
|
+
"monitor_entry",
|
|
63
|
+
"program_end",
|
|
64
|
+
"error",
|
|
65
|
+
"unknown"
|
|
66
|
+
]);
|
|
67
|
+
var sessionHealthSchema = import_zod.z.enum(["not_configured", "starting", "ready", "recovering", "stopped", "error"]);
|
|
68
|
+
var breakpointKindSchema = import_zod.z.enum(["exec", "read", "write", "read_write"]);
|
|
69
|
+
var resetModeSchema = import_zod.z.enum(["soft", "hard"]);
|
|
70
|
+
var inputActionSchema = import_zod.z.enum(["press", "release", "tap"]);
|
|
71
|
+
var joystickControlSchema = import_zod.z.enum(["up", "down", "left", "right", "fire"]);
|
|
72
|
+
var joystickPortSchema = import_zod.z.union([import_zod.z.literal(1), import_zod.z.literal(2)]);
|
|
73
|
+
var toolErrorCategorySchema = import_zod.z.enum([
|
|
74
|
+
"validation",
|
|
75
|
+
"configuration",
|
|
76
|
+
"session_state",
|
|
77
|
+
"process_launch",
|
|
78
|
+
"connection",
|
|
79
|
+
"protocol",
|
|
80
|
+
"timeout",
|
|
81
|
+
"io",
|
|
82
|
+
"unsupported",
|
|
83
|
+
"internal"
|
|
84
|
+
]);
|
|
85
|
+
var warningItemSchema = import_zod.z.object({
|
|
86
|
+
code: import_zod.z.string(),
|
|
87
|
+
message: import_zod.z.string()
|
|
88
|
+
});
|
|
89
|
+
var c64ConfigSchema = import_zod.z.object({
|
|
90
|
+
binaryPath: import_zod.z.string().optional(),
|
|
91
|
+
workingDirectory: import_zod.z.string().optional(),
|
|
92
|
+
arguments: import_zod.z.string().optional()
|
|
93
|
+
});
|
|
94
|
+
var responseMetaSchema = import_zod.z.object({
|
|
95
|
+
freshEmulator: import_zod.z.boolean(),
|
|
96
|
+
launchId: import_zod.z.number().int().nonnegative(),
|
|
97
|
+
restartCount: import_zod.z.number().int().nonnegative()
|
|
98
|
+
});
|
|
99
|
+
var C64_REGISTER_DEFINITIONS = [
|
|
100
|
+
{ fieldName: "PC", viceName: "PC", widthBits: 16, min: 0, max: 65535, description: "Program counter register" },
|
|
101
|
+
{ fieldName: "A", viceName: "A", widthBits: 8, min: 0, max: 255, description: "Accumulator register" },
|
|
102
|
+
{ fieldName: "X", viceName: "X", widthBits: 8, min: 0, max: 255, description: "X index register" },
|
|
103
|
+
{ fieldName: "Y", viceName: "Y", widthBits: 8, min: 0, max: 255, description: "Y index register" },
|
|
104
|
+
{ fieldName: "SP", viceName: "SP", widthBits: 8, min: 0, max: 255, description: "Stack pointer register" },
|
|
105
|
+
{ fieldName: "FL", viceName: "FL", widthBits: 8, min: 0, max: 255, description: "CPU flags register" },
|
|
106
|
+
{ fieldName: "00", viceName: "00", widthBits: 8, min: 0, max: 255, description: "Zero-page processor port register 00" },
|
|
107
|
+
{ fieldName: "01", viceName: "01", widthBits: 8, min: 0, max: 255, description: "Zero-page processor port register 01" },
|
|
108
|
+
{ fieldName: "LIN", viceName: "LIN", widthBits: 16, min: 0, max: 65535, description: "Current raster line register" },
|
|
109
|
+
{ fieldName: "CYC", viceName: "CYC", widthBits: 16, min: 0, max: 65535, description: "Current cycle position register" }
|
|
110
|
+
];
|
|
111
|
+
var toolErrorSchema = import_zod.z.object({
|
|
112
|
+
code: import_zod.z.string(),
|
|
113
|
+
message: import_zod.z.string(),
|
|
114
|
+
category: toolErrorCategorySchema,
|
|
115
|
+
retryable: import_zod.z.boolean(),
|
|
116
|
+
details: import_zod.z.record(import_zod.z.unknown()).optional()
|
|
117
|
+
});
|
|
118
|
+
function mainMemSpaceToProtocol() {
|
|
119
|
+
return 0;
|
|
120
|
+
}
|
|
121
|
+
function breakpointKindToOperation(kind) {
|
|
122
|
+
switch (kind) {
|
|
123
|
+
case "read":
|
|
124
|
+
return 1;
|
|
125
|
+
case "write":
|
|
126
|
+
return 2;
|
|
127
|
+
case "read_write":
|
|
128
|
+
return 3;
|
|
129
|
+
case "exec":
|
|
130
|
+
return 4;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
function cpuOperationToBreakpointKind(operation) {
|
|
134
|
+
if ((operation & 4) === 4) {
|
|
135
|
+
return "exec";
|
|
136
|
+
}
|
|
137
|
+
if ((operation & 1) === 1 && (operation & 2) === 2) {
|
|
138
|
+
return "read_write";
|
|
139
|
+
}
|
|
140
|
+
if ((operation & 1) === 1) {
|
|
141
|
+
return "read";
|
|
142
|
+
}
|
|
143
|
+
if ((operation & 2) === 2) {
|
|
144
|
+
return "write";
|
|
145
|
+
}
|
|
146
|
+
return "exec";
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// src/errors.ts
|
|
150
|
+
var import_zod3 = require("zod");
|
|
151
|
+
|
|
152
|
+
// src/schemas.ts
|
|
153
|
+
var import_zod2 = require("zod");
|
|
154
|
+
var warningSchema = warningItemSchema;
|
|
155
|
+
var address16Schema = import_zod2.z.number().int().min(0).max(65535);
|
|
156
|
+
var byteValueSchema = import_zod2.z.number().int().min(0).max(255).describe("Single raw byte value");
|
|
157
|
+
var byteArraySchema = import_zod2.z.array(byteValueSchema).describe("Raw bytes as a JSON array");
|
|
158
|
+
var c64RegisterValueSchema = import_zod2.z.object(
|
|
159
|
+
Object.fromEntries(
|
|
160
|
+
C64_REGISTER_DEFINITIONS.map((register) => [
|
|
161
|
+
register.fieldName,
|
|
162
|
+
import_zod2.z.number().int().min(register.min).max(register.max).describe(register.description)
|
|
163
|
+
])
|
|
164
|
+
)
|
|
165
|
+
);
|
|
166
|
+
var c64PartialRegisterValueSchema = import_zod2.z.object(
|
|
167
|
+
Object.fromEntries(
|
|
168
|
+
C64_REGISTER_DEFINITIONS.map((register) => [
|
|
169
|
+
register.fieldName,
|
|
170
|
+
import_zod2.z.number().int().min(register.min).max(register.max).describe(register.description).optional()
|
|
171
|
+
])
|
|
172
|
+
)
|
|
173
|
+
);
|
|
174
|
+
var debugStateSchema = import_zod2.z.object({
|
|
175
|
+
executionState: executionStateSchema.describe("Current execution state of the emulator"),
|
|
176
|
+
lastStopReason: stopReasonSchema.describe("Reason the emulator most recently stopped in the monitor"),
|
|
177
|
+
programCounter: address16Schema.describe("Current program counter"),
|
|
178
|
+
registers: c64RegisterValueSchema
|
|
179
|
+
});
|
|
180
|
+
var monitorStateSchema = import_zod2.z.object({
|
|
181
|
+
executionState: executionStateSchema.describe("Current execution state reported by the monitor"),
|
|
182
|
+
lastStopReason: stopReasonSchema.describe("Reason the monitor most recently reported a stop-like state"),
|
|
183
|
+
runtimeKnown: import_zod2.z.boolean().describe("Whether the monitor has reported a runtime event in this session"),
|
|
184
|
+
programCounter: address16Schema.nullable().describe("Program counter from the latest monitor event, or null if unknown")
|
|
185
|
+
});
|
|
186
|
+
var sessionStateResultSchema = import_zod2.z.object({
|
|
187
|
+
transportState: import_zod2.z.enum([
|
|
188
|
+
"not_started",
|
|
189
|
+
"starting",
|
|
190
|
+
"waiting_for_monitor",
|
|
191
|
+
"connecting",
|
|
192
|
+
"connected",
|
|
193
|
+
"reconnecting",
|
|
194
|
+
"disconnected",
|
|
195
|
+
"stopped",
|
|
196
|
+
"faulted"
|
|
197
|
+
]),
|
|
198
|
+
processState: import_zod2.z.enum(["not_applicable", "launching", "running", "exited", "crashed"]),
|
|
199
|
+
executionState: executionStateSchema.describe("Current execution state of the emulator session"),
|
|
200
|
+
lastStopReason: stopReasonSchema.describe("Reason the emulator most recently stopped in the monitor"),
|
|
201
|
+
idleAutoResumeArmed: import_zod2.z.boolean().describe("Whether the idle auto-resume timer is currently armed"),
|
|
202
|
+
explicitPauseActive: import_zod2.z.boolean().describe("Whether execution was explicitly paused by the caller"),
|
|
203
|
+
lastCheckpointId: import_zod2.z.number().int().nullable().describe("Most recent hit checkpoint/watchpoint id when known"),
|
|
204
|
+
lastCheckpointKind: breakpointKindSchema.nullable().describe("Most recent hit checkpoint/watchpoint kind when known"),
|
|
205
|
+
recoveryInProgress: import_zod2.z.boolean(),
|
|
206
|
+
launchId: import_zod2.z.number().int().nonnegative(),
|
|
207
|
+
restartCount: import_zod2.z.number().int().nonnegative(),
|
|
208
|
+
freshEmulatorPending: import_zod2.z.boolean(),
|
|
209
|
+
connectedSince: import_zod2.z.string().nullable(),
|
|
210
|
+
lastResponseAt: import_zod2.z.string().nullable(),
|
|
211
|
+
processId: import_zod2.z.number().int().nullable(),
|
|
212
|
+
warnings: import_zod2.z.array(warningSchema)
|
|
213
|
+
});
|
|
214
|
+
var breakpointSchema = import_zod2.z.object({
|
|
215
|
+
id: import_zod2.z.number().int().describe("Breakpoint identifier"),
|
|
216
|
+
address: address16Schema.describe("Start address of the breakpoint range"),
|
|
217
|
+
length: import_zod2.z.number().int().positive().describe("Size of the breakpoint range in bytes"),
|
|
218
|
+
enabled: import_zod2.z.boolean().describe("Whether the breakpoint is enabled"),
|
|
219
|
+
temporary: import_zod2.z.boolean().describe("Whether the breakpoint is temporary"),
|
|
220
|
+
hasCondition: import_zod2.z.boolean().describe("Whether the breakpoint has a condition expression"),
|
|
221
|
+
kind: breakpointKindSchema.describe("Breakpoint trigger kind"),
|
|
222
|
+
label: import_zod2.z.string().nullable().optional().describe("Optional caller-provided label")
|
|
223
|
+
});
|
|
224
|
+
var joystickStateSchema = import_zod2.z.object({
|
|
225
|
+
up: import_zod2.z.boolean().describe("Whether up is currently held on the selected joystick port"),
|
|
226
|
+
down: import_zod2.z.boolean().describe("Whether down is currently held on the selected joystick port"),
|
|
227
|
+
left: import_zod2.z.boolean().describe("Whether left is currently held on the selected joystick port"),
|
|
228
|
+
right: import_zod2.z.boolean().describe("Whether right is currently held on the selected joystick port"),
|
|
229
|
+
fire: import_zod2.z.boolean().describe("Whether fire is currently held on the selected joystick port")
|
|
230
|
+
});
|
|
231
|
+
var programLoadResultSchema = import_zod2.z.object({
|
|
232
|
+
filePath: import_zod2.z.string().describe("Absolute path to the program file that was loaded"),
|
|
233
|
+
autoStart: import_zod2.z.boolean().describe("Whether the loaded program was requested to start immediately after loading"),
|
|
234
|
+
fileIndex: import_zod2.z.number().int().nonnegative().describe("Autostart file index inside the image, when applicable"),
|
|
235
|
+
executionState: executionStateSchema.describe("Execution state after the monitor-driven load request")
|
|
236
|
+
});
|
|
237
|
+
var captureDisplayResultSchema = import_zod2.z.object({
|
|
238
|
+
imagePath: import_zod2.z.string().describe("Absolute path to the rendered PNG image"),
|
|
239
|
+
width: import_zod2.z.number().int().positive().describe("Width of the visible rendered screen image"),
|
|
240
|
+
height: import_zod2.z.number().int().positive().describe("Height of the visible rendered screen image"),
|
|
241
|
+
debugWidth: import_zod2.z.number().int().positive().describe("Width of the full uncropped debug display buffer"),
|
|
242
|
+
debugHeight: import_zod2.z.number().int().positive().describe("Height of the full uncropped debug display buffer"),
|
|
243
|
+
debugOffsetX: import_zod2.z.number().int().nonnegative().describe("X offset of the visible inner area within the debug display buffer"),
|
|
244
|
+
debugOffsetY: import_zod2.z.number().int().nonnegative().describe("Y offset of the visible inner area within the debug display buffer"),
|
|
245
|
+
bitsPerPixel: import_zod2.z.number().int().positive().describe("Bits per pixel reported by the emulator display payload")
|
|
246
|
+
});
|
|
247
|
+
var graphicsModeSchema = import_zod2.z.enum([
|
|
248
|
+
"standard_text",
|
|
249
|
+
"multicolor_text",
|
|
250
|
+
"standard_bitmap",
|
|
251
|
+
"multicolor_bitmap",
|
|
252
|
+
"extended_background_color_text",
|
|
253
|
+
"invalid_text_mode",
|
|
254
|
+
"invalid_bitmap_mode_1",
|
|
255
|
+
"invalid_bitmap_mode_2"
|
|
256
|
+
]);
|
|
257
|
+
var displayStateResultSchema = import_zod2.z.object({
|
|
258
|
+
graphicsMode: graphicsModeSchema.describe("Decoded VIC-II graphics mode from D011/D016"),
|
|
259
|
+
extendedColorMode: import_zod2.z.boolean().describe("Whether extended background color mode is enabled"),
|
|
260
|
+
bitmapMode: import_zod2.z.boolean().describe("Whether bitmap mode is enabled"),
|
|
261
|
+
multicolorMode: import_zod2.z.boolean().describe("Whether multicolor mode is enabled"),
|
|
262
|
+
vicBankAddress: address16Schema.describe("Base address of the active 16K VIC bank"),
|
|
263
|
+
screenRamAddress: address16Schema.describe("Base address of the active 1000-byte screen matrix"),
|
|
264
|
+
characterMemoryAddress: address16Schema.nullable().describe("Base address of the active character memory when the current mode uses character data"),
|
|
265
|
+
bitmapMemoryAddress: address16Schema.nullable().describe("Base address of the active bitmap memory when the current mode uses bitmap data"),
|
|
266
|
+
colorRamAddress: address16Schema.describe("Base address of the 1000-byte color RAM area"),
|
|
267
|
+
borderColor: import_zod2.z.number().int().min(0).max(15).describe("Current border color value"),
|
|
268
|
+
backgroundColor0: import_zod2.z.number().int().min(0).max(15).describe("Current background color 0 value"),
|
|
269
|
+
backgroundColor1: import_zod2.z.number().int().min(0).max(15).describe("Current background color 1 value"),
|
|
270
|
+
backgroundColor2: import_zod2.z.number().int().min(0).max(15).describe("Current background color 2 value"),
|
|
271
|
+
backgroundColor3: import_zod2.z.number().int().min(0).max(15).describe("Current background color 3 value"),
|
|
272
|
+
vicRegisters: import_zod2.z.object({
|
|
273
|
+
d011: byteValueSchema.describe("Raw VIC-II register $D011"),
|
|
274
|
+
d016: byteValueSchema.describe("Raw VIC-II register $D016"),
|
|
275
|
+
d018: byteValueSchema.describe("Raw VIC-II register $D018"),
|
|
276
|
+
dd00: byteValueSchema.describe("Raw CIA2/VIC bank register $DD00"),
|
|
277
|
+
d020: byteValueSchema.describe("Raw border color register $D020"),
|
|
278
|
+
d021: byteValueSchema.describe("Raw background color register $D021"),
|
|
279
|
+
d022: byteValueSchema.describe("Raw background color register $D022"),
|
|
280
|
+
d023: byteValueSchema.describe("Raw background color register $D023"),
|
|
281
|
+
d024: byteValueSchema.describe("Raw background color register $D024")
|
|
282
|
+
}),
|
|
283
|
+
screenRam: byteArraySchema.describe("Raw 1000-byte screen matrix contents"),
|
|
284
|
+
colorRam: byteArraySchema.describe("Raw 1000-byte color RAM contents, masked to the low nybble")
|
|
285
|
+
});
|
|
286
|
+
var displayTextResultSchema = import_zod2.z.object({
|
|
287
|
+
graphicsMode: graphicsModeSchema.describe("Decoded VIC-II graphics mode from D011/D016"),
|
|
288
|
+
textMode: import_zod2.z.boolean().describe("Whether the current graphics mode supports direct screen-text decoding"),
|
|
289
|
+
lossy: import_zod2.z.boolean().describe("Whether the screen-code to ASCII translation may lose C64-specific glyph information"),
|
|
290
|
+
columns: import_zod2.z.number().int().positive().describe("Number of text columns decoded per row"),
|
|
291
|
+
rows: import_zod2.z.number().int().positive().describe("Number of decoded text rows"),
|
|
292
|
+
screenRamAddress: address16Schema.describe("Base address of the active 1000-byte screen matrix"),
|
|
293
|
+
textLines: import_zod2.z.array(import_zod2.z.string()).describe("Decoded text rows from screen RAM, trimmed on the right"),
|
|
294
|
+
tokenLines: import_zod2.z.array(import_zod2.z.string()).optional().describe("Optional richer tokenized rows, included only when non-ASCII or ambiguous C64 glyphs are present")
|
|
295
|
+
});
|
|
296
|
+
var keyboardInputResultSchema = import_zod2.z.object({
|
|
297
|
+
action: inputActionSchema.describe("Keyboard action that was applied"),
|
|
298
|
+
keys: import_zod2.z.array(import_zod2.z.string()).min(1).max(4).describe("Normalized symbolic key names"),
|
|
299
|
+
applied: import_zod2.z.boolean().describe("Whether the request was accepted and applied"),
|
|
300
|
+
held: import_zod2.z.boolean().describe("Whether the keys are still treated as held after this request"),
|
|
301
|
+
mode: import_zod2.z.enum(["buffered_text", "buffered_text_repeat"]).describe("Keyboard delivery mode supported by the emulator debug connection")
|
|
302
|
+
});
|
|
303
|
+
var joystickInputResultSchema = import_zod2.z.object({
|
|
304
|
+
port: joystickPortSchema.describe("Joystick port that received the input"),
|
|
305
|
+
action: inputActionSchema.describe("Joystick action that was applied"),
|
|
306
|
+
control: joystickControlSchema.describe("Joystick control that was applied"),
|
|
307
|
+
applied: import_zod2.z.boolean().describe("Whether the request was accepted and applied"),
|
|
308
|
+
state: joystickStateSchema
|
|
309
|
+
});
|
|
310
|
+
var waitForStateResultSchema = import_zod2.z.object({
|
|
311
|
+
executionState: executionStateSchema.describe("Current execution state after waiting"),
|
|
312
|
+
lastStopReason: stopReasonSchema.describe("Reason the emulator most recently stopped in the monitor"),
|
|
313
|
+
runtimeKnown: import_zod2.z.boolean().describe("Whether the monitor has reported a runtime event in this session"),
|
|
314
|
+
programCounter: address16Schema.nullable().describe("Program counter from the latest monitor event, or null if unknown"),
|
|
315
|
+
reachedTarget: import_zod2.z.boolean().describe("Whether the requested target state was reached before timeout"),
|
|
316
|
+
waitedMs: import_zod2.z.number().int().nonnegative().describe("Milliseconds spent waiting")
|
|
317
|
+
});
|
|
318
|
+
function toolOutputSchema(dataSchema) {
|
|
319
|
+
return import_zod2.z.object({
|
|
320
|
+
meta: responseMetaSchema,
|
|
321
|
+
data: dataSchema
|
|
322
|
+
});
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// src/errors.ts
|
|
326
|
+
var ViceMcpError = class extends Error {
|
|
327
|
+
code;
|
|
328
|
+
category;
|
|
329
|
+
retryable;
|
|
330
|
+
details;
|
|
331
|
+
constructor(code, message, category, retryable = false, details) {
|
|
332
|
+
super(message);
|
|
333
|
+
this.name = "ViceMcpError";
|
|
334
|
+
this.code = code;
|
|
335
|
+
this.category = category;
|
|
336
|
+
this.retryable = retryable;
|
|
337
|
+
this.details = details;
|
|
338
|
+
}
|
|
339
|
+
};
|
|
340
|
+
function validationError(message, details) {
|
|
341
|
+
throw new ViceMcpError("validation_error", message, "validation", false, details);
|
|
342
|
+
}
|
|
343
|
+
function debuggerNotPausedError(commandName, details) {
|
|
344
|
+
throw new ViceMcpError(
|
|
345
|
+
"debugger_not_paused",
|
|
346
|
+
`${commandName} can only be executed when the emulator is stopped.`,
|
|
347
|
+
"session_state",
|
|
348
|
+
false,
|
|
349
|
+
{ commandName, requiredState: "stopped", ...details }
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
function emulatorNotRunningError(commandName, details) {
|
|
353
|
+
throw new ViceMcpError(
|
|
354
|
+
"emulator_not_running",
|
|
355
|
+
`${commandName} can only be executed when the emulator is running.`,
|
|
356
|
+
"session_state",
|
|
357
|
+
false,
|
|
358
|
+
{ commandName, requiredState: "running", ...details }
|
|
359
|
+
);
|
|
360
|
+
}
|
|
361
|
+
function unsupportedError(message, details) {
|
|
362
|
+
throw new ViceMcpError("unsupported", message, "unsupported", false, details);
|
|
363
|
+
}
|
|
364
|
+
function asToolError(error) {
|
|
365
|
+
return toolErrorSchema.parse({
|
|
366
|
+
code: error.code,
|
|
367
|
+
message: error.message,
|
|
368
|
+
category: error.category,
|
|
369
|
+
retryable: error.retryable,
|
|
370
|
+
details: error.details
|
|
371
|
+
});
|
|
372
|
+
}
|
|
373
|
+
function zodDetails(error) {
|
|
374
|
+
return {
|
|
375
|
+
issues: error.issues.map((issue) => ({
|
|
376
|
+
code: issue.code,
|
|
377
|
+
message: issue.message,
|
|
378
|
+
path: issue.path
|
|
379
|
+
}))
|
|
380
|
+
};
|
|
381
|
+
}
|
|
382
|
+
function publicMessageFor(error) {
|
|
383
|
+
switch (error.code) {
|
|
384
|
+
case "debugger_not_paused":
|
|
385
|
+
return error.message;
|
|
386
|
+
case "emulator_not_running":
|
|
387
|
+
return error.message;
|
|
388
|
+
case "validation_error":
|
|
389
|
+
case "invalid_prg":
|
|
390
|
+
case "unsupported":
|
|
391
|
+
case "program_file_missing":
|
|
392
|
+
case "program_file_invalid":
|
|
393
|
+
return error.message;
|
|
394
|
+
case "port_allocation_failed":
|
|
395
|
+
case "port_in_use":
|
|
396
|
+
case "monitor_timeout":
|
|
397
|
+
return "The server could not start a usable emulator session. Check the emulator configuration and try again.";
|
|
398
|
+
case "not_connected":
|
|
399
|
+
case "connection_closed":
|
|
400
|
+
case "socket_write_failed":
|
|
401
|
+
case "timeout":
|
|
402
|
+
return "The server could not communicate with the emulator. Try the request again.";
|
|
403
|
+
case "protocol_invalid_stx":
|
|
404
|
+
case "emulator_protocol_error":
|
|
405
|
+
return "The emulator returned an unexpected debugger response. Try the request again.";
|
|
406
|
+
default:
|
|
407
|
+
switch (error.category) {
|
|
408
|
+
case "validation":
|
|
409
|
+
case "session_state":
|
|
410
|
+
case "unsupported":
|
|
411
|
+
return error.message;
|
|
412
|
+
case "configuration":
|
|
413
|
+
case "process_launch":
|
|
414
|
+
return "The server could not start the emulator with the current configuration.";
|
|
415
|
+
case "connection":
|
|
416
|
+
case "timeout":
|
|
417
|
+
return "The server could not communicate with the emulator. Try the request again.";
|
|
418
|
+
case "protocol":
|
|
419
|
+
return "The emulator returned an unexpected debugger response. Try the request again.";
|
|
420
|
+
case "io":
|
|
421
|
+
return "The requested file operation could not be completed.";
|
|
422
|
+
case "internal":
|
|
423
|
+
return "The server hit an unexpected error.";
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
function publicDetailsFor(error) {
|
|
428
|
+
switch (error.category) {
|
|
429
|
+
case "validation":
|
|
430
|
+
case "session_state":
|
|
431
|
+
case "unsupported":
|
|
432
|
+
case "io":
|
|
433
|
+
return error.details;
|
|
434
|
+
default:
|
|
435
|
+
return void 0;
|
|
436
|
+
}
|
|
437
|
+
}
|
|
438
|
+
function normalizeToolError(error) {
|
|
439
|
+
if (error instanceof ViceMcpError) {
|
|
440
|
+
const normalized = asToolError(error);
|
|
441
|
+
return new ViceMcpError(
|
|
442
|
+
normalized.code,
|
|
443
|
+
publicMessageFor(error),
|
|
444
|
+
normalized.category,
|
|
445
|
+
normalized.retryable,
|
|
446
|
+
publicDetailsFor(error)
|
|
447
|
+
);
|
|
448
|
+
}
|
|
449
|
+
if (error instanceof import_zod3.ZodError) {
|
|
450
|
+
return new ViceMcpError(
|
|
451
|
+
"validation_error",
|
|
452
|
+
error.issues.map((issue) => issue.message).join("; ") || "Validation failed",
|
|
453
|
+
"validation",
|
|
454
|
+
false,
|
|
455
|
+
zodDetails(error)
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
if (error instanceof Error) {
|
|
459
|
+
return new ViceMcpError("internal_error", "The server hit an unexpected error.", "internal", false);
|
|
460
|
+
}
|
|
461
|
+
return new ViceMcpError("internal_error", "The server hit an unexpected error.", "internal", false);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
// src/session.ts
|
|
465
|
+
var import_promises = __toESM(require("fs/promises"), 1);
|
|
466
|
+
var import_node_fs = require("fs");
|
|
467
|
+
var import_node_net2 = __toESM(require("net"), 1);
|
|
468
|
+
var import_node_os = __toESM(require("os"), 1);
|
|
469
|
+
var import_node_path = __toESM(require("path"), 1);
|
|
470
|
+
var import_node_child_process = require("child_process");
|
|
471
|
+
var import_node_zlib = __toESM(require("zlib"), 1);
|
|
472
|
+
|
|
473
|
+
// src/vice-protocol.ts
|
|
474
|
+
var import_node_events = require("events");
|
|
475
|
+
var import_node_net = __toESM(require("net"), 1);
|
|
476
|
+
function parseLittleEndianVariableWidth(bytes) {
|
|
477
|
+
let value = 0;
|
|
478
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
479
|
+
value += (bytes[index] ?? 0) * 2 ** (index * 8);
|
|
480
|
+
}
|
|
481
|
+
return value;
|
|
482
|
+
}
|
|
483
|
+
function encodeHeader(commandType, requestId, body) {
|
|
484
|
+
const header = Buffer.alloc(11);
|
|
485
|
+
header[0] = VICE_STX;
|
|
486
|
+
header[1] = VICE_API_VERSION;
|
|
487
|
+
header.writeUInt32LE(body.length, 2);
|
|
488
|
+
header.writeUInt32LE(requestId, 6);
|
|
489
|
+
header[10] = commandType;
|
|
490
|
+
return Buffer.concat([header, body]);
|
|
491
|
+
}
|
|
492
|
+
function parseBuffer(buffer) {
|
|
493
|
+
const responses = [];
|
|
494
|
+
let offset = 0;
|
|
495
|
+
while (offset + 12 <= buffer.length) {
|
|
496
|
+
if (buffer[offset] !== VICE_STX) {
|
|
497
|
+
throw new ViceMcpError("protocol_invalid_stx", "Invalid response prefix from emulator debug connection", "protocol");
|
|
498
|
+
}
|
|
499
|
+
const bodyLength = buffer.readUInt32LE(offset + 2);
|
|
500
|
+
const frameLength = 12 + bodyLength;
|
|
501
|
+
if (offset + frameLength > buffer.length) {
|
|
502
|
+
break;
|
|
503
|
+
}
|
|
504
|
+
const body = buffer.subarray(offset + 12, offset + frameLength);
|
|
505
|
+
const responseType = buffer[offset + 6];
|
|
506
|
+
const errorCode = buffer[offset + 7];
|
|
507
|
+
const requestId = buffer.readUInt32LE(offset + 8);
|
|
508
|
+
responses.push(parseResponse(responseType, errorCode, requestId, body));
|
|
509
|
+
offset += frameLength;
|
|
510
|
+
}
|
|
511
|
+
return { responses, remainder: buffer.subarray(offset) };
|
|
512
|
+
}
|
|
513
|
+
function parseResponse(responseType, errorCode, requestId, body) {
|
|
514
|
+
switch (responseType) {
|
|
515
|
+
case 1 /* MemoryGet */: {
|
|
516
|
+
const length = errorCode === 0 /* OK */ ? body.readUInt16LE(0) : 0;
|
|
517
|
+
return { type: "memory_get", requestId, errorCode, bytes: body.subarray(2, 2 + length) };
|
|
518
|
+
}
|
|
519
|
+
case 49 /* RegisterInfo */: {
|
|
520
|
+
const count = errorCode === 0 /* OK */ ? body.readUInt16LE(0) : 0;
|
|
521
|
+
const registers = Array.from({ length: count }, (_, index) => {
|
|
522
|
+
const start = 2 + index * 4;
|
|
523
|
+
return { id: body[start + 1], value: body.readUInt16LE(start + 2) };
|
|
524
|
+
});
|
|
525
|
+
return { type: "registers", requestId, errorCode, registers };
|
|
526
|
+
}
|
|
527
|
+
case 131 /* RegistersAvailable */: {
|
|
528
|
+
const count = errorCode === 0 /* OK */ ? body.readUInt16LE(0) : 0;
|
|
529
|
+
let offset = 2;
|
|
530
|
+
const registers = [];
|
|
531
|
+
for (let index = 0; index < count; index += 1) {
|
|
532
|
+
const itemSize = body[offset];
|
|
533
|
+
const id = body[offset + 1];
|
|
534
|
+
const size = body[offset + 2];
|
|
535
|
+
const nameLength = body[offset + 3];
|
|
536
|
+
const name = body.subarray(offset + 4, offset + 4 + nameLength).toString("ascii");
|
|
537
|
+
registers.push({ id, size, name });
|
|
538
|
+
offset += itemSize + 1;
|
|
539
|
+
}
|
|
540
|
+
return { type: "registers_available", requestId, errorCode, registers };
|
|
541
|
+
}
|
|
542
|
+
case 133 /* Info */: {
|
|
543
|
+
const mainVersionLength = body[0] ?? 0;
|
|
544
|
+
const version = Array.from(body.subarray(1, 1 + mainVersionLength));
|
|
545
|
+
const svnLengthOffset = 1 + mainVersionLength;
|
|
546
|
+
const svnLength = body[svnLengthOffset] ?? 0;
|
|
547
|
+
const svnBytes = body.subarray(svnLengthOffset + 1, svnLengthOffset + 1 + svnLength);
|
|
548
|
+
return {
|
|
549
|
+
type: "info",
|
|
550
|
+
requestId,
|
|
551
|
+
errorCode,
|
|
552
|
+
version,
|
|
553
|
+
versionString: version.join("."),
|
|
554
|
+
svnVersion: parseLittleEndianVariableWidth(svnBytes)
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
case 17 /* CheckpointInfo */: {
|
|
558
|
+
const operation = body[11] ?? 4;
|
|
559
|
+
const checkpoint = {
|
|
560
|
+
id: body.readUInt32LE(0),
|
|
561
|
+
currentlyHit: body[4] === 1,
|
|
562
|
+
start: body.readUInt16LE(5),
|
|
563
|
+
end: body.readUInt16LE(7),
|
|
564
|
+
stopWhenHit: body[9] === 1,
|
|
565
|
+
enabled: body[10] === 1,
|
|
566
|
+
kind: cpuOperationToBreakpointKind(operation),
|
|
567
|
+
temporary: body[12] === 1,
|
|
568
|
+
hitCount: body.readUInt32LE(13),
|
|
569
|
+
ignoreCount: body.readUInt32LE(17),
|
|
570
|
+
hasCondition: body[21] === 1
|
|
571
|
+
};
|
|
572
|
+
return { type: "checkpoint_info", requestId, errorCode, checkpoint };
|
|
573
|
+
}
|
|
574
|
+
case 20 /* CheckpointList */: {
|
|
575
|
+
return {
|
|
576
|
+
type: "checkpoint_list",
|
|
577
|
+
requestId,
|
|
578
|
+
errorCode,
|
|
579
|
+
total: errorCode === 0 /* OK */ ? body.readUInt32LE(0) : 0,
|
|
580
|
+
checkpoints: []
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
case 132 /* DisplayGet */: {
|
|
584
|
+
const infoLength = errorCode === 0 /* OK */ ? body.readUInt32LE(0) : 0;
|
|
585
|
+
const imageLength = errorCode === 0 /* OK */ ? body.readUInt32LE(17) : 0;
|
|
586
|
+
return {
|
|
587
|
+
type: "display",
|
|
588
|
+
requestId,
|
|
589
|
+
errorCode,
|
|
590
|
+
debugWidth: body.readUInt16LE(4),
|
|
591
|
+
debugHeight: body.readUInt16LE(6),
|
|
592
|
+
debugOffsetX: body.readUInt16LE(8),
|
|
593
|
+
debugOffsetY: body.readUInt16LE(10),
|
|
594
|
+
innerWidth: body.readUInt16LE(12),
|
|
595
|
+
innerHeight: body.readUInt16LE(14),
|
|
596
|
+
bitsPerPixel: body[16] ?? 0,
|
|
597
|
+
imageBytes: body.subarray(infoLength + 4, infoLength + 4 + imageLength)
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
case 145 /* PaletteGet */: {
|
|
601
|
+
const count = errorCode === 0 /* OK */ ? body.readUInt16LE(0) : 0;
|
|
602
|
+
let offset = 2;
|
|
603
|
+
const items = [];
|
|
604
|
+
for (let index = 0; index < count; index += 1) {
|
|
605
|
+
const itemSize = body[offset] ?? 0;
|
|
606
|
+
items.push({
|
|
607
|
+
index,
|
|
608
|
+
red: body[offset + 1] ?? 0,
|
|
609
|
+
green: body[offset + 2] ?? 0,
|
|
610
|
+
blue: body[offset + 3] ?? 0
|
|
611
|
+
});
|
|
612
|
+
offset += itemSize + 1;
|
|
613
|
+
}
|
|
614
|
+
return { type: "palette", requestId, errorCode, items };
|
|
615
|
+
}
|
|
616
|
+
case 98 /* Stopped */:
|
|
617
|
+
return { type: "stopped", requestId, errorCode, programCounter: body.readUInt16LE(0) };
|
|
618
|
+
case 99 /* Resumed */:
|
|
619
|
+
return { type: "resumed", requestId, errorCode, programCounter: body.readUInt16LE(0) };
|
|
620
|
+
case 97 /* Jam */:
|
|
621
|
+
return { type: "jam", requestId, errorCode, programCounter: body.readUInt16LE(0) };
|
|
622
|
+
case 66 /* Undump */:
|
|
623
|
+
return { type: "undump", requestId, errorCode, programCounter: body.readUInt16LE(0) };
|
|
624
|
+
default:
|
|
625
|
+
return { type: "empty", requestId, errorCode, responseType };
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
var ViceMonitorClient = class extends import_node_events.EventEmitter {
|
|
629
|
+
#socket = null;
|
|
630
|
+
#buffer = Buffer.alloc(0);
|
|
631
|
+
#nextRequestId = 1;
|
|
632
|
+
#pending = /* @__PURE__ */ new Map();
|
|
633
|
+
#chain = Promise.resolve();
|
|
634
|
+
#host = null;
|
|
635
|
+
#port = null;
|
|
636
|
+
#runtimeState = {
|
|
637
|
+
connected: false,
|
|
638
|
+
runtimeKnown: false,
|
|
639
|
+
lastEventType: "unknown",
|
|
640
|
+
programCounter: null
|
|
641
|
+
};
|
|
642
|
+
get connected() {
|
|
643
|
+
return this.#socket != null && !this.#socket.destroyed;
|
|
644
|
+
}
|
|
645
|
+
runtimeState() {
|
|
646
|
+
return { ...this.#runtimeState };
|
|
647
|
+
}
|
|
648
|
+
async connect(host2, port2) {
|
|
649
|
+
if (this.connected && this.#host === host2 && this.#port === port2) {
|
|
650
|
+
return;
|
|
651
|
+
}
|
|
652
|
+
await this.disconnect();
|
|
653
|
+
this.#host = host2;
|
|
654
|
+
this.#port = port2;
|
|
655
|
+
this.#buffer = Buffer.alloc(0);
|
|
656
|
+
await new Promise((resolve, reject) => {
|
|
657
|
+
const socket = import_node_net.default.createConnection({ host: host2, port: port2 }, () => {
|
|
658
|
+
this.#socket = socket;
|
|
659
|
+
this.#runtimeState = {
|
|
660
|
+
connected: true,
|
|
661
|
+
runtimeKnown: false,
|
|
662
|
+
lastEventType: "unknown",
|
|
663
|
+
programCounter: null
|
|
664
|
+
};
|
|
665
|
+
resolve();
|
|
666
|
+
});
|
|
667
|
+
socket.on("data", (chunk) => this.#onData(chunk));
|
|
668
|
+
socket.on("close", () => this.#onClose());
|
|
669
|
+
socket.on("error", (error) => {
|
|
670
|
+
if (!this.#socket) {
|
|
671
|
+
reject(error);
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
this.emit("transport-error", error);
|
|
675
|
+
});
|
|
676
|
+
});
|
|
677
|
+
}
|
|
678
|
+
async disconnect() {
|
|
679
|
+
for (const pending of this.#pending.values()) {
|
|
680
|
+
clearTimeout(pending.timer);
|
|
681
|
+
pending.reject(new ViceMcpError("connection_closed", "Emulator debug connection closed", "connection", true));
|
|
682
|
+
}
|
|
683
|
+
this.#pending.clear();
|
|
684
|
+
if (!this.#socket) {
|
|
685
|
+
return;
|
|
686
|
+
}
|
|
687
|
+
const socket = this.#socket;
|
|
688
|
+
this.#socket = null;
|
|
689
|
+
this.#runtimeState = {
|
|
690
|
+
connected: false,
|
|
691
|
+
runtimeKnown: false,
|
|
692
|
+
lastEventType: "unknown",
|
|
693
|
+
programCounter: null
|
|
694
|
+
};
|
|
695
|
+
await new Promise((resolve) => {
|
|
696
|
+
socket.once("close", () => resolve());
|
|
697
|
+
socket.destroy();
|
|
698
|
+
});
|
|
699
|
+
}
|
|
700
|
+
async ping(timeoutMs = 2e3) {
|
|
701
|
+
await this.send(129 /* Ping */, Buffer.alloc(0), timeoutMs);
|
|
702
|
+
}
|
|
703
|
+
async getInfo() {
|
|
704
|
+
return this.send(133 /* Info */, Buffer.alloc(0));
|
|
705
|
+
}
|
|
706
|
+
async captureDisplay(useVic = true) {
|
|
707
|
+
return this.send(132 /* DisplayGet */, Buffer.from([useVic ? 1 : 0, 0]));
|
|
708
|
+
}
|
|
709
|
+
async getPalette(useVic = true) {
|
|
710
|
+
return this.send(145 /* PaletteGet */, Buffer.from([useVic ? 1 : 0]));
|
|
711
|
+
}
|
|
712
|
+
async getRegistersAvailable() {
|
|
713
|
+
return this.send(131 /* RegistersAvailable */, Buffer.from([mainMemSpaceToProtocol()]));
|
|
714
|
+
}
|
|
715
|
+
async getRegisters() {
|
|
716
|
+
return this.send(49 /* RegistersGet */, Buffer.from([mainMemSpaceToProtocol()]));
|
|
717
|
+
}
|
|
718
|
+
async setRegisters(registers) {
|
|
719
|
+
const body = Buffer.alloc(3 + registers.length * 4);
|
|
720
|
+
body[0] = mainMemSpaceToProtocol();
|
|
721
|
+
body.writeUInt16LE(registers.length, 1);
|
|
722
|
+
registers.forEach((register, index) => {
|
|
723
|
+
const offset = 3 + index * 4;
|
|
724
|
+
body[offset] = 3;
|
|
725
|
+
body[offset + 1] = register.id;
|
|
726
|
+
body.writeUInt16LE(register.value, offset + 2);
|
|
727
|
+
});
|
|
728
|
+
return this.send(50 /* RegistersSet */, body);
|
|
729
|
+
}
|
|
730
|
+
async readMemory(start, end, bankId = 0) {
|
|
731
|
+
const body = Buffer.alloc(8);
|
|
732
|
+
body[0] = 0;
|
|
733
|
+
body.writeUInt16LE(start, 1);
|
|
734
|
+
body.writeUInt16LE(end, 3);
|
|
735
|
+
body[5] = mainMemSpaceToProtocol();
|
|
736
|
+
body.writeUInt16LE(bankId, 6);
|
|
737
|
+
return this.send(1 /* MemoryGet */, body);
|
|
738
|
+
}
|
|
739
|
+
async writeMemory(start, bytes, bankId = 0) {
|
|
740
|
+
const body = Buffer.alloc(8 + bytes.length);
|
|
741
|
+
body[0] = 0;
|
|
742
|
+
body.writeUInt16LE(start, 1);
|
|
743
|
+
body.writeUInt16LE(start + bytes.length - 1, 3);
|
|
744
|
+
body[5] = mainMemSpaceToProtocol();
|
|
745
|
+
body.writeUInt16LE(bankId, 6);
|
|
746
|
+
Buffer.from(bytes).copy(body, 8);
|
|
747
|
+
return this.send(2 /* MemorySet */, body);
|
|
748
|
+
}
|
|
749
|
+
async continueExecution() {
|
|
750
|
+
return this.send(170 /* Exit */, Buffer.alloc(0));
|
|
751
|
+
}
|
|
752
|
+
async stepInstruction(count = 1, stepOver = false) {
|
|
753
|
+
const body = Buffer.alloc(3);
|
|
754
|
+
body[0] = stepOver ? 1 : 0;
|
|
755
|
+
body.writeUInt16LE(count, 1);
|
|
756
|
+
return this.send(113 /* AdvanceInstruction */, body);
|
|
757
|
+
}
|
|
758
|
+
async stepOut() {
|
|
759
|
+
return this.send(115 /* ExecuteUntilReturn */, Buffer.alloc(0));
|
|
760
|
+
}
|
|
761
|
+
async reset(mode) {
|
|
762
|
+
const body = Buffer.from([mode === "hard" ? 1 : 0]);
|
|
763
|
+
return this.send(204 /* Reset */, body);
|
|
764
|
+
}
|
|
765
|
+
async setBreakpoint(options) {
|
|
766
|
+
const body = Buffer.alloc(8);
|
|
767
|
+
body.writeUInt16LE(options.start, 0);
|
|
768
|
+
body.writeUInt16LE(options.end ?? options.start, 2);
|
|
769
|
+
body[4] = options.stopWhenHit === false ? 0 : 1;
|
|
770
|
+
body[5] = options.enabled === false ? 0 : 1;
|
|
771
|
+
body[6] = breakpointKindToOperation(options.kind);
|
|
772
|
+
body[7] = options.temporary ? 1 : 0;
|
|
773
|
+
const response = await this.send(18 /* CheckpointSet */, body);
|
|
774
|
+
if (options.condition && response.type === "checkpoint_info") {
|
|
775
|
+
const conditionBody = Buffer.alloc(5 + options.condition.length);
|
|
776
|
+
conditionBody.writeUInt32LE(response.checkpoint.id, 0);
|
|
777
|
+
conditionBody[4] = options.condition.length;
|
|
778
|
+
conditionBody.write(options.condition, 5, "ascii");
|
|
779
|
+
await this.send(34 /* ConditionSet */, conditionBody);
|
|
780
|
+
return {
|
|
781
|
+
...response,
|
|
782
|
+
checkpoint: {
|
|
783
|
+
...response.checkpoint,
|
|
784
|
+
hasCondition: true
|
|
785
|
+
}
|
|
786
|
+
};
|
|
787
|
+
}
|
|
788
|
+
return response;
|
|
789
|
+
}
|
|
790
|
+
async getBreakpoint(id) {
|
|
791
|
+
const body = Buffer.alloc(4);
|
|
792
|
+
body.writeUInt32LE(id, 0);
|
|
793
|
+
return this.send(17 /* CheckpointGet */, body);
|
|
794
|
+
}
|
|
795
|
+
async listBreakpoints() {
|
|
796
|
+
return this.send(20 /* CheckpointList */, Buffer.alloc(0));
|
|
797
|
+
}
|
|
798
|
+
async deleteBreakpoint(id) {
|
|
799
|
+
const body = Buffer.alloc(4);
|
|
800
|
+
body.writeUInt32LE(id, 0);
|
|
801
|
+
return this.send(19 /* CheckpointDelete */, body);
|
|
802
|
+
}
|
|
803
|
+
async toggleBreakpoint(id, enabled) {
|
|
804
|
+
const body = Buffer.alloc(5);
|
|
805
|
+
body.writeUInt32LE(id, 0);
|
|
806
|
+
body[4] = enabled ? 1 : 0;
|
|
807
|
+
return this.send(21 /* CheckpointToggle */, body);
|
|
808
|
+
}
|
|
809
|
+
async setBreakpointCondition(id, condition) {
|
|
810
|
+
const conditionBytes = Buffer.from(condition, "ascii");
|
|
811
|
+
const body = Buffer.alloc(5 + conditionBytes.length);
|
|
812
|
+
body.writeUInt32LE(id, 0);
|
|
813
|
+
body[4] = conditionBytes.length;
|
|
814
|
+
conditionBytes.copy(body, 5);
|
|
815
|
+
return this.send(34 /* ConditionSet */, body);
|
|
816
|
+
}
|
|
817
|
+
async autostartProgram(filename, autoStart, fileIndex = 0) {
|
|
818
|
+
const body = Buffer.alloc(4 + Buffer.byteLength(filename));
|
|
819
|
+
body[0] = autoStart ? 1 : 0;
|
|
820
|
+
body.writeUInt16LE(fileIndex, 1);
|
|
821
|
+
body[3] = Buffer.byteLength(filename);
|
|
822
|
+
body.write(filename, 4, "ascii");
|
|
823
|
+
return this.send(221 /* AutoStart */, body);
|
|
824
|
+
}
|
|
825
|
+
async quit() {
|
|
826
|
+
return this.send(187 /* Quit */, Buffer.alloc(0));
|
|
827
|
+
}
|
|
828
|
+
async sendKeys(text) {
|
|
829
|
+
const encoded = Buffer.from(text, "binary");
|
|
830
|
+
const body = Buffer.alloc(1 + encoded.length);
|
|
831
|
+
body[0] = encoded.length;
|
|
832
|
+
encoded.copy(body, 1);
|
|
833
|
+
return this.send(114 /* KeyboardFeed */, body);
|
|
834
|
+
}
|
|
835
|
+
async setJoyport(port2, value) {
|
|
836
|
+
const body = Buffer.alloc(4);
|
|
837
|
+
body.writeUInt16LE(port2, 0);
|
|
838
|
+
body.writeUInt16LE(value, 2);
|
|
839
|
+
return this.send(162 /* JoyportSet */, body);
|
|
840
|
+
}
|
|
841
|
+
async send(commandType, body, timeoutMs = 5e3) {
|
|
842
|
+
const next = this.#chain.catch(() => void 0).then(async () => this.#execute(commandType, body, timeoutMs));
|
|
843
|
+
this.#chain = next.then(
|
|
844
|
+
() => void 0,
|
|
845
|
+
() => void 0
|
|
846
|
+
);
|
|
847
|
+
return next;
|
|
848
|
+
}
|
|
849
|
+
async #execute(commandType, body, timeoutMs) {
|
|
850
|
+
if (!this.#socket || this.#socket.destroyed) {
|
|
851
|
+
throw new ViceMcpError("not_connected", "Emulator debug connection is not connected", "connection", true);
|
|
852
|
+
}
|
|
853
|
+
const requestId = this.#nextRequestId++;
|
|
854
|
+
const packet = encodeHeader(commandType, requestId, body);
|
|
855
|
+
return await new Promise((resolve, reject) => {
|
|
856
|
+
const timer = setTimeout(() => {
|
|
857
|
+
this.#pending.delete(requestId);
|
|
858
|
+
reject(new ViceMcpError("timeout", `Emulator debug command timed out (0x${commandType.toString(16)})`, "timeout", true));
|
|
859
|
+
}, timeoutMs);
|
|
860
|
+
this.#pending.set(requestId, {
|
|
861
|
+
type: commandType,
|
|
862
|
+
timer,
|
|
863
|
+
resolve: (response) => resolve(response),
|
|
864
|
+
reject,
|
|
865
|
+
linkedCheckpointInfo: commandType === 20 /* CheckpointList */ ? [] : void 0
|
|
866
|
+
});
|
|
867
|
+
this.#socket.write(packet, (error) => {
|
|
868
|
+
if (error) {
|
|
869
|
+
clearTimeout(timer);
|
|
870
|
+
this.#pending.delete(requestId);
|
|
871
|
+
reject(new ViceMcpError("socket_write_failed", error.message, "connection", true));
|
|
872
|
+
}
|
|
873
|
+
});
|
|
874
|
+
});
|
|
875
|
+
}
|
|
876
|
+
#onData(chunk) {
|
|
877
|
+
this.#buffer = Buffer.concat([this.#buffer, chunk]);
|
|
878
|
+
const { responses, remainder } = parseBuffer(this.#buffer);
|
|
879
|
+
this.#buffer = Buffer.from(remainder);
|
|
880
|
+
for (const response of responses) {
|
|
881
|
+
this.emit("response", response);
|
|
882
|
+
if (response.requestId === VICE_BROADCAST_REQUEST_ID) {
|
|
883
|
+
this.#applyRuntimeResponse(response);
|
|
884
|
+
this.emit("event", response);
|
|
885
|
+
continue;
|
|
886
|
+
}
|
|
887
|
+
const pending = this.#pending.get(response.requestId);
|
|
888
|
+
if (!pending) {
|
|
889
|
+
this.emit("event", response);
|
|
890
|
+
continue;
|
|
891
|
+
}
|
|
892
|
+
if (pending.type === 20 /* CheckpointList */ && response.type === "checkpoint_info") {
|
|
893
|
+
pending.linkedCheckpointInfo?.push(response);
|
|
894
|
+
continue;
|
|
895
|
+
}
|
|
896
|
+
clearTimeout(pending.timer);
|
|
897
|
+
this.#pending.delete(response.requestId);
|
|
898
|
+
if (response.errorCode !== 0 /* OK */) {
|
|
899
|
+
pending.reject(
|
|
900
|
+
new ViceMcpError("emulator_protocol_error", `Emulator returned error ${response.errorCode}`, "protocol", false, {
|
|
901
|
+
commandType: pending.type,
|
|
902
|
+
requestId: response.requestId,
|
|
903
|
+
emulatorErrorCode: response.errorCode
|
|
904
|
+
})
|
|
905
|
+
);
|
|
906
|
+
continue;
|
|
907
|
+
}
|
|
908
|
+
if (pending.type === 20 /* CheckpointList */ && response.type === "checkpoint_list") {
|
|
909
|
+
if (!pending.linkedCheckpointInfo) {
|
|
910
|
+
pending.linkedCheckpointInfo = [];
|
|
911
|
+
}
|
|
912
|
+
response.checkpoints = pending.linkedCheckpointInfo.map((entry) => entry.checkpoint);
|
|
913
|
+
}
|
|
914
|
+
pending.resolve(response);
|
|
915
|
+
}
|
|
916
|
+
}
|
|
917
|
+
#onClose() {
|
|
918
|
+
const pendingError = new ViceMcpError("connection_closed", "Emulator debug connection closed", "connection", true);
|
|
919
|
+
for (const pending of this.#pending.values()) {
|
|
920
|
+
clearTimeout(pending.timer);
|
|
921
|
+
pending.reject(pendingError);
|
|
922
|
+
}
|
|
923
|
+
this.#pending.clear();
|
|
924
|
+
this.#socket = null;
|
|
925
|
+
this.#runtimeState = {
|
|
926
|
+
connected: false,
|
|
927
|
+
runtimeKnown: false,
|
|
928
|
+
lastEventType: "unknown",
|
|
929
|
+
programCounter: null
|
|
930
|
+
};
|
|
931
|
+
this.emit("close");
|
|
932
|
+
}
|
|
933
|
+
#applyRuntimeResponse(response) {
|
|
934
|
+
switch (response.type) {
|
|
935
|
+
case "resumed":
|
|
936
|
+
case "stopped":
|
|
937
|
+
case "jam":
|
|
938
|
+
this.#runtimeState = {
|
|
939
|
+
connected: this.connected,
|
|
940
|
+
runtimeKnown: true,
|
|
941
|
+
lastEventType: response.type,
|
|
942
|
+
programCounter: response.programCounter
|
|
943
|
+
};
|
|
944
|
+
break;
|
|
945
|
+
default:
|
|
946
|
+
break;
|
|
947
|
+
}
|
|
948
|
+
}
|
|
949
|
+
};
|
|
950
|
+
|
|
951
|
+
// src/session.ts
|
|
952
|
+
function sleep(ms) {
|
|
953
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
954
|
+
}
|
|
955
|
+
function nowIso() {
|
|
956
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
957
|
+
}
|
|
958
|
+
function defaultC64Config() {
|
|
959
|
+
return c64ConfigSchema.parse({});
|
|
960
|
+
}
|
|
961
|
+
async function buildViceLaunchEnv() {
|
|
962
|
+
const env = { ...process.env };
|
|
963
|
+
const uid = import_node_os.default.userInfo().uid;
|
|
964
|
+
const runtimeDir = env.XDG_RUNTIME_DIR || `/run/user/${uid}`;
|
|
965
|
+
env.XDG_RUNTIME_DIR ||= runtimeDir;
|
|
966
|
+
if (!env.WAYLAND_DISPLAY) {
|
|
967
|
+
const waylandDisplay = await firstRuntimeEntry(runtimeDir, /^wayland-\d+$/);
|
|
968
|
+
if (waylandDisplay) {
|
|
969
|
+
env.WAYLAND_DISPLAY = waylandDisplay;
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
if (!env.XAUTHORITY) {
|
|
973
|
+
const xauthority = await firstRuntimeEntry(runtimeDir, /^\.mutter-Xwaylandauth\./, true);
|
|
974
|
+
if (xauthority) {
|
|
975
|
+
env.XAUTHORITY = xauthority;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
if (!env.DISPLAY) {
|
|
979
|
+
env.DISPLAY = ":0";
|
|
980
|
+
}
|
|
981
|
+
return env;
|
|
982
|
+
}
|
|
983
|
+
async function firstRuntimeEntry(runtimeDir, pattern, returnAbsolutePath = false) {
|
|
984
|
+
try {
|
|
985
|
+
const entries = await import_promises.default.readdir(runtimeDir);
|
|
986
|
+
const match = entries.find((entry) => pattern.test(entry));
|
|
987
|
+
if (!match) {
|
|
988
|
+
return null;
|
|
989
|
+
}
|
|
990
|
+
return returnAbsolutePath ? import_node_path.default.join(runtimeDir, match) : match;
|
|
991
|
+
} catch {
|
|
992
|
+
return null;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
function makeWarning(message, code = "warning") {
|
|
996
|
+
return { code, message };
|
|
997
|
+
}
|
|
998
|
+
function lowNibble(value) {
|
|
999
|
+
return value & 15;
|
|
1000
|
+
}
|
|
1001
|
+
function decodeVicBankAddress(dd00) {
|
|
1002
|
+
return ((dd00 ^ 3) & 3) * 16384;
|
|
1003
|
+
}
|
|
1004
|
+
function decodeGraphicsMode(d011, d016) {
|
|
1005
|
+
const extendedColorMode = (d011 & 64) !== 0;
|
|
1006
|
+
const bitmapMode = (d011 & 32) !== 0;
|
|
1007
|
+
const multicolorMode = (d016 & 16) !== 0;
|
|
1008
|
+
let graphicsMode;
|
|
1009
|
+
if (!extendedColorMode && !bitmapMode && !multicolorMode) {
|
|
1010
|
+
graphicsMode = "standard_text";
|
|
1011
|
+
} else if (!extendedColorMode && !bitmapMode && multicolorMode) {
|
|
1012
|
+
graphicsMode = "multicolor_text";
|
|
1013
|
+
} else if (!extendedColorMode && bitmapMode && !multicolorMode) {
|
|
1014
|
+
graphicsMode = "standard_bitmap";
|
|
1015
|
+
} else if (!extendedColorMode && bitmapMode && multicolorMode) {
|
|
1016
|
+
graphicsMode = "multicolor_bitmap";
|
|
1017
|
+
} else if (extendedColorMode && !bitmapMode && !multicolorMode) {
|
|
1018
|
+
graphicsMode = "extended_background_color_text";
|
|
1019
|
+
} else if (extendedColorMode && !bitmapMode && multicolorMode) {
|
|
1020
|
+
graphicsMode = "invalid_text_mode";
|
|
1021
|
+
} else if (extendedColorMode && bitmapMode && !multicolorMode) {
|
|
1022
|
+
graphicsMode = "invalid_bitmap_mode_1";
|
|
1023
|
+
} else {
|
|
1024
|
+
graphicsMode = "invalid_bitmap_mode_2";
|
|
1025
|
+
}
|
|
1026
|
+
return {
|
|
1027
|
+
graphicsMode,
|
|
1028
|
+
extendedColorMode,
|
|
1029
|
+
bitmapMode,
|
|
1030
|
+
multicolorMode
|
|
1031
|
+
};
|
|
1032
|
+
}
|
|
1033
|
+
function isTextGraphicsMode(graphicsMode) {
|
|
1034
|
+
return graphicsMode === "standard_text" || graphicsMode === "multicolor_text" || graphicsMode === "extended_background_color_text" || graphicsMode === "invalid_text_mode";
|
|
1035
|
+
}
|
|
1036
|
+
function petsciiToScreenCode(value) {
|
|
1037
|
+
if (value === 255) {
|
|
1038
|
+
return 94;
|
|
1039
|
+
}
|
|
1040
|
+
if (value < 32) {
|
|
1041
|
+
return value ^ 128;
|
|
1042
|
+
}
|
|
1043
|
+
if (value < 96) {
|
|
1044
|
+
return value & 63;
|
|
1045
|
+
}
|
|
1046
|
+
if (value < 128) {
|
|
1047
|
+
return value & 95;
|
|
1048
|
+
}
|
|
1049
|
+
if (value < 160) {
|
|
1050
|
+
return value | 64;
|
|
1051
|
+
}
|
|
1052
|
+
if (value < 192) {
|
|
1053
|
+
return value ^ 192;
|
|
1054
|
+
}
|
|
1055
|
+
if (value < 255) {
|
|
1056
|
+
return value ^ 128;
|
|
1057
|
+
}
|
|
1058
|
+
return 94;
|
|
1059
|
+
}
|
|
1060
|
+
function decodeScreenCodeCell(code) {
|
|
1061
|
+
if (code === 32 || code === 160) {
|
|
1062
|
+
return { ascii: " ", lossy: false };
|
|
1063
|
+
}
|
|
1064
|
+
if (code >= 1 && code <= 26) {
|
|
1065
|
+
return { ascii: String.fromCharCode(64 + code), lossy: false };
|
|
1066
|
+
}
|
|
1067
|
+
if (code >= 48 && code <= 57) {
|
|
1068
|
+
return { ascii: String.fromCharCode(code), lossy: false };
|
|
1069
|
+
}
|
|
1070
|
+
switch (code) {
|
|
1071
|
+
case 0:
|
|
1072
|
+
return { ascii: "@", lossy: false };
|
|
1073
|
+
case 27:
|
|
1074
|
+
return { ascii: "[", lossy: false };
|
|
1075
|
+
case 28:
|
|
1076
|
+
return { ascii: "\xA3", lossy: false };
|
|
1077
|
+
case 29:
|
|
1078
|
+
return { ascii: "]", lossy: false };
|
|
1079
|
+
case 34:
|
|
1080
|
+
return { ascii: '"', lossy: false };
|
|
1081
|
+
case 35:
|
|
1082
|
+
return { ascii: "#", lossy: false };
|
|
1083
|
+
case 36:
|
|
1084
|
+
return { ascii: "$", lossy: false };
|
|
1085
|
+
case 37:
|
|
1086
|
+
return { ascii: "%", lossy: false };
|
|
1087
|
+
case 38:
|
|
1088
|
+
return { ascii: "&", lossy: false };
|
|
1089
|
+
case 39:
|
|
1090
|
+
return { ascii: "'", lossy: false };
|
|
1091
|
+
case 40:
|
|
1092
|
+
return { ascii: "(", lossy: false };
|
|
1093
|
+
case 41:
|
|
1094
|
+
return { ascii: ")", lossy: false };
|
|
1095
|
+
case 42:
|
|
1096
|
+
return { ascii: "*", lossy: false };
|
|
1097
|
+
case 43:
|
|
1098
|
+
return { ascii: "+", lossy: false };
|
|
1099
|
+
case 44:
|
|
1100
|
+
return { ascii: ",", lossy: false };
|
|
1101
|
+
case 45:
|
|
1102
|
+
return { ascii: "-", lossy: false };
|
|
1103
|
+
case 46:
|
|
1104
|
+
return { ascii: ".", lossy: false };
|
|
1105
|
+
case 47:
|
|
1106
|
+
return { ascii: "/", lossy: false };
|
|
1107
|
+
case 58:
|
|
1108
|
+
return { ascii: ":", lossy: false };
|
|
1109
|
+
case 59:
|
|
1110
|
+
return { ascii: ";", lossy: false };
|
|
1111
|
+
case 60:
|
|
1112
|
+
return { ascii: "\u2191", lossy: false };
|
|
1113
|
+
case 61:
|
|
1114
|
+
return { ascii: "=", lossy: false };
|
|
1115
|
+
case 62:
|
|
1116
|
+
return { ascii: "\u2190", lossy: false };
|
|
1117
|
+
case 63:
|
|
1118
|
+
return { ascii: "?", lossy: false };
|
|
1119
|
+
case 94:
|
|
1120
|
+
return { ascii: "\u03C0", lossy: false };
|
|
1121
|
+
default:
|
|
1122
|
+
return {
|
|
1123
|
+
ascii: "\uFFFD",
|
|
1124
|
+
token: SCREEN_CODE_TOKEN_MAP.get(code) ?? `<SC:${code}>`,
|
|
1125
|
+
lossy: true
|
|
1126
|
+
};
|
|
1127
|
+
}
|
|
1128
|
+
}
|
|
1129
|
+
function uint32(value) {
|
|
1130
|
+
const buffer = Buffer.alloc(4);
|
|
1131
|
+
buffer.writeUInt32BE(value, 0);
|
|
1132
|
+
return buffer;
|
|
1133
|
+
}
|
|
1134
|
+
var CRC_TABLE = new Uint32Array(256).map((_, index) => {
|
|
1135
|
+
let c = index;
|
|
1136
|
+
for (let k = 0; k < 8; k += 1) {
|
|
1137
|
+
c = (c & 1) === 1 ? 3988292384 ^ c >>> 1 : c >>> 1;
|
|
1138
|
+
}
|
|
1139
|
+
return c >>> 0;
|
|
1140
|
+
});
|
|
1141
|
+
function crc32(buffer) {
|
|
1142
|
+
let crc = 4294967295;
|
|
1143
|
+
for (const byte of buffer) {
|
|
1144
|
+
crc = CRC_TABLE[(crc ^ byte) & 255] ^ crc >>> 8;
|
|
1145
|
+
}
|
|
1146
|
+
return (crc ^ 4294967295) >>> 0;
|
|
1147
|
+
}
|
|
1148
|
+
function pngChunk(type, data) {
|
|
1149
|
+
const typeBuffer = Buffer.from(type, "ascii");
|
|
1150
|
+
const crc = crc32(Buffer.concat([typeBuffer, data]));
|
|
1151
|
+
return Buffer.concat([uint32(data.length), typeBuffer, data, uint32(crc >>> 0)]);
|
|
1152
|
+
}
|
|
1153
|
+
function encodePngRgb(width, height, pixels) {
|
|
1154
|
+
const stride = width * 3;
|
|
1155
|
+
const rows = Buffer.alloc((stride + 1) * height);
|
|
1156
|
+
for (let y = 0; y < height; y += 1) {
|
|
1157
|
+
const rowOffset = y * (stride + 1);
|
|
1158
|
+
rows[rowOffset] = 0;
|
|
1159
|
+
Buffer.from(pixels.subarray(y * stride, y * stride + stride)).copy(rows, rowOffset + 1);
|
|
1160
|
+
}
|
|
1161
|
+
const signature = Buffer.from([137, 80, 78, 71, 13, 10, 26, 10]);
|
|
1162
|
+
const chunks = [
|
|
1163
|
+
pngChunk(
|
|
1164
|
+
"IHDR",
|
|
1165
|
+
Buffer.concat([
|
|
1166
|
+
uint32(width),
|
|
1167
|
+
uint32(height),
|
|
1168
|
+
Buffer.from([8, 2, 0, 0, 0])
|
|
1169
|
+
])
|
|
1170
|
+
),
|
|
1171
|
+
pngChunk("IDAT", import_node_zlib.default.deflateSync(rows)),
|
|
1172
|
+
pngChunk("IEND", Buffer.alloc(0))
|
|
1173
|
+
];
|
|
1174
|
+
return Buffer.concat([signature, ...chunks]);
|
|
1175
|
+
}
|
|
1176
|
+
var PETSCII_TOKEN_DEFINITIONS = [
|
|
1177
|
+
{ canonical: "RETURN", bytes: [13], aliases: ["ENTER"] },
|
|
1178
|
+
{ canonical: "SHIFT RETURN", bytes: [141], aliases: ["SHIFT ENTER", "SH RETURN", "SH ENTER", "STRET"] },
|
|
1179
|
+
{ canonical: "SPACE", bytes: [32], aliases: ["SPC"] },
|
|
1180
|
+
{ canonical: "SHIFT SPACE", bytes: [160], aliases: ["SH SPACE"] },
|
|
1181
|
+
{ canonical: "HOME", bytes: [19], aliases: ["CURSOR HOME", "CUR HOME"] },
|
|
1182
|
+
{ canonical: "CLEAR", bytes: [147], aliases: ["CLR", "CLEAR SCREEN"] },
|
|
1183
|
+
{ canonical: "DELETE", bytes: [20], aliases: ["DEL", "BACKSPACE"] },
|
|
1184
|
+
{ canonical: "INSERT", bytes: [148], aliases: ["INST", "INS"] },
|
|
1185
|
+
{ canonical: "DOWN", bytes: [17], aliases: ["CURSOR DOWN", "CUR DOWN"] },
|
|
1186
|
+
{ canonical: "UP", bytes: [145], aliases: ["CURSOR UP", "CUR UP"] },
|
|
1187
|
+
{ canonical: "LEFT", bytes: [157], aliases: ["CURSOR LEFT", "CUR LEFT"] },
|
|
1188
|
+
{ canonical: "RIGHT", bytes: [29], aliases: ["CURSOR RIGHT", "CUR RIGHT"] },
|
|
1189
|
+
{ canonical: "REVERSE ON", bytes: [18], aliases: ["RVS ON", "RVON", "RVRS ON"] },
|
|
1190
|
+
{ canonical: "REVERSE OFF", bytes: [146], aliases: ["RVS OFF", "RVOF", "RVRS OFF"] },
|
|
1191
|
+
{ canonical: "BLACK", bytes: [144], aliases: ["BLK"] },
|
|
1192
|
+
{ canonical: "WHITE", bytes: [5], aliases: ["WHT"] },
|
|
1193
|
+
{ canonical: "RED", bytes: [28], aliases: [] },
|
|
1194
|
+
{ canonical: "CYAN", bytes: [159], aliases: ["CYN"] },
|
|
1195
|
+
{ canonical: "PURPLE", bytes: [156], aliases: ["PUR"] },
|
|
1196
|
+
{ canonical: "GREEN", bytes: [30], aliases: ["GRN"] },
|
|
1197
|
+
{ canonical: "BLUE", bytes: [31], aliases: ["BLU"] },
|
|
1198
|
+
{ canonical: "YELLOW", bytes: [158], aliases: ["YEL"] },
|
|
1199
|
+
{ canonical: "ORANGE", bytes: [129], aliases: ["ORNG"] },
|
|
1200
|
+
{ canonical: "BROWN", bytes: [149], aliases: ["BRN"] },
|
|
1201
|
+
{ canonical: "LIGHT RED", bytes: [150], aliases: ["LRED", "PINK", "LT RED"] },
|
|
1202
|
+
{ canonical: "DARK GRAY", bytes: [151], aliases: ["DARK GREY", "GRAY1", "GREY1", "GRY1"] },
|
|
1203
|
+
{ canonical: "GRAY", bytes: [152], aliases: ["GREY", "GRAY2", "GREY2", "GRY2", "MEDIUM GRAY", "MEDIUM GREY"] },
|
|
1204
|
+
{ canonical: "LIGHT GREEN", bytes: [153], aliases: ["LGRN", "LT GREEN"] },
|
|
1205
|
+
{ canonical: "LIGHT BLUE", bytes: [154], aliases: ["LBLU", "LT BLUE"] },
|
|
1206
|
+
{ canonical: "LIGHT GRAY", bytes: [155], aliases: ["LIGHT GREY", "GRAY3", "GREY3", "GRY3"] },
|
|
1207
|
+
{ canonical: "F1", bytes: [133], aliases: [] },
|
|
1208
|
+
{ canonical: "F2", bytes: [137], aliases: [] },
|
|
1209
|
+
{ canonical: "F3", bytes: [134], aliases: [] },
|
|
1210
|
+
{ canonical: "F4", bytes: [138], aliases: [] },
|
|
1211
|
+
{ canonical: "F5", bytes: [135], aliases: [] },
|
|
1212
|
+
{ canonical: "F6", bytes: [139], aliases: [] },
|
|
1213
|
+
{ canonical: "F7", bytes: [136], aliases: [] },
|
|
1214
|
+
{ canonical: "F8", bytes: [140], aliases: [] },
|
|
1215
|
+
{ canonical: "STOP", bytes: [3], aliases: ["RUN STOP", "RUNSTOP"] },
|
|
1216
|
+
{ canonical: "LOWER", bytes: [14], aliases: ["LOWERCASE", "SWLC"] },
|
|
1217
|
+
{ canonical: "UPPER", bytes: [142], aliases: ["UPPERCASE", "SWUC"] },
|
|
1218
|
+
{ canonical: "POUND", bytes: [92], aliases: ["GBP", "UK POUND"] },
|
|
1219
|
+
{ canonical: "UP ARROW", bytes: [94], aliases: ["ARROW UP"] },
|
|
1220
|
+
{ canonical: "LEFT ARROW", bytes: [95], aliases: ["ARROW LEFT", "BACK ARROW"] },
|
|
1221
|
+
{ canonical: "PI", bytes: [255], aliases: [] }
|
|
1222
|
+
];
|
|
1223
|
+
var PETSCII_TOKEN_MAP = /* @__PURE__ */ new Map();
|
|
1224
|
+
for (const definition of PETSCII_TOKEN_DEFINITIONS) {
|
|
1225
|
+
PETSCII_TOKEN_MAP.set(definition.canonical, definition);
|
|
1226
|
+
for (const alias of definition.aliases) {
|
|
1227
|
+
PETSCII_TOKEN_MAP.set(alias, definition);
|
|
1228
|
+
}
|
|
1229
|
+
}
|
|
1230
|
+
var SCREEN_CODE_TOKEN_MAP = /* @__PURE__ */ new Map();
|
|
1231
|
+
for (const definition of PETSCII_TOKEN_DEFINITIONS) {
|
|
1232
|
+
for (const byte of definition.bytes) {
|
|
1233
|
+
const screenCode = petsciiToScreenCode(byte);
|
|
1234
|
+
if (!SCREEN_CODE_TOKEN_MAP.has(screenCode)) {
|
|
1235
|
+
SCREEN_CODE_TOKEN_MAP.set(screenCode, `<${definition.canonical}>`);
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
}
|
|
1239
|
+
var DIRECT_PETSCII_CHAR_BYTES = /* @__PURE__ */ new Map([
|
|
1240
|
+
["\xA3", 92],
|
|
1241
|
+
["\u2191", 94],
|
|
1242
|
+
["\u2190", 95],
|
|
1243
|
+
["\u03C0", 255],
|
|
1244
|
+
["\u03A0", 255]
|
|
1245
|
+
]);
|
|
1246
|
+
function normalizePetsciiTokenName(token) {
|
|
1247
|
+
return token.trim().toUpperCase().replace(/[\s_-]+/g, " ");
|
|
1248
|
+
}
|
|
1249
|
+
function lookupPetsciiToken(token) {
|
|
1250
|
+
const normalized = normalizePetsciiTokenName(token);
|
|
1251
|
+
const definition = PETSCII_TOKEN_MAP.get(normalized);
|
|
1252
|
+
if (!definition) {
|
|
1253
|
+
unsupportedError("Requested keyboard token is not representable through PETSCII keyboard-buffer input.", {
|
|
1254
|
+
token,
|
|
1255
|
+
normalizedToken: normalized
|
|
1256
|
+
});
|
|
1257
|
+
}
|
|
1258
|
+
return definition;
|
|
1259
|
+
}
|
|
1260
|
+
function encodeLiteralPetsciiChar(char) {
|
|
1261
|
+
if (char === "\n" || char === "\r") {
|
|
1262
|
+
return 13;
|
|
1263
|
+
}
|
|
1264
|
+
if (char === " ") {
|
|
1265
|
+
return 32;
|
|
1266
|
+
}
|
|
1267
|
+
const direct = DIRECT_PETSCII_CHAR_BYTES.get(char);
|
|
1268
|
+
if (direct != null) {
|
|
1269
|
+
return direct;
|
|
1270
|
+
}
|
|
1271
|
+
const code = char.codePointAt(0);
|
|
1272
|
+
if (code == null) {
|
|
1273
|
+
validationError("write_text received an empty character while encoding PETSCII");
|
|
1274
|
+
}
|
|
1275
|
+
if (code >= 97 && code <= 122) {
|
|
1276
|
+
return code - 32;
|
|
1277
|
+
}
|
|
1278
|
+
if (code >= 32 && code <= 93) {
|
|
1279
|
+
return code;
|
|
1280
|
+
}
|
|
1281
|
+
validationError("write_text only supports characters representable in the supported PETSCII subset", {
|
|
1282
|
+
character: char,
|
|
1283
|
+
codePoint: code
|
|
1284
|
+
});
|
|
1285
|
+
}
|
|
1286
|
+
function decodeWriteTextToPetscii(input) {
|
|
1287
|
+
const bytes = [];
|
|
1288
|
+
for (let index = 0; index < input.length; index += 1) {
|
|
1289
|
+
const char = input[index];
|
|
1290
|
+
if (char === "\\") {
|
|
1291
|
+
const next = input[index + 1];
|
|
1292
|
+
if (next == null) {
|
|
1293
|
+
validationError("write_text received a trailing backslash escape with no character after it");
|
|
1294
|
+
}
|
|
1295
|
+
switch (next) {
|
|
1296
|
+
case "n":
|
|
1297
|
+
bytes.push(13);
|
|
1298
|
+
break;
|
|
1299
|
+
case "r":
|
|
1300
|
+
bytes.push(13);
|
|
1301
|
+
break;
|
|
1302
|
+
case "t":
|
|
1303
|
+
bytes.push(32);
|
|
1304
|
+
break;
|
|
1305
|
+
case "\\":
|
|
1306
|
+
bytes.push(encodeLiteralPetsciiChar("\\"));
|
|
1307
|
+
break;
|
|
1308
|
+
case '"':
|
|
1309
|
+
bytes.push(encodeLiteralPetsciiChar('"'));
|
|
1310
|
+
break;
|
|
1311
|
+
case "'":
|
|
1312
|
+
bytes.push(encodeLiteralPetsciiChar("'"));
|
|
1313
|
+
break;
|
|
1314
|
+
default:
|
|
1315
|
+
validationError("write_text received an unsupported escape sequence", {
|
|
1316
|
+
escape: `\\${next}`
|
|
1317
|
+
});
|
|
1318
|
+
}
|
|
1319
|
+
index += 1;
|
|
1320
|
+
continue;
|
|
1321
|
+
}
|
|
1322
|
+
if (char === "{") {
|
|
1323
|
+
const end = input.indexOf("}", index + 1);
|
|
1324
|
+
if (end === -1) {
|
|
1325
|
+
validationError("write_text received an opening brace without a closing brace", {
|
|
1326
|
+
position: index
|
|
1327
|
+
});
|
|
1328
|
+
}
|
|
1329
|
+
const rawToken = input.slice(index + 1, end);
|
|
1330
|
+
if (!rawToken.trim()) {
|
|
1331
|
+
validationError("write_text received an empty brace token", {
|
|
1332
|
+
position: index
|
|
1333
|
+
});
|
|
1334
|
+
}
|
|
1335
|
+
const definition = lookupPetsciiToken(rawToken);
|
|
1336
|
+
bytes.push(...definition.bytes);
|
|
1337
|
+
index = end;
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
bytes.push(encodeLiteralPetsciiChar(char));
|
|
1341
|
+
}
|
|
1342
|
+
return Uint8Array.from(bytes);
|
|
1343
|
+
}
|
|
1344
|
+
function resolveKeyboardInputKey(key) {
|
|
1345
|
+
const trimmed = key.trim();
|
|
1346
|
+
if (!trimmed) {
|
|
1347
|
+
validationError("keyboard_input requires a non-empty key name");
|
|
1348
|
+
}
|
|
1349
|
+
if (trimmed.length === 1) {
|
|
1350
|
+
return {
|
|
1351
|
+
canonical: normalizePetsciiTokenName(trimmed),
|
|
1352
|
+
bytes: Uint8Array.from([encodeLiteralPetsciiChar(trimmed)])
|
|
1353
|
+
};
|
|
1354
|
+
}
|
|
1355
|
+
const definition = lookupPetsciiToken(trimmed);
|
|
1356
|
+
return {
|
|
1357
|
+
canonical: definition.canonical,
|
|
1358
|
+
bytes: Uint8Array.from(definition.bytes)
|
|
1359
|
+
};
|
|
1360
|
+
}
|
|
1361
|
+
var JOYSTICK_CONTROL_BITS = {
|
|
1362
|
+
up: 1,
|
|
1363
|
+
down: 2,
|
|
1364
|
+
left: 4,
|
|
1365
|
+
right: 8,
|
|
1366
|
+
fire: 16
|
|
1367
|
+
};
|
|
1368
|
+
var JOYSTICK_RELEASED_MASK = 31;
|
|
1369
|
+
var DEFAULT_INPUT_TAP_MS = 75;
|
|
1370
|
+
var DEFAULT_KEYBOARD_REPEAT_MS = 100;
|
|
1371
|
+
var VICE_PROCESS_LOG_PATH = import_node_path.default.join(import_node_os.default.tmpdir(), "c64-debug-mcp-x64sc.log");
|
|
1372
|
+
var DISPLAY_CAPTURE_DIR = import_node_path.default.resolve(process.cwd(), ".vice-debug-mcp-artifacts");
|
|
1373
|
+
var MIRROR_EMULATOR_LOGS_TO_STDERR = /^(1|true|yes|on)$/i.test(process.env.C64_DEBUG_CONSOLE_LOGS ?? "");
|
|
1374
|
+
var EXECUTION_EVENT_WAIT_MS = 1e3;
|
|
1375
|
+
var EXECUTION_SETTLE_DELAY_MS = 2e3;
|
|
1376
|
+
var BOOTSTRAP_INITIAL_DELAY_MS = 2e3;
|
|
1377
|
+
var BOOTSTRAP_SETTLE_TIMEOUT_MS = 15e3;
|
|
1378
|
+
var BOOTSTRAP_POLL_MS = 250;
|
|
1379
|
+
var BOOTSTRAP_RUNNING_STABLE_MS = 3e3;
|
|
1380
|
+
var BOOTSTRAP_RESUME_COOLDOWN_MS = 500;
|
|
1381
|
+
var PROGRAM_LOAD_SETTLE_TIMEOUT_MS = 15e3;
|
|
1382
|
+
var PROGRAM_LOAD_SETTLE_POLL_MS = 250;
|
|
1383
|
+
var PROGRAM_LOAD_RUNNING_STABLE_MS = 3e3;
|
|
1384
|
+
var PROGRAM_LOAD_RESUME_COOLDOWN_MS = 500;
|
|
1385
|
+
var DISPLAY_SETTLE_TIMEOUT_MS = 5e3;
|
|
1386
|
+
var DISPLAY_SETTLE_POLL_MS = 100;
|
|
1387
|
+
var DISPLAY_PAUSE_TIMEOUT_MS = 5e3;
|
|
1388
|
+
var DISPLAY_RUNNING_STABLE_MS = 750;
|
|
1389
|
+
var MAX_WRITE_TEXT_BYTES = 64;
|
|
1390
|
+
var DISPLAY_RESUME_COOLDOWN_MS = 250;
|
|
1391
|
+
var INPUT_SETTLE_TIMEOUT_MS = 5e3;
|
|
1392
|
+
var INPUT_SETTLE_POLL_MS = 100;
|
|
1393
|
+
var INPUT_RUNNING_STABLE_MS = 750;
|
|
1394
|
+
var INPUT_RESUME_COOLDOWN_MS = 250;
|
|
1395
|
+
var CHECKPOINT_HIT_SETTLE_MS = 1e3;
|
|
1396
|
+
var STOPPED_IDLE_TIMEOUT_MS = 2e4;
|
|
1397
|
+
var MIN_TAP_DURATION_MS = 10;
|
|
1398
|
+
var MAX_TAP_DURATION_MS = 1e4;
|
|
1399
|
+
var RESET_GRACE_PERIOD_MS = 150;
|
|
1400
|
+
function clampTapDuration(durationMs) {
|
|
1401
|
+
if (durationMs == null) {
|
|
1402
|
+
return DEFAULT_INPUT_TAP_MS;
|
|
1403
|
+
}
|
|
1404
|
+
if (!Number.isInteger(durationMs)) {
|
|
1405
|
+
validationError("durationMs must be an integer", { durationMs });
|
|
1406
|
+
}
|
|
1407
|
+
return Math.max(MIN_TAP_DURATION_MS, Math.min(MAX_TAP_DURATION_MS, durationMs));
|
|
1408
|
+
}
|
|
1409
|
+
function joystickPortToProtocol(port2) {
|
|
1410
|
+
return port2 - 1;
|
|
1411
|
+
}
|
|
1412
|
+
var PortAllocator = class {
|
|
1413
|
+
#forbiddenPorts;
|
|
1414
|
+
constructor(forbiddenPorts) {
|
|
1415
|
+
this.#forbiddenPorts = new Set(forbiddenPorts ?? DEFAULT_FORBIDDEN_PORTS);
|
|
1416
|
+
}
|
|
1417
|
+
get forbiddenPorts() {
|
|
1418
|
+
return [...this.#forbiddenPorts].sort((left, right) => left - right);
|
|
1419
|
+
}
|
|
1420
|
+
assertAllowed(port2) {
|
|
1421
|
+
if (this.#forbiddenPorts.has(port2)) {
|
|
1422
|
+
validationError("Standard/default debug monitor ports are forbidden in managed mode", {
|
|
1423
|
+
port: port2,
|
|
1424
|
+
forbiddenPorts: this.forbiddenPorts
|
|
1425
|
+
});
|
|
1426
|
+
}
|
|
1427
|
+
}
|
|
1428
|
+
async allocate() {
|
|
1429
|
+
for (let attempts = 0; attempts < 30; attempts += 1) {
|
|
1430
|
+
const candidate = await this.#probeEphemeralPort();
|
|
1431
|
+
if (candidate < 1024 || this.#forbiddenPorts.has(candidate)) {
|
|
1432
|
+
continue;
|
|
1433
|
+
}
|
|
1434
|
+
return candidate;
|
|
1435
|
+
}
|
|
1436
|
+
throw new ViceMcpError("port_allocation_failed", "Could not allocate a non-default monitor port", "configuration");
|
|
1437
|
+
}
|
|
1438
|
+
async ensureFree(port2, host2 = DEFAULT_MONITOR_HOST) {
|
|
1439
|
+
this.assertAllowed(port2);
|
|
1440
|
+
const available = await isPortAvailable(host2, port2);
|
|
1441
|
+
if (!available) {
|
|
1442
|
+
throw new ViceMcpError("port_in_use", `Monitor port ${port2} is already in use`, "configuration", false, {
|
|
1443
|
+
host: host2,
|
|
1444
|
+
port: port2
|
|
1445
|
+
});
|
|
1446
|
+
}
|
|
1447
|
+
}
|
|
1448
|
+
async #probeEphemeralPort() {
|
|
1449
|
+
return await new Promise((resolve, reject) => {
|
|
1450
|
+
const server = import_node_net2.default.createServer();
|
|
1451
|
+
server.once("error", reject);
|
|
1452
|
+
server.listen(0, DEFAULT_MONITOR_HOST, () => {
|
|
1453
|
+
const address = server.address();
|
|
1454
|
+
if (address && typeof address === "object") {
|
|
1455
|
+
const port2 = address.port;
|
|
1456
|
+
server.close((error) => {
|
|
1457
|
+
if (error) {
|
|
1458
|
+
reject(error);
|
|
1459
|
+
return;
|
|
1460
|
+
}
|
|
1461
|
+
resolve(port2);
|
|
1462
|
+
});
|
|
1463
|
+
return;
|
|
1464
|
+
}
|
|
1465
|
+
reject(new Error("Failed to allocate port"));
|
|
1466
|
+
});
|
|
1467
|
+
});
|
|
1468
|
+
}
|
|
1469
|
+
};
|
|
1470
|
+
async function isPortAvailable(host2, port2) {
|
|
1471
|
+
return await new Promise((resolve) => {
|
|
1472
|
+
const socket = import_node_net2.default.connect({ host: host2, port: port2 });
|
|
1473
|
+
socket.once("connect", () => {
|
|
1474
|
+
socket.destroy();
|
|
1475
|
+
resolve(false);
|
|
1476
|
+
});
|
|
1477
|
+
socket.once("error", () => resolve(true));
|
|
1478
|
+
});
|
|
1479
|
+
}
|
|
1480
|
+
var ViceSession = class {
|
|
1481
|
+
#client = new ViceMonitorClient();
|
|
1482
|
+
#portAllocator;
|
|
1483
|
+
#transportState = "not_started";
|
|
1484
|
+
#processState = "not_applicable";
|
|
1485
|
+
#executionState = "unknown";
|
|
1486
|
+
#lastStopReason = "none";
|
|
1487
|
+
#host = null;
|
|
1488
|
+
#port = null;
|
|
1489
|
+
#connectedSince = null;
|
|
1490
|
+
#lastResponseAt = null;
|
|
1491
|
+
#process = null;
|
|
1492
|
+
#processLogStream = null;
|
|
1493
|
+
#stdoutMirrorBuffer = "";
|
|
1494
|
+
#stderrMirrorBuffer = "";
|
|
1495
|
+
#warnings = [];
|
|
1496
|
+
#lastExecutionIntent = "unknown";
|
|
1497
|
+
#lastRegisters = null;
|
|
1498
|
+
#lastRuntimeEventType = "unknown";
|
|
1499
|
+
#lastRuntimeProgramCounter = null;
|
|
1500
|
+
#config = null;
|
|
1501
|
+
#recoveryInProgress = false;
|
|
1502
|
+
#recoveryPromise = null;
|
|
1503
|
+
#freshEmulatorPending = false;
|
|
1504
|
+
#launchId = 0;
|
|
1505
|
+
#restartCount = 0;
|
|
1506
|
+
#suppressRecovery = false;
|
|
1507
|
+
#shuttingDown = false;
|
|
1508
|
+
#heldKeyboardIntervals = /* @__PURE__ */ new Map();
|
|
1509
|
+
#heldJoystickMasks = /* @__PURE__ */ new Map();
|
|
1510
|
+
#breakpointLabels = /* @__PURE__ */ new Map();
|
|
1511
|
+
#stoppedAt = null;
|
|
1512
|
+
#autoResumeTimer = null;
|
|
1513
|
+
#explicitPauseActive = false;
|
|
1514
|
+
#pendingCheckpointHit = null;
|
|
1515
|
+
#lastCheckpointHit = null;
|
|
1516
|
+
#checkpointQueryPending = false;
|
|
1517
|
+
#executionOperationLock = null;
|
|
1518
|
+
#displayOperationLock = null;
|
|
1519
|
+
constructor(portAllocator = new PortAllocator()) {
|
|
1520
|
+
this.#portAllocator = portAllocator;
|
|
1521
|
+
this.#client.on("response", (response) => {
|
|
1522
|
+
this.#lastResponseAt = nowIso();
|
|
1523
|
+
this.#writeProcessLogLine(`[monitor-response] type=${response.type} requestId=${response.requestId} errorCode=${response.errorCode}`);
|
|
1524
|
+
});
|
|
1525
|
+
this.#client.on("close", () => {
|
|
1526
|
+
this.#writeProcessLogLine("[monitor-close] debugger connection closed");
|
|
1527
|
+
if (this.#transportState !== "stopped") {
|
|
1528
|
+
this.#transportState = "disconnected";
|
|
1529
|
+
}
|
|
1530
|
+
this.#syncMonitorRuntimeState();
|
|
1531
|
+
if (!this.#suppressRecovery && !this.#shuttingDown && this.#config) {
|
|
1532
|
+
void this.#scheduleRecovery();
|
|
1533
|
+
}
|
|
1534
|
+
});
|
|
1535
|
+
this.#client.on("event", (event) => {
|
|
1536
|
+
if (event.type === "checkpoint_info" && event.checkpoint.currentlyHit) {
|
|
1537
|
+
this.#pendingCheckpointHit = {
|
|
1538
|
+
id: event.checkpoint.id,
|
|
1539
|
+
kind: event.checkpoint.kind,
|
|
1540
|
+
observedAt: Date.now()
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
const programCounter = "programCounter" in event && typeof event.programCounter === "number" ? event.programCounter : null;
|
|
1544
|
+
this.#writeProcessLogLine(
|
|
1545
|
+
`[monitor-event] type=${event.type}${programCounter == null ? "" : ` pc=$${programCounter.toString(16).padStart(4, "0")}`}`
|
|
1546
|
+
);
|
|
1547
|
+
this.#syncMonitorRuntimeState();
|
|
1548
|
+
});
|
|
1549
|
+
}
|
|
1550
|
+
snapshot() {
|
|
1551
|
+
return {
|
|
1552
|
+
transportState: this.#transportState,
|
|
1553
|
+
processState: this.#processState,
|
|
1554
|
+
executionState: this.#executionState,
|
|
1555
|
+
lastStopReason: this.#lastStopReason,
|
|
1556
|
+
idleAutoResumeArmed: this.#autoResumeTimer != null,
|
|
1557
|
+
explicitPauseActive: this.#explicitPauseActive,
|
|
1558
|
+
lastCheckpointId: this.#lastCheckpointHit?.id ?? null,
|
|
1559
|
+
lastCheckpointKind: this.#lastCheckpointHit?.kind ?? null,
|
|
1560
|
+
recoveryInProgress: this.#recoveryInProgress,
|
|
1561
|
+
launchId: this.#launchId,
|
|
1562
|
+
restartCount: this.#restartCount,
|
|
1563
|
+
freshEmulatorPending: this.#freshEmulatorPending,
|
|
1564
|
+
connectedSince: this.#connectedSince,
|
|
1565
|
+
lastResponseAt: this.#lastResponseAt,
|
|
1566
|
+
processId: this.#process?.pid ?? null,
|
|
1567
|
+
warnings: [...this.#warnings]
|
|
1568
|
+
};
|
|
1569
|
+
}
|
|
1570
|
+
async getMonitorState() {
|
|
1571
|
+
await this.#ensureReady();
|
|
1572
|
+
this.#syncMonitorRuntimeState();
|
|
1573
|
+
const runtime = this.#client.runtimeState();
|
|
1574
|
+
return {
|
|
1575
|
+
executionState: this.#executionState,
|
|
1576
|
+
lastStopReason: this.#lastStopReason,
|
|
1577
|
+
runtimeKnown: runtime.runtimeKnown,
|
|
1578
|
+
programCounter: runtime.programCounter
|
|
1579
|
+
};
|
|
1580
|
+
}
|
|
1581
|
+
async getRegisters() {
|
|
1582
|
+
await this.#ensurePausedForDebug("get_registers");
|
|
1583
|
+
return {
|
|
1584
|
+
registers: await this.#readRegisters()
|
|
1585
|
+
};
|
|
1586
|
+
}
|
|
1587
|
+
async shutdown() {
|
|
1588
|
+
if (this.#shuttingDown) {
|
|
1589
|
+
return;
|
|
1590
|
+
}
|
|
1591
|
+
this.#shuttingDown = true;
|
|
1592
|
+
this.#clearIdleAutoResume();
|
|
1593
|
+
this.#suppressRecovery = true;
|
|
1594
|
+
this.#config = null;
|
|
1595
|
+
this.#recoveryPromise = null;
|
|
1596
|
+
this.#recoveryInProgress = false;
|
|
1597
|
+
this.#freshEmulatorPending = false;
|
|
1598
|
+
this.#clearHeldInputState();
|
|
1599
|
+
this.#breakpointLabels.clear();
|
|
1600
|
+
try {
|
|
1601
|
+
await this.#resumeBeforeShutdown();
|
|
1602
|
+
await this.#stopManagedProcess(true);
|
|
1603
|
+
} finally {
|
|
1604
|
+
this.#transportState = "stopped";
|
|
1605
|
+
this.#processState = "not_applicable";
|
|
1606
|
+
this.#executionState = "unknown";
|
|
1607
|
+
this.#lastStopReason = "none";
|
|
1608
|
+
this.#explicitPauseActive = false;
|
|
1609
|
+
this.#pendingCheckpointHit = null;
|
|
1610
|
+
this.#lastCheckpointHit = null;
|
|
1611
|
+
this.#lastRuntimeEventType = "unknown";
|
|
1612
|
+
this.#lastRuntimeProgramCounter = null;
|
|
1613
|
+
this.#host = null;
|
|
1614
|
+
this.#port = null;
|
|
1615
|
+
this.#connectedSince = null;
|
|
1616
|
+
this.#lastRegisters = null;
|
|
1617
|
+
}
|
|
1618
|
+
}
|
|
1619
|
+
takeResponseMeta() {
|
|
1620
|
+
const meta = {
|
|
1621
|
+
freshEmulator: this.#freshEmulatorPending,
|
|
1622
|
+
launchId: this.#launchId,
|
|
1623
|
+
restartCount: this.#restartCount
|
|
1624
|
+
};
|
|
1625
|
+
this.#freshEmulatorPending = false;
|
|
1626
|
+
return meta;
|
|
1627
|
+
}
|
|
1628
|
+
async execute(action, count = 1, resetMode = "soft", waitUntilRunningStable = false) {
|
|
1629
|
+
switch (action) {
|
|
1630
|
+
case "pause":
|
|
1631
|
+
return await this.pauseExecution();
|
|
1632
|
+
case "resume":
|
|
1633
|
+
return await this.continueExecution(waitUntilRunningStable);
|
|
1634
|
+
case "step":
|
|
1635
|
+
return await this.stepInstruction(count, false);
|
|
1636
|
+
case "step_over":
|
|
1637
|
+
return await this.stepInstruction(count, true);
|
|
1638
|
+
case "step_out":
|
|
1639
|
+
return await this.stepOut();
|
|
1640
|
+
case "reset":
|
|
1641
|
+
return await this.resetMachine(resetMode);
|
|
1642
|
+
}
|
|
1643
|
+
}
|
|
1644
|
+
async setRegisters(registers) {
|
|
1645
|
+
await this.#ensurePausedForDebug("set_registers");
|
|
1646
|
+
const metadata = await this.#client.getRegistersAvailable();
|
|
1647
|
+
const metadataByName = new Map(metadata.registers.map((item) => [item.name.toUpperCase(), item]));
|
|
1648
|
+
const payload = Object.entries(registers).map(([fieldName, value]) => {
|
|
1649
|
+
const definition = C64_REGISTER_DEFINITIONS.find((item) => item.fieldName === fieldName);
|
|
1650
|
+
if (!definition) {
|
|
1651
|
+
validationError(`Unknown register ${fieldName}`, { registerName: fieldName });
|
|
1652
|
+
}
|
|
1653
|
+
if (!Number.isInteger(value)) {
|
|
1654
|
+
validationError(`Register ${fieldName} must be an integer`, { registerName: fieldName, value });
|
|
1655
|
+
}
|
|
1656
|
+
if (value < definition.min || value > definition.max) {
|
|
1657
|
+
validationError(`Register ${fieldName} must be between ${definition.min} and ${definition.max}`, {
|
|
1658
|
+
registerName: fieldName,
|
|
1659
|
+
min: definition.min,
|
|
1660
|
+
max: definition.max,
|
|
1661
|
+
value
|
|
1662
|
+
});
|
|
1663
|
+
}
|
|
1664
|
+
const meta = metadataByName.get(definition.viceName.toUpperCase());
|
|
1665
|
+
if (!meta) {
|
|
1666
|
+
validationError(`Required C64 register is missing from the emulator: ${definition.viceName}`, {
|
|
1667
|
+
registerName: definition.fieldName,
|
|
1668
|
+
viceName: definition.viceName
|
|
1669
|
+
});
|
|
1670
|
+
}
|
|
1671
|
+
return {
|
|
1672
|
+
id: meta.id,
|
|
1673
|
+
value
|
|
1674
|
+
};
|
|
1675
|
+
});
|
|
1676
|
+
const response = await this.#client.setRegisters(payload);
|
|
1677
|
+
const updatedById = new Map(response.registers.map((register) => [register.id, register.value]));
|
|
1678
|
+
this.#lastRegisters = this.#mergeRegisters(
|
|
1679
|
+
this.#lastRegisters,
|
|
1680
|
+
Object.fromEntries(
|
|
1681
|
+
C64_REGISTER_DEFINITIONS.flatMap((definition) => {
|
|
1682
|
+
const meta = metadataByName.get(definition.viceName.toUpperCase());
|
|
1683
|
+
if (!meta) {
|
|
1684
|
+
return [];
|
|
1685
|
+
}
|
|
1686
|
+
const value = updatedById.get(meta.id);
|
|
1687
|
+
if (value == null) {
|
|
1688
|
+
return [];
|
|
1689
|
+
}
|
|
1690
|
+
return [[definition.fieldName, value]];
|
|
1691
|
+
})
|
|
1692
|
+
)
|
|
1693
|
+
);
|
|
1694
|
+
return {
|
|
1695
|
+
updated: Object.fromEntries(
|
|
1696
|
+
C64_REGISTER_DEFINITIONS.flatMap((definition) => {
|
|
1697
|
+
const meta = metadataByName.get(definition.viceName.toUpperCase());
|
|
1698
|
+
if (!meta) {
|
|
1699
|
+
return [];
|
|
1700
|
+
}
|
|
1701
|
+
const value = updatedById.get(meta.id);
|
|
1702
|
+
if (value == null) {
|
|
1703
|
+
return [];
|
|
1704
|
+
}
|
|
1705
|
+
return [[definition.fieldName, value]];
|
|
1706
|
+
})
|
|
1707
|
+
),
|
|
1708
|
+
executionState: this.#executionState
|
|
1709
|
+
};
|
|
1710
|
+
}
|
|
1711
|
+
async readMemory(start, end, bank = 0) {
|
|
1712
|
+
await this.#ensurePausedForDebug("memory_read");
|
|
1713
|
+
this.#validateRange(start, end);
|
|
1714
|
+
const response = await this.#client.readMemory(start, end, bank);
|
|
1715
|
+
return {
|
|
1716
|
+
length: response.bytes.length,
|
|
1717
|
+
data: Array.from(response.bytes)
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
async writeMemory(start, data, bank = 0) {
|
|
1721
|
+
await this.#ensurePausedForDebug("memory_write");
|
|
1722
|
+
const bytes = Uint8Array.from(data);
|
|
1723
|
+
if (bytes.length === 0) {
|
|
1724
|
+
validationError("write_memory requires at least one byte");
|
|
1725
|
+
}
|
|
1726
|
+
if (data.some((value) => !Number.isInteger(value) || value < 0 || value > 255)) {
|
|
1727
|
+
validationError("write_memory data must contain only integer byte values between 0 and 255");
|
|
1728
|
+
}
|
|
1729
|
+
await this.#client.writeMemory(start, bytes, bank);
|
|
1730
|
+
const debugState = await this.#readDebugState();
|
|
1731
|
+
return {
|
|
1732
|
+
address: start,
|
|
1733
|
+
length: bytes.length,
|
|
1734
|
+
worked: true,
|
|
1735
|
+
executionState: debugState.executionState,
|
|
1736
|
+
lastStopReason: debugState.lastStopReason,
|
|
1737
|
+
programCounter: debugState.programCounter,
|
|
1738
|
+
registers: debugState.registers
|
|
1739
|
+
};
|
|
1740
|
+
}
|
|
1741
|
+
async pauseExecution() {
|
|
1742
|
+
return this.#withExecutionLock(async () => {
|
|
1743
|
+
await this.#ensureReady();
|
|
1744
|
+
this.#syncMonitorRuntimeState();
|
|
1745
|
+
if (this.#executionState === "stopped") {
|
|
1746
|
+
this.#explicitPauseActive = true;
|
|
1747
|
+
const debugState2 = await this.#readDebugState();
|
|
1748
|
+
return {
|
|
1749
|
+
executionState: debugState2.executionState,
|
|
1750
|
+
lastStopReason: debugState2.lastStopReason,
|
|
1751
|
+
programCounter: debugState2.programCounter,
|
|
1752
|
+
registers: debugState2.registers,
|
|
1753
|
+
warnings: []
|
|
1754
|
+
};
|
|
1755
|
+
}
|
|
1756
|
+
if (this.#executionState !== "running") {
|
|
1757
|
+
emulatorNotRunningError("execute pause", {
|
|
1758
|
+
executionState: this.#executionState,
|
|
1759
|
+
lastStopReason: this.#lastStopReason
|
|
1760
|
+
});
|
|
1761
|
+
}
|
|
1762
|
+
this.#explicitPauseActive = true;
|
|
1763
|
+
this.#lastExecutionIntent = "monitor_entry";
|
|
1764
|
+
this.#writeProcessLogLine("[tx] execute pause");
|
|
1765
|
+
await this.#client.ping();
|
|
1766
|
+
const paused = await this.waitForState("stopped", 5e3, 0);
|
|
1767
|
+
if (!paused.reachedTarget) {
|
|
1768
|
+
throw new ViceMcpError("pause_timeout", "execute pause could not reach a stopped state before timeout.", "timeout", true, {
|
|
1769
|
+
executionState: paused.executionState,
|
|
1770
|
+
lastStopReason: paused.lastStopReason
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
const debugState = await this.#readDebugState();
|
|
1774
|
+
return {
|
|
1775
|
+
executionState: debugState.executionState,
|
|
1776
|
+
lastStopReason: debugState.lastStopReason,
|
|
1777
|
+
programCounter: debugState.programCounter,
|
|
1778
|
+
registers: debugState.registers,
|
|
1779
|
+
warnings: []
|
|
1780
|
+
};
|
|
1781
|
+
});
|
|
1782
|
+
}
|
|
1783
|
+
async continueExecution(waitUntilRunningStable = false) {
|
|
1784
|
+
return this.#withExecutionLock(async () => {
|
|
1785
|
+
await this.#ensureReady();
|
|
1786
|
+
if (this.#executionState !== "stopped") {
|
|
1787
|
+
debuggerNotPausedError("execute resume", {
|
|
1788
|
+
executionState: this.#executionState,
|
|
1789
|
+
lastStopReason: this.#lastStopReason
|
|
1790
|
+
});
|
|
1791
|
+
}
|
|
1792
|
+
const debugState = this.#lastRegisters == null ? await this.#readDebugState() : this.#buildDebugState(this.#lastRegisters);
|
|
1793
|
+
this.#lastExecutionIntent = "unknown";
|
|
1794
|
+
this.#writeProcessLogLine("[tx] execute resume");
|
|
1795
|
+
const executionEvent = this.#waitForExecutionEvent(1e3);
|
|
1796
|
+
await this.#client.continueExecution();
|
|
1797
|
+
const event = await executionEvent;
|
|
1798
|
+
if (!event || event.type !== "resumed") {
|
|
1799
|
+
this.#writeProcessLogLine(`[execute-resume] no resumed event within 1000ms (got ${event?.type ?? "nothing"})`);
|
|
1800
|
+
}
|
|
1801
|
+
if (waitUntilRunningStable) {
|
|
1802
|
+
await this.waitForState("running", 5e3, INPUT_RUNNING_STABLE_MS);
|
|
1803
|
+
} else {
|
|
1804
|
+
this.#syncMonitorRuntimeState();
|
|
1805
|
+
}
|
|
1806
|
+
this.#explicitPauseActive = false;
|
|
1807
|
+
const runtime = this.#client.runtimeState();
|
|
1808
|
+
const warnings = [];
|
|
1809
|
+
const currentExecutionState = this.#executionState;
|
|
1810
|
+
if (!waitUntilRunningStable && currentExecutionState !== "running") {
|
|
1811
|
+
warnings.push(
|
|
1812
|
+
makeWarning("Resume acknowledged but state transition not yet confirmed; use wait_for_state for authoritative state.", "resume_async")
|
|
1813
|
+
);
|
|
1814
|
+
}
|
|
1815
|
+
return {
|
|
1816
|
+
executionState: currentExecutionState,
|
|
1817
|
+
lastStopReason: this.#lastStopReason,
|
|
1818
|
+
programCounter: runtime.programCounter ?? debugState.programCounter,
|
|
1819
|
+
registers: debugState.registers,
|
|
1820
|
+
warnings
|
|
1821
|
+
};
|
|
1822
|
+
});
|
|
1823
|
+
}
|
|
1824
|
+
async stepInstruction(count = 1, stepOver = false) {
|
|
1825
|
+
await this.#ensurePausedForDebug(stepOver ? "execute step_over" : "execute step");
|
|
1826
|
+
this.#lastExecutionIntent = "step_complete";
|
|
1827
|
+
this.#writeProcessLogLine(`[tx] ${stepOver ? "execute step_over" : "execute step"} count=${count}`);
|
|
1828
|
+
await this.#client.stepInstruction(count, stepOver);
|
|
1829
|
+
this.#syncMonitorRuntimeState();
|
|
1830
|
+
const debugState = await this.#readDebugState();
|
|
1831
|
+
return {
|
|
1832
|
+
executionState: debugState.executionState,
|
|
1833
|
+
lastStopReason: debugState.lastStopReason,
|
|
1834
|
+
programCounter: debugState.programCounter,
|
|
1835
|
+
registers: debugState.registers,
|
|
1836
|
+
stepsExecuted: count,
|
|
1837
|
+
warnings: []
|
|
1838
|
+
};
|
|
1839
|
+
}
|
|
1840
|
+
async stepOut() {
|
|
1841
|
+
await this.#ensurePausedForDebug("execute step_out");
|
|
1842
|
+
this.#lastExecutionIntent = "step_complete";
|
|
1843
|
+
this.#writeProcessLogLine("[tx] execute step_out");
|
|
1844
|
+
await this.#client.stepOut();
|
|
1845
|
+
this.#syncMonitorRuntimeState();
|
|
1846
|
+
const debugState = await this.#readDebugState();
|
|
1847
|
+
return {
|
|
1848
|
+
executionState: debugState.executionState,
|
|
1849
|
+
lastStopReason: debugState.lastStopReason,
|
|
1850
|
+
programCounter: debugState.programCounter,
|
|
1851
|
+
registers: debugState.registers,
|
|
1852
|
+
warnings: []
|
|
1853
|
+
};
|
|
1854
|
+
}
|
|
1855
|
+
async resetMachine(mode) {
|
|
1856
|
+
return this.#withExecutionLock(async () => {
|
|
1857
|
+
await this.#ensureReady();
|
|
1858
|
+
const wasPaused = this.#explicitPauseActive;
|
|
1859
|
+
this.#lastExecutionIntent = "reset";
|
|
1860
|
+
this.#writeProcessLogLine(`[tx] execute reset mode=${mode}`);
|
|
1861
|
+
await this.#client.reset(mode);
|
|
1862
|
+
await sleep(RESET_GRACE_PERIOD_MS);
|
|
1863
|
+
this.#explicitPauseActive = wasPaused;
|
|
1864
|
+
this.#syncMonitorRuntimeState();
|
|
1865
|
+
await sleep(50);
|
|
1866
|
+
this.#syncMonitorRuntimeState();
|
|
1867
|
+
const debugState = await this.#readDebugState();
|
|
1868
|
+
return {
|
|
1869
|
+
executionState: debugState.executionState,
|
|
1870
|
+
lastStopReason: debugState.lastStopReason,
|
|
1871
|
+
programCounter: debugState.programCounter,
|
|
1872
|
+
registers: debugState.registers,
|
|
1873
|
+
warnings: []
|
|
1874
|
+
};
|
|
1875
|
+
});
|
|
1876
|
+
}
|
|
1877
|
+
async listBreakpoints(includeDisabled = true) {
|
|
1878
|
+
await this.#ensureReady();
|
|
1879
|
+
this.#writeProcessLogLine(`[tx] breakpoint_list includeDisabled=${includeDisabled}`);
|
|
1880
|
+
const response = await this.#client.listBreakpoints();
|
|
1881
|
+
this.#pruneBreakpointLabels(response.checkpoints.map((breakpoint) => breakpoint.id));
|
|
1882
|
+
return {
|
|
1883
|
+
breakpoints: response.checkpoints.filter((breakpoint) => includeDisabled ? true : breakpoint.enabled).map((breakpoint) => this.#attachBreakpointLabel(breakpoint))
|
|
1884
|
+
};
|
|
1885
|
+
}
|
|
1886
|
+
async setBreakpoint(options) {
|
|
1887
|
+
await this.#ensureReady();
|
|
1888
|
+
this.#writeProcessLogLine(
|
|
1889
|
+
`[tx] breakpoint_set kind=${options.kind} start=$${options.start.toString(16).padStart(4, "0")}${options.end == null ? "" : ` end=$${options.end.toString(16).padStart(4, "0")}`}${options.temporary ? " temporary=true" : ""}${options.enabled === false ? " enabled=false" : ""}`
|
|
1890
|
+
);
|
|
1891
|
+
const response = await this.#client.setBreakpoint({
|
|
1892
|
+
start: options.start,
|
|
1893
|
+
end: options.end,
|
|
1894
|
+
kind: options.kind,
|
|
1895
|
+
condition: options.condition,
|
|
1896
|
+
temporary: options.temporary,
|
|
1897
|
+
enabled: options.enabled,
|
|
1898
|
+
stopWhenHit: true
|
|
1899
|
+
});
|
|
1900
|
+
if (options.label?.trim()) {
|
|
1901
|
+
this.#breakpointLabels.set(response.checkpoint.id, options.label.trim());
|
|
1902
|
+
} else {
|
|
1903
|
+
this.#breakpointLabels.delete(response.checkpoint.id);
|
|
1904
|
+
}
|
|
1905
|
+
return {
|
|
1906
|
+
breakpoint: this.#attachBreakpointLabel(response.checkpoint),
|
|
1907
|
+
executionState: this.#executionState,
|
|
1908
|
+
lastStopReason: this.#lastStopReason,
|
|
1909
|
+
programCounter: this.#lastRegisters?.PC ?? null,
|
|
1910
|
+
registers: this.#lastRegisters
|
|
1911
|
+
};
|
|
1912
|
+
}
|
|
1913
|
+
async deleteBreakpoint(breakpointId) {
|
|
1914
|
+
await this.#ensureReady();
|
|
1915
|
+
this.#writeProcessLogLine(`[tx] breakpoint_clear id=${breakpointId}`);
|
|
1916
|
+
try {
|
|
1917
|
+
await this.#client.deleteBreakpoint(breakpointId);
|
|
1918
|
+
} catch (error) {
|
|
1919
|
+
if (error instanceof ViceMcpError && error.code === "emulator_protocol_error" && error.details?.emulatorErrorCode === 1) {
|
|
1920
|
+
return {
|
|
1921
|
+
cleared: false,
|
|
1922
|
+
breakpointId,
|
|
1923
|
+
executionState: this.#executionState,
|
|
1924
|
+
lastStopReason: this.#lastStopReason,
|
|
1925
|
+
programCounter: this.#lastRegisters?.PC ?? null,
|
|
1926
|
+
registers: this.#lastRegisters
|
|
1927
|
+
};
|
|
1928
|
+
}
|
|
1929
|
+
throw error;
|
|
1930
|
+
}
|
|
1931
|
+
this.#breakpointLabels.delete(breakpointId);
|
|
1932
|
+
return {
|
|
1933
|
+
cleared: true,
|
|
1934
|
+
breakpointId,
|
|
1935
|
+
executionState: this.#executionState,
|
|
1936
|
+
lastStopReason: this.#lastStopReason,
|
|
1937
|
+
programCounter: this.#lastRegisters?.PC ?? null,
|
|
1938
|
+
registers: this.#lastRegisters
|
|
1939
|
+
};
|
|
1940
|
+
}
|
|
1941
|
+
async breakpointSet(options) {
|
|
1942
|
+
const length = options.length ?? 1;
|
|
1943
|
+
if (!Number.isInteger(length) || length <= 0) {
|
|
1944
|
+
validationError("Breakpoint length must be a positive integer", { length });
|
|
1945
|
+
}
|
|
1946
|
+
const end = options.address + length - 1;
|
|
1947
|
+
this.#validateRange(options.address, end);
|
|
1948
|
+
return await this.setBreakpoint({
|
|
1949
|
+
kind: options.kind,
|
|
1950
|
+
start: options.address,
|
|
1951
|
+
end,
|
|
1952
|
+
condition: options.condition,
|
|
1953
|
+
label: options.label,
|
|
1954
|
+
temporary: options.temporary,
|
|
1955
|
+
enabled: options.enabled
|
|
1956
|
+
});
|
|
1957
|
+
}
|
|
1958
|
+
async breakpointClear(breakpointId) {
|
|
1959
|
+
return await this.deleteBreakpoint(breakpointId);
|
|
1960
|
+
}
|
|
1961
|
+
async programLoad(options) {
|
|
1962
|
+
const filePath = import_node_path.default.resolve(options.filePath);
|
|
1963
|
+
await this.#assertReadableProgramFile(filePath);
|
|
1964
|
+
await this.#ensureRunning("program_load");
|
|
1965
|
+
this.#explicitPauseActive = false;
|
|
1966
|
+
const result = await this.autostartProgram(filePath, options.autoStart ?? true, options.fileIndex ?? 0);
|
|
1967
|
+
return {
|
|
1968
|
+
filePath: result.filePath,
|
|
1969
|
+
autoStart: result.autoStart,
|
|
1970
|
+
fileIndex: result.fileIndex,
|
|
1971
|
+
executionState: result.executionState
|
|
1972
|
+
};
|
|
1973
|
+
}
|
|
1974
|
+
async captureDisplay(useVic = true) {
|
|
1975
|
+
return this.#withDisplayLock(async () => {
|
|
1976
|
+
await this.#ensureReady();
|
|
1977
|
+
this.#syncMonitorRuntimeState();
|
|
1978
|
+
const previousExecutionState = this.#executionState;
|
|
1979
|
+
this.#writeProcessLogLine(`[tx] capture_display useVic=${useVic}`);
|
|
1980
|
+
const display = await this.#client.captureDisplay(useVic);
|
|
1981
|
+
const palette = await this.#client.getPalette(useVic);
|
|
1982
|
+
if (display.bitsPerPixel !== 8) {
|
|
1983
|
+
throw new ViceMcpError(
|
|
1984
|
+
"display_bpp_unsupported",
|
|
1985
|
+
`capture_display only supports 8-bit indexed display payloads, got ${display.bitsPerPixel}.`,
|
|
1986
|
+
"unsupported",
|
|
1987
|
+
false,
|
|
1988
|
+
{ bitsPerPixel: display.bitsPerPixel }
|
|
1989
|
+
);
|
|
1990
|
+
}
|
|
1991
|
+
const expectedLength = display.debugWidth * display.debugHeight;
|
|
1992
|
+
if (display.imageBytes.length !== expectedLength) {
|
|
1993
|
+
throw new ViceMcpError(
|
|
1994
|
+
"display_payload_invalid",
|
|
1995
|
+
"capture_display received a display payload whose length does not match the reported debug dimensions.",
|
|
1996
|
+
"protocol",
|
|
1997
|
+
false,
|
|
1998
|
+
{
|
|
1999
|
+
debugWidth: display.debugWidth,
|
|
2000
|
+
debugHeight: display.debugHeight,
|
|
2001
|
+
expectedLength,
|
|
2002
|
+
actualLength: display.imageBytes.length
|
|
2003
|
+
}
|
|
2004
|
+
);
|
|
2005
|
+
}
|
|
2006
|
+
if (palette.items.length === 0) {
|
|
2007
|
+
throw new ViceMcpError("display_palette_missing", "capture_display received an empty display palette.", "protocol", false);
|
|
2008
|
+
}
|
|
2009
|
+
if (display.debugOffsetX + display.innerWidth > display.debugWidth || display.debugOffsetY + display.innerHeight > display.debugHeight) {
|
|
2010
|
+
throw new ViceMcpError(
|
|
2011
|
+
"display_crop_invalid",
|
|
2012
|
+
"capture_display received crop geometry outside the display buffer bounds.",
|
|
2013
|
+
"protocol",
|
|
2014
|
+
false,
|
|
2015
|
+
{
|
|
2016
|
+
debugWidth: display.debugWidth,
|
|
2017
|
+
debugHeight: display.debugHeight,
|
|
2018
|
+
debugOffsetX: display.debugOffsetX,
|
|
2019
|
+
debugOffsetY: display.debugOffsetY,
|
|
2020
|
+
innerWidth: display.innerWidth,
|
|
2021
|
+
innerHeight: display.innerHeight
|
|
2022
|
+
}
|
|
2023
|
+
);
|
|
2024
|
+
}
|
|
2025
|
+
const rgbPixels = new Uint8Array(display.innerWidth * display.innerHeight * 3);
|
|
2026
|
+
for (let y = 0; y < display.innerHeight; y += 1) {
|
|
2027
|
+
for (let x = 0; x < display.innerWidth; x += 1) {
|
|
2028
|
+
const sourceX = display.debugOffsetX + x;
|
|
2029
|
+
const sourceY = display.debugOffsetY + y;
|
|
2030
|
+
const sourceIndex = sourceY * display.debugWidth + sourceX;
|
|
2031
|
+
const paletteIndex = display.imageBytes[sourceIndex];
|
|
2032
|
+
const color = palette.items[paletteIndex];
|
|
2033
|
+
if (!color) {
|
|
2034
|
+
throw new ViceMcpError(
|
|
2035
|
+
"display_palette_index_invalid",
|
|
2036
|
+
"capture_display encountered a pixel index that is outside the current palette.",
|
|
2037
|
+
"protocol",
|
|
2038
|
+
false,
|
|
2039
|
+
{ paletteIndex, paletteSize: palette.items.length }
|
|
2040
|
+
);
|
|
2041
|
+
}
|
|
2042
|
+
const targetIndex = (y * display.innerWidth + x) * 3;
|
|
2043
|
+
rgbPixels[targetIndex] = color.red;
|
|
2044
|
+
rgbPixels[targetIndex + 1] = color.green;
|
|
2045
|
+
rgbPixels[targetIndex + 2] = color.blue;
|
|
2046
|
+
}
|
|
2047
|
+
}
|
|
2048
|
+
await import_promises.default.mkdir(DISPLAY_CAPTURE_DIR, { recursive: true });
|
|
2049
|
+
const imagePath = import_node_path.default.join(DISPLAY_CAPTURE_DIR, `capture-${Date.now()}-${process.pid}.png`);
|
|
2050
|
+
await import_promises.default.writeFile(imagePath, encodePngRgb(display.innerWidth, display.innerHeight, rgbPixels));
|
|
2051
|
+
await this.#settleDisplayToolState("capture_display", previousExecutionState);
|
|
2052
|
+
return {
|
|
2053
|
+
imagePath,
|
|
2054
|
+
width: display.innerWidth,
|
|
2055
|
+
height: display.innerHeight,
|
|
2056
|
+
debugWidth: display.debugWidth,
|
|
2057
|
+
debugHeight: display.debugHeight,
|
|
2058
|
+
debugOffsetX: display.debugOffsetX,
|
|
2059
|
+
debugOffsetY: display.debugOffsetY,
|
|
2060
|
+
bitsPerPixel: display.bitsPerPixel
|
|
2061
|
+
};
|
|
2062
|
+
});
|
|
2063
|
+
}
|
|
2064
|
+
async getDisplayState() {
|
|
2065
|
+
await this.#ensureReady();
|
|
2066
|
+
this.#syncMonitorRuntimeState();
|
|
2067
|
+
const previousExecutionState = this.#executionState;
|
|
2068
|
+
await this.#pauseForDisplayInspection("get_display_state", previousExecutionState);
|
|
2069
|
+
try {
|
|
2070
|
+
this.#writeProcessLogLine("[tx] get_display_state");
|
|
2071
|
+
const vicPrimary = await this.#client.readMemory(53265, 53272, 0);
|
|
2072
|
+
const cia2Bank = await this.#client.readMemory(56576, 56576, 0);
|
|
2073
|
+
const colors = await this.#client.readMemory(53280, 53284, 0);
|
|
2074
|
+
const d011 = vicPrimary.bytes[0] ?? 0;
|
|
2075
|
+
const d016 = vicPrimary.bytes[5] ?? 0;
|
|
2076
|
+
const d018 = vicPrimary.bytes[7] ?? 0;
|
|
2077
|
+
const dd00 = cia2Bank.bytes[0] ?? 0;
|
|
2078
|
+
const d020 = colors.bytes[0] ?? 0;
|
|
2079
|
+
const d021 = colors.bytes[1] ?? 0;
|
|
2080
|
+
const d022 = colors.bytes[2] ?? 0;
|
|
2081
|
+
const d023 = colors.bytes[3] ?? 0;
|
|
2082
|
+
const d024 = colors.bytes[4] ?? 0;
|
|
2083
|
+
const vicBankAddress = decodeVicBankAddress(dd00);
|
|
2084
|
+
const screenRamAddress = vicBankAddress + (d018 >> 4 & 15) * 1024;
|
|
2085
|
+
const characterMemoryAddress = vicBankAddress + (d018 >> 1 & 7) * 2048;
|
|
2086
|
+
const bitmapMemoryAddress = vicBankAddress + (d018 >> 3 & 1) * 8192;
|
|
2087
|
+
const { graphicsMode, extendedColorMode, bitmapMode, multicolorMode } = decodeGraphicsMode(d011, d016);
|
|
2088
|
+
const screenRam = await this.#client.readMemory(screenRamAddress, screenRamAddress + 999, 0);
|
|
2089
|
+
const colorRam = await this.#client.readMemory(55296, 55296 + 999, 0);
|
|
2090
|
+
return {
|
|
2091
|
+
graphicsMode,
|
|
2092
|
+
extendedColorMode,
|
|
2093
|
+
bitmapMode,
|
|
2094
|
+
multicolorMode,
|
|
2095
|
+
vicBankAddress,
|
|
2096
|
+
screenRamAddress,
|
|
2097
|
+
characterMemoryAddress: bitmapMode ? null : characterMemoryAddress,
|
|
2098
|
+
bitmapMemoryAddress: bitmapMode ? bitmapMemoryAddress : null,
|
|
2099
|
+
colorRamAddress: 55296,
|
|
2100
|
+
borderColor: lowNibble(d020),
|
|
2101
|
+
backgroundColor0: lowNibble(d021),
|
|
2102
|
+
backgroundColor1: lowNibble(d022),
|
|
2103
|
+
backgroundColor2: lowNibble(d023),
|
|
2104
|
+
backgroundColor3: lowNibble(d024),
|
|
2105
|
+
vicRegisters: {
|
|
2106
|
+
d011,
|
|
2107
|
+
d016,
|
|
2108
|
+
d018,
|
|
2109
|
+
dd00,
|
|
2110
|
+
d020,
|
|
2111
|
+
d021,
|
|
2112
|
+
d022,
|
|
2113
|
+
d023,
|
|
2114
|
+
d024
|
|
2115
|
+
},
|
|
2116
|
+
screenRam: Array.from(screenRam.bytes),
|
|
2117
|
+
colorRam: Array.from(colorRam.bytes, (value) => lowNibble(value))
|
|
2118
|
+
};
|
|
2119
|
+
} finally {
|
|
2120
|
+
await this.#settleDisplayToolState("get_display_state", previousExecutionState);
|
|
2121
|
+
}
|
|
2122
|
+
}
|
|
2123
|
+
async getDisplayText() {
|
|
2124
|
+
let displayState = await this.getDisplayState();
|
|
2125
|
+
if (displayState.screenRamAddress === 0 && displayState.graphicsMode === "standard_text") {
|
|
2126
|
+
this.#writeProcessLogLine("[display] screen RAM address still zero in text mode, retrying display state after short settle");
|
|
2127
|
+
await sleep(250);
|
|
2128
|
+
displayState = await this.getDisplayState();
|
|
2129
|
+
}
|
|
2130
|
+
if (!isTextGraphicsMode(displayState.graphicsMode)) {
|
|
2131
|
+
unsupportedError("get_display_text is only available when the current graphics mode is a text mode.", {
|
|
2132
|
+
graphicsMode: displayState.graphicsMode
|
|
2133
|
+
});
|
|
2134
|
+
}
|
|
2135
|
+
const columns = 40;
|
|
2136
|
+
const rows = 25;
|
|
2137
|
+
let hasDetailedTokens = false;
|
|
2138
|
+
const textLines = Array.from({ length: rows }, (_, row) => {
|
|
2139
|
+
const start = row * columns;
|
|
2140
|
+
return displayState.screenRam.slice(start, start + columns).map((code) => {
|
|
2141
|
+
const decoded = decodeScreenCodeCell(code);
|
|
2142
|
+
hasDetailedTokens ||= decoded.lossy;
|
|
2143
|
+
return decoded.ascii;
|
|
2144
|
+
}).join("").replace(/\s+$/, "");
|
|
2145
|
+
});
|
|
2146
|
+
const tokenLines = hasDetailedTokens ? Array.from({ length: rows }, (_, row) => {
|
|
2147
|
+
const start = row * columns;
|
|
2148
|
+
return displayState.screenRam.slice(start, start + columns).map((code) => {
|
|
2149
|
+
const decoded = decodeScreenCodeCell(code);
|
|
2150
|
+
return decoded.token ?? decoded.ascii;
|
|
2151
|
+
}).join("").replace(/\s+$/, "");
|
|
2152
|
+
}) : void 0;
|
|
2153
|
+
return {
|
|
2154
|
+
graphicsMode: displayState.graphicsMode,
|
|
2155
|
+
textMode: true,
|
|
2156
|
+
lossy: hasDetailedTokens,
|
|
2157
|
+
columns,
|
|
2158
|
+
rows,
|
|
2159
|
+
screenRamAddress: displayState.screenRamAddress,
|
|
2160
|
+
textLines,
|
|
2161
|
+
...tokenLines ? { tokenLines } : {}
|
|
2162
|
+
};
|
|
2163
|
+
}
|
|
2164
|
+
async autostartProgram(filePath, autoStart = true, fileIndex = 0) {
|
|
2165
|
+
await this.#ensureReady();
|
|
2166
|
+
const absolutePath = import_node_path.default.resolve(filePath);
|
|
2167
|
+
const previousExecutionState = this.#executionState;
|
|
2168
|
+
const previousStopReason = this.#lastStopReason;
|
|
2169
|
+
const executionEvent = this.#waitForExecutionEvent(EXECUTION_EVENT_WAIT_MS);
|
|
2170
|
+
this.#lastExecutionIntent = autoStart ? "none" : "monitor_entry";
|
|
2171
|
+
try {
|
|
2172
|
+
this.#writeProcessLogLine(`[tx] program_load filePath=${absolutePath} autoStart=${autoStart} fileIndex=${fileIndex}`);
|
|
2173
|
+
await this.#client.autostartProgram(absolutePath, autoStart, fileIndex);
|
|
2174
|
+
} catch (error) {
|
|
2175
|
+
const event2 = await executionEvent;
|
|
2176
|
+
const accepted = this.#autostartWasAcceptedAfterError(error, event2, previousExecutionState, previousStopReason);
|
|
2177
|
+
if (!accepted) {
|
|
2178
|
+
throw error;
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
const event = await executionEvent;
|
|
2182
|
+
if (event) {
|
|
2183
|
+
} else {
|
|
2184
|
+
this.#writeProcessLogLine(
|
|
2185
|
+
`[autostart] no runtime event observed within ${EXECUTION_EVENT_WAIT_MS}ms, waiting ${EXECUTION_SETTLE_DELAY_MS}ms for emulator settle`
|
|
2186
|
+
);
|
|
2187
|
+
await sleep(EXECUTION_SETTLE_DELAY_MS);
|
|
2188
|
+
}
|
|
2189
|
+
await this.#settleProgramLoadState(autoStart);
|
|
2190
|
+
return {
|
|
2191
|
+
filePath: absolutePath,
|
|
2192
|
+
autoStart,
|
|
2193
|
+
fileIndex,
|
|
2194
|
+
executionState: this.#executionState
|
|
2195
|
+
};
|
|
2196
|
+
}
|
|
2197
|
+
async #assertReadableProgramFile(filePath) {
|
|
2198
|
+
let stats;
|
|
2199
|
+
try {
|
|
2200
|
+
await import_promises.default.access(filePath);
|
|
2201
|
+
stats = await import_promises.default.stat(filePath);
|
|
2202
|
+
} catch (error) {
|
|
2203
|
+
throw new ViceMcpError("program_file_missing", `Program file does not exist or is not readable: ${filePath}`, "io", false, {
|
|
2204
|
+
filePath,
|
|
2205
|
+
cause: error instanceof Error ? error.message : String(error)
|
|
2206
|
+
});
|
|
2207
|
+
}
|
|
2208
|
+
if (!stats.isFile()) {
|
|
2209
|
+
throw new ViceMcpError("program_file_invalid", `Program path is not a regular file: ${filePath}`, "io", false, {
|
|
2210
|
+
filePath
|
|
2211
|
+
});
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
2214
|
+
async writeText(text) {
|
|
2215
|
+
await this.#ensureRunning("write_text");
|
|
2216
|
+
const encoded = decodeWriteTextToPetscii(text);
|
|
2217
|
+
if (encoded.length > MAX_WRITE_TEXT_BYTES) {
|
|
2218
|
+
validationError("write_text exceeds the maximum allowed byte length for one request", {
|
|
2219
|
+
length: encoded.length,
|
|
2220
|
+
max: MAX_WRITE_TEXT_BYTES
|
|
2221
|
+
});
|
|
2222
|
+
}
|
|
2223
|
+
this.#writeProcessLogLine(`[tx] write_text length=${encoded.length} text=${JSON.stringify(text)}`);
|
|
2224
|
+
await this.#client.sendKeys(Buffer.from(encoded).toString("binary"));
|
|
2225
|
+
await this.#settleInputState("write_text", "running");
|
|
2226
|
+
return {
|
|
2227
|
+
sent: true,
|
|
2228
|
+
length: encoded.length
|
|
2229
|
+
};
|
|
2230
|
+
}
|
|
2231
|
+
async keyboardInput(action, keys, durationMs) {
|
|
2232
|
+
await this.#ensureRunning("keyboard_input");
|
|
2233
|
+
if (!Array.isArray(keys) || keys.length === 0 || keys.length > 4) {
|
|
2234
|
+
validationError("keyboard_input requires between 1 and 4 keys", { keys });
|
|
2235
|
+
}
|
|
2236
|
+
const resolvedKeys = keys.map((key) => resolveKeyboardInputKey(key));
|
|
2237
|
+
const normalizedKeys = resolvedKeys.map((key) => key.canonical);
|
|
2238
|
+
this.#writeProcessLogLine(
|
|
2239
|
+
`[tx] keyboard_input action=${action} keys=${normalizedKeys.join(",")}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2240
|
+
);
|
|
2241
|
+
switch (action) {
|
|
2242
|
+
case "tap": {
|
|
2243
|
+
const duration = clampTapDuration(durationMs);
|
|
2244
|
+
const bytes = Uint8Array.from(resolvedKeys.flatMap((key) => Array.from(key.bytes)));
|
|
2245
|
+
await this.#client.sendKeys(Buffer.from(bytes).toString("binary"));
|
|
2246
|
+
await this.#settleInputState("keyboard_input", "running");
|
|
2247
|
+
await sleep(duration);
|
|
2248
|
+
return {
|
|
2249
|
+
action,
|
|
2250
|
+
keys: normalizedKeys,
|
|
2251
|
+
applied: true,
|
|
2252
|
+
held: false,
|
|
2253
|
+
mode: "buffered_text"
|
|
2254
|
+
};
|
|
2255
|
+
}
|
|
2256
|
+
case "press": {
|
|
2257
|
+
const singleByteKeys = resolvedKeys.map((key) => {
|
|
2258
|
+
if (key.bytes.length !== 1) {
|
|
2259
|
+
unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
|
|
2260
|
+
key: key.canonical
|
|
2261
|
+
});
|
|
2262
|
+
}
|
|
2263
|
+
return key.bytes[0];
|
|
2264
|
+
});
|
|
2265
|
+
for (let index = 0; index < normalizedKeys.length; index += 1) {
|
|
2266
|
+
const heldKey = normalizedKeys[index];
|
|
2267
|
+
const byte = singleByteKeys[index];
|
|
2268
|
+
if (!this.#heldKeyboardIntervals.has(heldKey)) {
|
|
2269
|
+
await this.#client.sendKeys(Buffer.from([byte]).toString("binary"));
|
|
2270
|
+
await this.#settleInputState("keyboard_input", "running");
|
|
2271
|
+
const interval = setInterval(() => {
|
|
2272
|
+
void this.#client.sendKeys(Buffer.from([byte]).toString("binary")).then(() => this.#settleInputState("keyboard_input", "running")).catch(() => void 0);
|
|
2273
|
+
}, DEFAULT_KEYBOARD_REPEAT_MS);
|
|
2274
|
+
this.#heldKeyboardIntervals.set(heldKey, interval);
|
|
2275
|
+
}
|
|
2276
|
+
}
|
|
2277
|
+
return {
|
|
2278
|
+
action,
|
|
2279
|
+
keys: normalizedKeys,
|
|
2280
|
+
applied: true,
|
|
2281
|
+
held: true,
|
|
2282
|
+
mode: "buffered_text_repeat"
|
|
2283
|
+
};
|
|
2284
|
+
}
|
|
2285
|
+
case "release": {
|
|
2286
|
+
for (const key of resolvedKeys) {
|
|
2287
|
+
if (key.bytes.length !== 1) {
|
|
2288
|
+
unsupportedError("keyboard_input press/release only supports keys that map to a single PETSCII byte.", {
|
|
2289
|
+
key: key.canonical
|
|
2290
|
+
});
|
|
2291
|
+
}
|
|
2292
|
+
}
|
|
2293
|
+
for (const heldKey of normalizedKeys) {
|
|
2294
|
+
const interval = this.#heldKeyboardIntervals.get(heldKey);
|
|
2295
|
+
if (interval) {
|
|
2296
|
+
clearInterval(interval);
|
|
2297
|
+
this.#heldKeyboardIntervals.delete(heldKey);
|
|
2298
|
+
}
|
|
2299
|
+
}
|
|
2300
|
+
return {
|
|
2301
|
+
action,
|
|
2302
|
+
keys: normalizedKeys,
|
|
2303
|
+
applied: true,
|
|
2304
|
+
held: false,
|
|
2305
|
+
mode: "buffered_text_repeat"
|
|
2306
|
+
};
|
|
2307
|
+
}
|
|
2308
|
+
}
|
|
2309
|
+
}
|
|
2310
|
+
async joystickInput(port2, action, control, durationMs) {
|
|
2311
|
+
await this.#ensureRunning("joystick_input");
|
|
2312
|
+
const previousExecutionState = this.#executionState;
|
|
2313
|
+
const bit = JOYSTICK_CONTROL_BITS[control];
|
|
2314
|
+
if (bit == null) {
|
|
2315
|
+
validationError("Unsupported joystick control", { control });
|
|
2316
|
+
}
|
|
2317
|
+
this.#writeProcessLogLine(
|
|
2318
|
+
`[tx] joystick_input port=${port2} action=${action} control=${control}${durationMs == null ? "" : ` durationMs=${durationMs}`}`
|
|
2319
|
+
);
|
|
2320
|
+
switch (action) {
|
|
2321
|
+
case "tap": {
|
|
2322
|
+
const duration = clampTapDuration(durationMs);
|
|
2323
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2324
|
+
await sleep(duration);
|
|
2325
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2326
|
+
break;
|
|
2327
|
+
}
|
|
2328
|
+
case "press":
|
|
2329
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) & ~bit);
|
|
2330
|
+
break;
|
|
2331
|
+
case "release":
|
|
2332
|
+
await this.#applyJoystickMask(port2, this.#getJoystickMask(port2) | bit);
|
|
2333
|
+
break;
|
|
2334
|
+
}
|
|
2335
|
+
await this.#settleInputState("joystick_input", previousExecutionState);
|
|
2336
|
+
return {
|
|
2337
|
+
port: port2,
|
|
2338
|
+
action,
|
|
2339
|
+
control,
|
|
2340
|
+
applied: true,
|
|
2341
|
+
state: this.#describeJoystickState(port2)
|
|
2342
|
+
};
|
|
2343
|
+
}
|
|
2344
|
+
async waitForState(targetState, timeoutMs = 5e3, stableMs = targetState === "running" ? INPUT_RUNNING_STABLE_MS : 0) {
|
|
2345
|
+
await this.#ensureReady();
|
|
2346
|
+
const startedAt = Date.now();
|
|
2347
|
+
const deadline = startedAt + timeoutMs;
|
|
2348
|
+
let matchingSince = null;
|
|
2349
|
+
while (true) {
|
|
2350
|
+
this.#syncMonitorRuntimeState();
|
|
2351
|
+
if (this.#executionState === targetState) {
|
|
2352
|
+
matchingSince ??= Date.now();
|
|
2353
|
+
if (Date.now() - matchingSince >= stableMs) {
|
|
2354
|
+
const runtime = this.#client.runtimeState();
|
|
2355
|
+
return {
|
|
2356
|
+
executionState: this.#executionState,
|
|
2357
|
+
lastStopReason: this.#lastStopReason,
|
|
2358
|
+
runtimeKnown: runtime.runtimeKnown,
|
|
2359
|
+
programCounter: runtime.programCounter,
|
|
2360
|
+
reachedTarget: true,
|
|
2361
|
+
waitedMs: Date.now() - startedAt
|
|
2362
|
+
};
|
|
2363
|
+
}
|
|
2364
|
+
} else {
|
|
2365
|
+
matchingSince = null;
|
|
2366
|
+
}
|
|
2367
|
+
if (Date.now() >= deadline) {
|
|
2368
|
+
const runtime = this.#client.runtimeState();
|
|
2369
|
+
return {
|
|
2370
|
+
executionState: this.#executionState,
|
|
2371
|
+
lastStopReason: this.#lastStopReason,
|
|
2372
|
+
runtimeKnown: runtime.runtimeKnown,
|
|
2373
|
+
programCounter: runtime.programCounter,
|
|
2374
|
+
reachedTarget: false,
|
|
2375
|
+
waitedMs: Date.now() - startedAt
|
|
2376
|
+
};
|
|
2377
|
+
}
|
|
2378
|
+
await sleep(INPUT_SETTLE_POLL_MS);
|
|
2379
|
+
}
|
|
2380
|
+
}
|
|
2381
|
+
async #ensureReady() {
|
|
2382
|
+
this.#ensureConfig();
|
|
2383
|
+
await this.#ensureHealthyConnection();
|
|
2384
|
+
}
|
|
2385
|
+
async #ensurePausedForDebug(commandName) {
|
|
2386
|
+
await this.#ensureReady();
|
|
2387
|
+
this.#syncMonitorRuntimeState();
|
|
2388
|
+
if (this.#executionState !== "stopped") {
|
|
2389
|
+
debuggerNotPausedError(commandName, {
|
|
2390
|
+
executionState: this.#executionState,
|
|
2391
|
+
lastStopReason: this.#lastStopReason
|
|
2392
|
+
});
|
|
2393
|
+
}
|
|
2394
|
+
}
|
|
2395
|
+
async #ensureRunning(commandName) {
|
|
2396
|
+
await this.#ensureReady();
|
|
2397
|
+
this.#syncMonitorRuntimeState();
|
|
2398
|
+
if (this.#executionState !== "running") {
|
|
2399
|
+
emulatorNotRunningError(commandName, {
|
|
2400
|
+
executionState: this.#executionState,
|
|
2401
|
+
lastStopReason: this.#lastStopReason
|
|
2402
|
+
});
|
|
2403
|
+
}
|
|
2404
|
+
}
|
|
2405
|
+
async #ensureHealthyConnection() {
|
|
2406
|
+
if (this.#recoveryPromise) {
|
|
2407
|
+
await this.#recoveryPromise;
|
|
2408
|
+
}
|
|
2409
|
+
const processAlive = this.#process != null && this.#process.exitCode == null && !this.#process.killed;
|
|
2410
|
+
if (processAlive && this.#client.connected) {
|
|
2411
|
+
this.#syncMonitorRuntimeState();
|
|
2412
|
+
return;
|
|
2413
|
+
}
|
|
2414
|
+
await this.#scheduleRecovery();
|
|
2415
|
+
}
|
|
2416
|
+
async #scheduleRecovery() {
|
|
2417
|
+
this.#ensureConfig();
|
|
2418
|
+
if (this.#recoveryPromise) {
|
|
2419
|
+
return await this.#recoveryPromise;
|
|
2420
|
+
}
|
|
2421
|
+
this.#recoveryPromise = (async () => {
|
|
2422
|
+
this.#recoveryInProgress = true;
|
|
2423
|
+
try {
|
|
2424
|
+
const processAlive = this.#process != null && this.#process.exitCode == null && !this.#process.killed;
|
|
2425
|
+
if (processAlive && this.#host && this.#port) {
|
|
2426
|
+
this.#transportState = "reconnecting";
|
|
2427
|
+
await this.#client.connect(this.#host, this.#port);
|
|
2428
|
+
this.#transportState = "connected";
|
|
2429
|
+
if (!this.#connectedSince) {
|
|
2430
|
+
this.#connectedSince = nowIso();
|
|
2431
|
+
}
|
|
2432
|
+
await this.#hydrateExecutionState();
|
|
2433
|
+
return;
|
|
2434
|
+
}
|
|
2435
|
+
await this.#launchManagedEmulator("restart");
|
|
2436
|
+
} finally {
|
|
2437
|
+
this.#recoveryInProgress = false;
|
|
2438
|
+
this.#recoveryPromise = null;
|
|
2439
|
+
}
|
|
2440
|
+
})();
|
|
2441
|
+
return await this.#recoveryPromise;
|
|
2442
|
+
}
|
|
2443
|
+
async #replaceManagedEmulator(mode) {
|
|
2444
|
+
await this.#stopManagedProcess(true);
|
|
2445
|
+
await this.#launchManagedEmulator(mode);
|
|
2446
|
+
}
|
|
2447
|
+
async #launchManagedEmulator(mode) {
|
|
2448
|
+
const config = this.#ensureConfig();
|
|
2449
|
+
const host2 = DEFAULT_MONITOR_HOST;
|
|
2450
|
+
const port2 = await this.#portAllocator.allocate();
|
|
2451
|
+
await this.#portAllocator.ensureFree(port2, host2);
|
|
2452
|
+
const binary = config.binaryPath ?? DEFAULT_C64_BINARY;
|
|
2453
|
+
const args = ["-autostartprgmode", "1", "-binarymonitor", "-binarymonitoraddress", `${host2}:${port2}`];
|
|
2454
|
+
if (config.arguments) {
|
|
2455
|
+
args.push(...splitCommandLine(config.arguments));
|
|
2456
|
+
}
|
|
2457
|
+
this.#transportState = "starting";
|
|
2458
|
+
this.#processState = "launching";
|
|
2459
|
+
this.#host = host2;
|
|
2460
|
+
this.#port = port2;
|
|
2461
|
+
this.#connectedSince = null;
|
|
2462
|
+
this.#executionState = "unknown";
|
|
2463
|
+
this.#breakpointLabels.clear();
|
|
2464
|
+
this.#explicitPauseActive = false;
|
|
2465
|
+
this.#pendingCheckpointHit = null;
|
|
2466
|
+
this.#lastCheckpointHit = null;
|
|
2467
|
+
this.#lastRuntimeEventType = "unknown";
|
|
2468
|
+
this.#lastRuntimeProgramCounter = null;
|
|
2469
|
+
const env = await buildViceLaunchEnv();
|
|
2470
|
+
const child = (0, import_node_child_process.spawn)(binary, args, {
|
|
2471
|
+
cwd: config.workingDirectory ? import_node_path.default.resolve(config.workingDirectory) : void 0,
|
|
2472
|
+
env,
|
|
2473
|
+
stdio: ["ignore", "pipe", "pipe"]
|
|
2474
|
+
});
|
|
2475
|
+
this.#process = child;
|
|
2476
|
+
this.#attachProcessLogging(child, binary, args);
|
|
2477
|
+
this.#bindProcessLifecycle(child);
|
|
2478
|
+
this.#processState = "running";
|
|
2479
|
+
this.#transportState = "waiting_for_monitor";
|
|
2480
|
+
try {
|
|
2481
|
+
await waitForMonitor(host2, port2, 5e3);
|
|
2482
|
+
await this.#client.connect(host2, port2);
|
|
2483
|
+
this.#transportState = "connected";
|
|
2484
|
+
this.#connectedSince = nowIso();
|
|
2485
|
+
this.#launchId += 1;
|
|
2486
|
+
if (mode === "restart") {
|
|
2487
|
+
this.#restartCount += 1;
|
|
2488
|
+
}
|
|
2489
|
+
this.#freshEmulatorPending = true;
|
|
2490
|
+
await this.#hydrateExecutionState();
|
|
2491
|
+
} catch (error) {
|
|
2492
|
+
this.#processState = "crashed";
|
|
2493
|
+
this.#transportState = "faulted";
|
|
2494
|
+
this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "launch_failed"), makeWarning(String(error.message ?? error), "launch_failed")];
|
|
2495
|
+
await this.#stopManagedProcess(true);
|
|
2496
|
+
throw error;
|
|
2497
|
+
}
|
|
2498
|
+
}
|
|
2499
|
+
#ensureConfig() {
|
|
2500
|
+
if (!this.#config) {
|
|
2501
|
+
this.#config = defaultC64Config();
|
|
2502
|
+
}
|
|
2503
|
+
return this.#config;
|
|
2504
|
+
}
|
|
2505
|
+
#bindProcessLifecycle(child) {
|
|
2506
|
+
child.once("exit", (code, signal) => {
|
|
2507
|
+
if (this.#process !== child) {
|
|
2508
|
+
return;
|
|
2509
|
+
}
|
|
2510
|
+
this.#closeProcessLog(child, `process exit (${code ?? "null"} / ${signal ?? "null"})`);
|
|
2511
|
+
this.#process = null;
|
|
2512
|
+
this.#processState = code === 0 ? "exited" : "crashed";
|
|
2513
|
+
this.#transportState = "disconnected";
|
|
2514
|
+
this.#warnings = [
|
|
2515
|
+
...this.#warnings.filter((warning) => warning.code !== "process_exit"),
|
|
2516
|
+
makeWarning(`C64 emulator process exited (${code ?? "null"} / ${signal ?? "null"})`, "process_exit")
|
|
2517
|
+
];
|
|
2518
|
+
if (!this.#suppressRecovery && !this.#shuttingDown && this.#config) {
|
|
2519
|
+
void this.#scheduleRecovery();
|
|
2520
|
+
}
|
|
2521
|
+
});
|
|
2522
|
+
child.once("error", (error) => {
|
|
2523
|
+
if (this.#process !== child) {
|
|
2524
|
+
return;
|
|
2525
|
+
}
|
|
2526
|
+
this.#closeProcessLog(child, `process error (${error.message})`);
|
|
2527
|
+
this.#process = null;
|
|
2528
|
+
this.#processState = "crashed";
|
|
2529
|
+
this.#transportState = "faulted";
|
|
2530
|
+
this.#warnings = [...this.#warnings.filter((warning) => warning.code !== "process_error"), makeWarning(error.message, "process_error")];
|
|
2531
|
+
if (!this.#suppressRecovery && !this.#shuttingDown && this.#config) {
|
|
2532
|
+
void this.#scheduleRecovery();
|
|
2533
|
+
}
|
|
2534
|
+
});
|
|
2535
|
+
}
|
|
2536
|
+
#attachProcessLogging(child, binary, args) {
|
|
2537
|
+
const logStream = (0, import_node_fs.createWriteStream)(VICE_PROCESS_LOG_PATH, { flags: "a" });
|
|
2538
|
+
this.#processLogStream = logStream;
|
|
2539
|
+
this.#stdoutMirrorBuffer = "";
|
|
2540
|
+
this.#stderrMirrorBuffer = "";
|
|
2541
|
+
logStream.write(`
|
|
2542
|
+
=== Emulator launch ${nowIso()} ===
|
|
2543
|
+
`);
|
|
2544
|
+
logStream.write(`binary: ${binary}
|
|
2545
|
+
`);
|
|
2546
|
+
logStream.write(`args: ${args.join(" ")}
|
|
2547
|
+
`);
|
|
2548
|
+
child.stdout?.pipe(logStream, { end: false });
|
|
2549
|
+
child.stderr?.pipe(logStream, { end: false });
|
|
2550
|
+
if (MIRROR_EMULATOR_LOGS_TO_STDERR) {
|
|
2551
|
+
child.stdout?.on("data", (chunk) => {
|
|
2552
|
+
this.#mirrorViceOutputChunk("stdout", chunk);
|
|
2553
|
+
});
|
|
2554
|
+
child.stderr?.on("data", (chunk) => {
|
|
2555
|
+
this.#mirrorViceOutputChunk("stderr", chunk);
|
|
2556
|
+
});
|
|
2557
|
+
}
|
|
2558
|
+
}
|
|
2559
|
+
#closeProcessLog(child, reason) {
|
|
2560
|
+
const logStream = this.#processLogStream;
|
|
2561
|
+
if (!logStream) {
|
|
2562
|
+
return;
|
|
2563
|
+
}
|
|
2564
|
+
child.stdout?.unpipe(logStream);
|
|
2565
|
+
child.stderr?.unpipe(logStream);
|
|
2566
|
+
this.#flushViceOutputMirror("stdout");
|
|
2567
|
+
this.#flushViceOutputMirror("stderr");
|
|
2568
|
+
logStream.write(`
|
|
2569
|
+
=== Emulator stream closed ${nowIso()} (${reason}) ===
|
|
2570
|
+
`);
|
|
2571
|
+
logStream.end();
|
|
2572
|
+
this.#processLogStream = null;
|
|
2573
|
+
}
|
|
2574
|
+
#mirrorViceOutputChunk(stream, chunk) {
|
|
2575
|
+
const text = String(chunk);
|
|
2576
|
+
const buffer = stream === "stdout" ? this.#stdoutMirrorBuffer : this.#stderrMirrorBuffer;
|
|
2577
|
+
const combined = buffer + text;
|
|
2578
|
+
const lines = combined.split(/\r?\n/);
|
|
2579
|
+
const remainder = lines.pop() ?? "";
|
|
2580
|
+
for (const line of lines) {
|
|
2581
|
+
process.stderr.write(`[vice ${stream}] ${line}
|
|
2582
|
+
`);
|
|
2583
|
+
}
|
|
2584
|
+
if (stream === "stdout") {
|
|
2585
|
+
this.#stdoutMirrorBuffer = remainder;
|
|
2586
|
+
} else {
|
|
2587
|
+
this.#stderrMirrorBuffer = remainder;
|
|
2588
|
+
}
|
|
2589
|
+
}
|
|
2590
|
+
#flushViceOutputMirror(stream) {
|
|
2591
|
+
const remainder = stream === "stdout" ? this.#stdoutMirrorBuffer : this.#stderrMirrorBuffer;
|
|
2592
|
+
if (!remainder) {
|
|
2593
|
+
return;
|
|
2594
|
+
}
|
|
2595
|
+
process.stderr.write(`[vice ${stream}] ${remainder}
|
|
2596
|
+
`);
|
|
2597
|
+
if (stream === "stdout") {
|
|
2598
|
+
this.#stdoutMirrorBuffer = "";
|
|
2599
|
+
} else {
|
|
2600
|
+
this.#stderrMirrorBuffer = "";
|
|
2601
|
+
}
|
|
2602
|
+
}
|
|
2603
|
+
#writeProcessLogLine(line) {
|
|
2604
|
+
this.#processLogStream?.write(`${nowIso()} ${line}
|
|
2605
|
+
`);
|
|
2606
|
+
if (MIRROR_EMULATOR_LOGS_TO_STDERR) {
|
|
2607
|
+
process.stderr.write(`[vice monitor] ${line}
|
|
2608
|
+
`);
|
|
2609
|
+
}
|
|
2610
|
+
}
|
|
2611
|
+
async #stopManagedProcess(fullReset) {
|
|
2612
|
+
this.#suppressRecovery = true;
|
|
2613
|
+
try {
|
|
2614
|
+
this.#clearHeldInputState();
|
|
2615
|
+
const processId = this.#process?.pid ?? null;
|
|
2616
|
+
this.#breakpointLabels.clear();
|
|
2617
|
+
this.#explicitPauseActive = false;
|
|
2618
|
+
this.#pendingCheckpointHit = null;
|
|
2619
|
+
this.#lastCheckpointHit = null;
|
|
2620
|
+
if (this.#client.connected) {
|
|
2621
|
+
try {
|
|
2622
|
+
await this.#client.quit();
|
|
2623
|
+
} catch {
|
|
2624
|
+
if (this.#process) {
|
|
2625
|
+
this.#process.kill("SIGTERM");
|
|
2626
|
+
}
|
|
2627
|
+
}
|
|
2628
|
+
} else if (this.#process) {
|
|
2629
|
+
this.#process.kill("SIGTERM");
|
|
2630
|
+
}
|
|
2631
|
+
if (this.#process) {
|
|
2632
|
+
await waitForProcessExit(this.#process, 1e3).catch(() => {
|
|
2633
|
+
this.#process?.kill("SIGKILL");
|
|
2634
|
+
});
|
|
2635
|
+
}
|
|
2636
|
+
await this.#client.disconnect();
|
|
2637
|
+
this.#process = null;
|
|
2638
|
+
if (fullReset) {
|
|
2639
|
+
this.#transportState = "stopped";
|
|
2640
|
+
this.#processState = processId == null ? "not_applicable" : "exited";
|
|
2641
|
+
}
|
|
2642
|
+
} finally {
|
|
2643
|
+
this.#suppressRecovery = false;
|
|
2644
|
+
}
|
|
2645
|
+
}
|
|
2646
|
+
async #resumeBeforeShutdown() {
|
|
2647
|
+
if (!this.#client.connected) {
|
|
2648
|
+
return;
|
|
2649
|
+
}
|
|
2650
|
+
this.#syncMonitorRuntimeState();
|
|
2651
|
+
if (this.#executionState !== "stopped") {
|
|
2652
|
+
return;
|
|
2653
|
+
}
|
|
2654
|
+
this.#writeProcessLogLine("[shutdown] emulator stopped in monitor, resuming before quit");
|
|
2655
|
+
try {
|
|
2656
|
+
this.#lastExecutionIntent = "unknown";
|
|
2657
|
+
await this.#client.continueExecution();
|
|
2658
|
+
await this.#waitForExecutionEvent(1e3);
|
|
2659
|
+
this.#syncMonitorRuntimeState();
|
|
2660
|
+
} catch (error) {
|
|
2661
|
+
this.#writeProcessLogLine(
|
|
2662
|
+
`[shutdown] resume before quit failed: ${error instanceof Error ? error.message : String(error)}`
|
|
2663
|
+
);
|
|
2664
|
+
}
|
|
2665
|
+
}
|
|
2666
|
+
async #hydrateExecutionState() {
|
|
2667
|
+
await this.#stabilizeLaunchExecutionState();
|
|
2668
|
+
}
|
|
2669
|
+
async #readRegisters() {
|
|
2670
|
+
const metadata = await this.#client.getRegistersAvailable();
|
|
2671
|
+
const values = await this.#client.getRegisters();
|
|
2672
|
+
const registers = this.#mapC64Registers(metadata.registers, values.registers);
|
|
2673
|
+
this.#lastRegisters = registers;
|
|
2674
|
+
return registers;
|
|
2675
|
+
}
|
|
2676
|
+
async #readDebugState() {
|
|
2677
|
+
const registers = await this.#readRegisters();
|
|
2678
|
+
return this.#buildDebugState(registers);
|
|
2679
|
+
}
|
|
2680
|
+
#buildDebugState(registers) {
|
|
2681
|
+
this.#lastRegisters = registers;
|
|
2682
|
+
return {
|
|
2683
|
+
executionState: this.#executionState,
|
|
2684
|
+
lastStopReason: this.#lastStopReason,
|
|
2685
|
+
programCounter: registers.PC,
|
|
2686
|
+
registers
|
|
2687
|
+
};
|
|
2688
|
+
}
|
|
2689
|
+
#mergeRegisters(current, updated) {
|
|
2690
|
+
if (!current) {
|
|
2691
|
+
return null;
|
|
2692
|
+
}
|
|
2693
|
+
return {
|
|
2694
|
+
...current,
|
|
2695
|
+
...updated
|
|
2696
|
+
};
|
|
2697
|
+
}
|
|
2698
|
+
#validateRange(start, end) {
|
|
2699
|
+
if (end < start) {
|
|
2700
|
+
validationError("End address must be greater than or equal to start address", { start, end });
|
|
2701
|
+
}
|
|
2702
|
+
if (start < 0 || end > 65535) {
|
|
2703
|
+
validationError("Address range must fit in 16-bit address space", { start, end });
|
|
2704
|
+
}
|
|
2705
|
+
}
|
|
2706
|
+
#mapC64Registers(metadata, values) {
|
|
2707
|
+
const metadataByName = new Map(metadata.map((item) => [item.name.toUpperCase(), item]));
|
|
2708
|
+
const valuesById = new Map(values.map((item) => [item.id, item.value]));
|
|
2709
|
+
return Object.fromEntries(
|
|
2710
|
+
C64_REGISTER_DEFINITIONS.map((definition) => {
|
|
2711
|
+
const meta = metadataByName.get(definition.viceName.toUpperCase());
|
|
2712
|
+
if (!meta) {
|
|
2713
|
+
validationError(`Required C64 register is missing from the emulator: ${definition.viceName}`, {
|
|
2714
|
+
registerName: definition.fieldName,
|
|
2715
|
+
viceName: definition.viceName
|
|
2716
|
+
});
|
|
2717
|
+
}
|
|
2718
|
+
const value = valuesById.get(meta.id);
|
|
2719
|
+
if (value == null) {
|
|
2720
|
+
validationError(`Required C64 register value is missing from the emulator: ${definition.viceName}`, {
|
|
2721
|
+
registerName: definition.fieldName,
|
|
2722
|
+
viceName: definition.viceName
|
|
2723
|
+
});
|
|
2724
|
+
}
|
|
2725
|
+
return [definition.fieldName, value];
|
|
2726
|
+
})
|
|
2727
|
+
);
|
|
2728
|
+
}
|
|
2729
|
+
#getJoystickMask(port2) {
|
|
2730
|
+
return this.#heldJoystickMasks.get(port2) ?? JOYSTICK_RELEASED_MASK;
|
|
2731
|
+
}
|
|
2732
|
+
async #applyJoystickMask(port2, mask) {
|
|
2733
|
+
const normalizedMask = mask & JOYSTICK_RELEASED_MASK;
|
|
2734
|
+
this.#heldJoystickMasks.set(port2, normalizedMask);
|
|
2735
|
+
await this.#client.setJoyport(joystickPortToProtocol(port2), normalizedMask);
|
|
2736
|
+
}
|
|
2737
|
+
#describeJoystickState(port2) {
|
|
2738
|
+
const mask = this.#getJoystickMask(port2);
|
|
2739
|
+
return {
|
|
2740
|
+
up: (mask & JOYSTICK_CONTROL_BITS.up) === 0,
|
|
2741
|
+
down: (mask & JOYSTICK_CONTROL_BITS.down) === 0,
|
|
2742
|
+
left: (mask & JOYSTICK_CONTROL_BITS.left) === 0,
|
|
2743
|
+
right: (mask & JOYSTICK_CONTROL_BITS.right) === 0,
|
|
2744
|
+
fire: (mask & JOYSTICK_CONTROL_BITS.fire) === 0
|
|
2745
|
+
};
|
|
2746
|
+
}
|
|
2747
|
+
#clearHeldInputState() {
|
|
2748
|
+
for (const interval of this.#heldKeyboardIntervals.values()) {
|
|
2749
|
+
clearInterval(interval);
|
|
2750
|
+
}
|
|
2751
|
+
this.#heldKeyboardIntervals.clear();
|
|
2752
|
+
this.#heldJoystickMasks.clear();
|
|
2753
|
+
}
|
|
2754
|
+
#attachBreakpointLabel(breakpoint) {
|
|
2755
|
+
return {
|
|
2756
|
+
...breakpoint,
|
|
2757
|
+
label: this.#breakpointLabels.get(breakpoint.id) ?? null
|
|
2758
|
+
};
|
|
2759
|
+
}
|
|
2760
|
+
#pruneBreakpointLabels(activeBreakpointIds) {
|
|
2761
|
+
const active = new Set(activeBreakpointIds);
|
|
2762
|
+
for (const breakpointId of this.#breakpointLabels.keys()) {
|
|
2763
|
+
if (!active.has(breakpointId)) {
|
|
2764
|
+
this.#breakpointLabels.delete(breakpointId);
|
|
2765
|
+
}
|
|
2766
|
+
}
|
|
2767
|
+
}
|
|
2768
|
+
async #waitForExecutionEvent(timeoutMs) {
|
|
2769
|
+
return await new Promise((resolve) => {
|
|
2770
|
+
const timer = setTimeout(() => {
|
|
2771
|
+
this.#client.off("event", onEvent);
|
|
2772
|
+
resolve(null);
|
|
2773
|
+
}, timeoutMs);
|
|
2774
|
+
const onEvent = (event) => {
|
|
2775
|
+
if (event.type !== "resumed" && event.type !== "stopped" && event.type !== "jam") {
|
|
2776
|
+
return;
|
|
2777
|
+
}
|
|
2778
|
+
clearTimeout(timer);
|
|
2779
|
+
this.#client.off("event", onEvent);
|
|
2780
|
+
resolve({ type: event.type });
|
|
2781
|
+
};
|
|
2782
|
+
this.#client.on("event", onEvent);
|
|
2783
|
+
});
|
|
2784
|
+
}
|
|
2785
|
+
async #stabilizeLaunchExecutionState() {
|
|
2786
|
+
this.#writeProcessLogLine(`[bootstrap] waiting ${BOOTSTRAP_INITIAL_DELAY_MS}ms before probing launch state`);
|
|
2787
|
+
await sleep(BOOTSTRAP_INITIAL_DELAY_MS);
|
|
2788
|
+
const deadline = Date.now() + BOOTSTRAP_SETTLE_TIMEOUT_MS;
|
|
2789
|
+
let runningSince = null;
|
|
2790
|
+
let lastResumeAt = 0;
|
|
2791
|
+
while (true) {
|
|
2792
|
+
this.#syncMonitorRuntimeState();
|
|
2793
|
+
if (this.#executionState === "running") {
|
|
2794
|
+
runningSince ??= Date.now();
|
|
2795
|
+
if (Date.now() - runningSince >= BOOTSTRAP_RUNNING_STABLE_MS) {
|
|
2796
|
+
this.#writeProcessLogLine("[bootstrap] emulator reached stable running state after launch");
|
|
2797
|
+
return;
|
|
2798
|
+
}
|
|
2799
|
+
} else {
|
|
2800
|
+
runningSince = null;
|
|
2801
|
+
}
|
|
2802
|
+
if (this.#executionState !== "running" && Date.now() - lastResumeAt >= BOOTSTRAP_RESUME_COOLDOWN_MS) {
|
|
2803
|
+
this.#writeProcessLogLine(`[bootstrap] observed ${this.#executionState} state after launch, sending resume`);
|
|
2804
|
+
const previousExecutionState = this.#executionState;
|
|
2805
|
+
const previousStopReason = this.#lastStopReason;
|
|
2806
|
+
const executionEvent = this.#waitForExecutionEvent(EXECUTION_EVENT_WAIT_MS);
|
|
2807
|
+
this.#lastExecutionIntent = "unknown";
|
|
2808
|
+
try {
|
|
2809
|
+
await this.#client.continueExecution();
|
|
2810
|
+
} catch (error) {
|
|
2811
|
+
const event2 = await executionEvent;
|
|
2812
|
+
const accepted = this.#resumeWasAcceptedAfterError(error, event2, previousExecutionState, previousStopReason);
|
|
2813
|
+
if (!accepted) {
|
|
2814
|
+
this.#writeProcessLogLine(
|
|
2815
|
+
`[bootstrap] resume after launch failed without runtime transition: ${error instanceof Error ? error.message : String(error)}`
|
|
2816
|
+
);
|
|
2817
|
+
throw error;
|
|
2818
|
+
}
|
|
2819
|
+
}
|
|
2820
|
+
const event = await executionEvent;
|
|
2821
|
+
if (!event) {
|
|
2822
|
+
this.#writeProcessLogLine(
|
|
2823
|
+
`[bootstrap] no runtime event observed within ${EXECUTION_EVENT_WAIT_MS}ms after resume, waiting ${BOOTSTRAP_POLL_MS}ms`
|
|
2824
|
+
);
|
|
2825
|
+
await sleep(BOOTSTRAP_POLL_MS);
|
|
2826
|
+
}
|
|
2827
|
+
lastResumeAt = Date.now();
|
|
2828
|
+
continue;
|
|
2829
|
+
}
|
|
2830
|
+
if (Date.now() >= deadline) {
|
|
2831
|
+
this.#writeProcessLogLine(
|
|
2832
|
+
`[bootstrap] settle timeout reached after ${BOOTSTRAP_SETTLE_TIMEOUT_MS}ms with executionState=${this.#executionState}`
|
|
2833
|
+
);
|
|
2834
|
+
throw new ViceMcpError(
|
|
2835
|
+
"bootstrap_timeout",
|
|
2836
|
+
"Emulator launch did not reach a stable running state before timeout.",
|
|
2837
|
+
"timeout",
|
|
2838
|
+
true,
|
|
2839
|
+
{
|
|
2840
|
+
executionState: this.#executionState,
|
|
2841
|
+
lastStopReason: this.#lastStopReason
|
|
2842
|
+
}
|
|
2843
|
+
);
|
|
2844
|
+
}
|
|
2845
|
+
await sleep(BOOTSTRAP_POLL_MS);
|
|
2846
|
+
}
|
|
2847
|
+
}
|
|
2848
|
+
async #withExecutionLock(operation) {
|
|
2849
|
+
while (this.#executionOperationLock) {
|
|
2850
|
+
await this.#executionOperationLock;
|
|
2851
|
+
}
|
|
2852
|
+
let resolve;
|
|
2853
|
+
this.#executionOperationLock = new Promise((r) => resolve = r);
|
|
2854
|
+
try {
|
|
2855
|
+
return await operation();
|
|
2856
|
+
} finally {
|
|
2857
|
+
this.#executionOperationLock = null;
|
|
2858
|
+
resolve();
|
|
2859
|
+
}
|
|
2860
|
+
}
|
|
2861
|
+
async #withDisplayLock(operation) {
|
|
2862
|
+
while (this.#displayOperationLock) {
|
|
2863
|
+
await this.#displayOperationLock;
|
|
2864
|
+
}
|
|
2865
|
+
let resolve;
|
|
2866
|
+
this.#displayOperationLock = new Promise((r) => resolve = r);
|
|
2867
|
+
try {
|
|
2868
|
+
return await operation();
|
|
2869
|
+
} finally {
|
|
2870
|
+
this.#displayOperationLock = null;
|
|
2871
|
+
resolve();
|
|
2872
|
+
}
|
|
2873
|
+
}
|
|
2874
|
+
async #settleProgramLoadState(autoStart) {
|
|
2875
|
+
const deadline = Date.now() + PROGRAM_LOAD_SETTLE_TIMEOUT_MS;
|
|
2876
|
+
let runningSince = null;
|
|
2877
|
+
let lastResumeAt = 0;
|
|
2878
|
+
while (true) {
|
|
2879
|
+
this.#syncMonitorRuntimeState();
|
|
2880
|
+
if (this.#executionState === "running") {
|
|
2881
|
+
runningSince ??= Date.now();
|
|
2882
|
+
if (Date.now() - runningSince >= PROGRAM_LOAD_RUNNING_STABLE_MS) {
|
|
2883
|
+
return;
|
|
2884
|
+
}
|
|
2885
|
+
} else {
|
|
2886
|
+
runningSince = null;
|
|
2887
|
+
}
|
|
2888
|
+
if (this.#executionState === "stopped" && Date.now() - lastResumeAt >= PROGRAM_LOAD_RESUME_COOLDOWN_MS) {
|
|
2889
|
+
this.#writeProcessLogLine(
|
|
2890
|
+
`[autostart] observed stopped state after load with autoStart=${autoStart}, sending resume`
|
|
2891
|
+
);
|
|
2892
|
+
this.#explicitPauseActive = false;
|
|
2893
|
+
this.#lastExecutionIntent = "unknown";
|
|
2894
|
+
await this.#client.continueExecution();
|
|
2895
|
+
lastResumeAt = Date.now();
|
|
2896
|
+
}
|
|
2897
|
+
if (Date.now() >= deadline) {
|
|
2898
|
+
this.#writeProcessLogLine(
|
|
2899
|
+
`[autostart] settle timeout reached after ${PROGRAM_LOAD_SETTLE_TIMEOUT_MS}ms with executionState=${this.#executionState}`
|
|
2900
|
+
);
|
|
2901
|
+
return;
|
|
2902
|
+
}
|
|
2903
|
+
await sleep(PROGRAM_LOAD_SETTLE_POLL_MS);
|
|
2904
|
+
}
|
|
2905
|
+
}
|
|
2906
|
+
async #pauseForDisplayInspection(commandName, previousExecutionState) {
|
|
2907
|
+
if (previousExecutionState === "stopped") {
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
if (previousExecutionState !== "running") {
|
|
2911
|
+
emulatorNotRunningError(commandName, {
|
|
2912
|
+
executionState: previousExecutionState,
|
|
2913
|
+
lastStopReason: this.#lastStopReason
|
|
2914
|
+
});
|
|
2915
|
+
}
|
|
2916
|
+
this.#writeProcessLogLine(`[display] ${commandName} started while running, waiting for a temporary stop`);
|
|
2917
|
+
await this.#client.setBreakpoint({
|
|
2918
|
+
start: 0,
|
|
2919
|
+
end: 65535,
|
|
2920
|
+
kind: "exec",
|
|
2921
|
+
temporary: true,
|
|
2922
|
+
enabled: true,
|
|
2923
|
+
stopWhenHit: true
|
|
2924
|
+
});
|
|
2925
|
+
const deadline = Date.now() + DISPLAY_PAUSE_TIMEOUT_MS;
|
|
2926
|
+
while (true) {
|
|
2927
|
+
this.#syncMonitorRuntimeState();
|
|
2928
|
+
if (this.#executionState === "stopped") {
|
|
2929
|
+
return;
|
|
2930
|
+
}
|
|
2931
|
+
if (Date.now() >= deadline) {
|
|
2932
|
+
throw new ViceMcpError(
|
|
2933
|
+
"display_pause_timeout",
|
|
2934
|
+
`${commandName} could not reach a temporary stopped state before timeout.`,
|
|
2935
|
+
"timeout",
|
|
2936
|
+
true,
|
|
2937
|
+
{
|
|
2938
|
+
commandName,
|
|
2939
|
+
executionState: this.#executionState,
|
|
2940
|
+
lastStopReason: this.#lastStopReason
|
|
2941
|
+
}
|
|
2942
|
+
);
|
|
2943
|
+
}
|
|
2944
|
+
await sleep(DISPLAY_SETTLE_POLL_MS);
|
|
2945
|
+
}
|
|
2946
|
+
}
|
|
2947
|
+
async #settleDisplayToolState(commandName, previousExecutionState) {
|
|
2948
|
+
this.#syncMonitorRuntimeState();
|
|
2949
|
+
if (previousExecutionState !== "running") {
|
|
2950
|
+
return;
|
|
2951
|
+
}
|
|
2952
|
+
const deadline = Date.now() + DISPLAY_SETTLE_TIMEOUT_MS;
|
|
2953
|
+
let runningSince = null;
|
|
2954
|
+
let lastResumeAt = 0;
|
|
2955
|
+
while (true) {
|
|
2956
|
+
this.#syncMonitorRuntimeState();
|
|
2957
|
+
if (this.#executionState === "running") {
|
|
2958
|
+
runningSince ??= Date.now();
|
|
2959
|
+
if (Date.now() - runningSince >= DISPLAY_RUNNING_STABLE_MS) {
|
|
2960
|
+
return;
|
|
2961
|
+
}
|
|
2962
|
+
} else {
|
|
2963
|
+
runningSince = null;
|
|
2964
|
+
}
|
|
2965
|
+
if (this.#executionState === "stopped" && Date.now() - lastResumeAt >= DISPLAY_RESUME_COOLDOWN_MS) {
|
|
2966
|
+
this.#writeProcessLogLine(`[display] observed stopped state after ${commandName}, sending resume`);
|
|
2967
|
+
this.#lastExecutionIntent = "unknown";
|
|
2968
|
+
await this.#client.continueExecution();
|
|
2969
|
+
lastResumeAt = Date.now();
|
|
2970
|
+
}
|
|
2971
|
+
if (Date.now() >= deadline) {
|
|
2972
|
+
this.#writeProcessLogLine(
|
|
2973
|
+
`[display] settle timeout reached after ${DISPLAY_SETTLE_TIMEOUT_MS}ms after ${commandName} with executionState=${this.#executionState}`
|
|
2974
|
+
);
|
|
2975
|
+
if (this.#executionState !== "running") {
|
|
2976
|
+
emulatorNotRunningError(commandName, {
|
|
2977
|
+
executionState: this.#executionState,
|
|
2978
|
+
lastStopReason: this.#lastStopReason
|
|
2979
|
+
});
|
|
2980
|
+
}
|
|
2981
|
+
return;
|
|
2982
|
+
}
|
|
2983
|
+
await sleep(DISPLAY_SETTLE_POLL_MS);
|
|
2984
|
+
}
|
|
2985
|
+
}
|
|
2986
|
+
async #settleInputState(commandName, previousExecutionState) {
|
|
2987
|
+
if (previousExecutionState !== "running") {
|
|
2988
|
+
return;
|
|
2989
|
+
}
|
|
2990
|
+
const deadline = Date.now() + INPUT_SETTLE_TIMEOUT_MS;
|
|
2991
|
+
let runningSince = null;
|
|
2992
|
+
let lastResumeAt = 0;
|
|
2993
|
+
while (true) {
|
|
2994
|
+
this.#syncMonitorRuntimeState();
|
|
2995
|
+
if (this.#executionState === "running") {
|
|
2996
|
+
runningSince ??= Date.now();
|
|
2997
|
+
if (Date.now() - runningSince >= INPUT_RUNNING_STABLE_MS) {
|
|
2998
|
+
return;
|
|
2999
|
+
}
|
|
3000
|
+
} else {
|
|
3001
|
+
runningSince = null;
|
|
3002
|
+
}
|
|
3003
|
+
if (this.#executionState === "stopped" && Date.now() - lastResumeAt >= INPUT_RESUME_COOLDOWN_MS) {
|
|
3004
|
+
this.#writeProcessLogLine(`[input] observed stopped state after ${commandName}, sending resume`);
|
|
3005
|
+
this.#explicitPauseActive = false;
|
|
3006
|
+
this.#lastExecutionIntent = "unknown";
|
|
3007
|
+
await this.#client.continueExecution();
|
|
3008
|
+
lastResumeAt = Date.now();
|
|
3009
|
+
}
|
|
3010
|
+
if (Date.now() >= deadline) {
|
|
3011
|
+
this.#writeProcessLogLine(
|
|
3012
|
+
`[input] settle timeout reached after ${INPUT_SETTLE_TIMEOUT_MS}ms with executionState=${this.#executionState}`
|
|
3013
|
+
);
|
|
3014
|
+
if (this.#executionState !== "running") {
|
|
3015
|
+
emulatorNotRunningError(commandName, {
|
|
3016
|
+
executionState: this.#executionState,
|
|
3017
|
+
lastStopReason: this.#lastStopReason
|
|
3018
|
+
});
|
|
3019
|
+
}
|
|
3020
|
+
return;
|
|
3021
|
+
}
|
|
3022
|
+
await sleep(INPUT_SETTLE_POLL_MS);
|
|
3023
|
+
}
|
|
3024
|
+
}
|
|
3025
|
+
#autostartWasAcceptedAfterError(error, event, previousExecutionState, previousStopReason) {
|
|
3026
|
+
if (!(error instanceof ViceMcpError)) {
|
|
3027
|
+
return false;
|
|
3028
|
+
}
|
|
3029
|
+
if (!["emulator_protocol_error", "timeout", "connection_closed"].includes(error.code)) {
|
|
3030
|
+
return false;
|
|
3031
|
+
}
|
|
3032
|
+
return this.#executionState !== previousExecutionState && (this.#executionState === "running" || this.#executionState === "stopped") && this.#lastStopReason !== previousStopReason && event != null;
|
|
3033
|
+
}
|
|
3034
|
+
#resumeWasAcceptedAfterError(error, event, previousExecutionState, previousStopReason) {
|
|
3035
|
+
if (!(error instanceof ViceMcpError)) {
|
|
3036
|
+
return false;
|
|
3037
|
+
}
|
|
3038
|
+
if (!["emulator_protocol_error", "timeout", "connection_closed"].includes(error.code)) {
|
|
3039
|
+
return false;
|
|
3040
|
+
}
|
|
3041
|
+
if (event) {
|
|
3042
|
+
this.#writeProcessLogLine(
|
|
3043
|
+
`[bootstrap] resume probe accepted after ${error.code} because monitor reported ${event.type}`
|
|
3044
|
+
);
|
|
3045
|
+
return true;
|
|
3046
|
+
}
|
|
3047
|
+
return this.#executionState !== previousExecutionState || this.#lastStopReason !== previousStopReason;
|
|
3048
|
+
}
|
|
3049
|
+
#syncMonitorRuntimeState() {
|
|
3050
|
+
const runtime = this.#client.runtimeState();
|
|
3051
|
+
const eventType = runtime.lastEventType;
|
|
3052
|
+
this.#executionState = runtime.runtimeKnown && eventType === "resumed" ? "running" : runtime.runtimeKnown ? "stopped" : "unknown";
|
|
3053
|
+
if (eventType !== this.#lastRuntimeEventType || runtime.programCounter !== this.#lastRuntimeProgramCounter) {
|
|
3054
|
+
this.#lastStopReason = this.#stopReasonFromMonitorEvent(eventType);
|
|
3055
|
+
this.#lastRuntimeEventType = eventType;
|
|
3056
|
+
this.#lastRuntimeProgramCounter = runtime.programCounter;
|
|
3057
|
+
}
|
|
3058
|
+
this.#writeProcessLogLine(
|
|
3059
|
+
`[monitor-state] executionState=${this.#executionState} lastStopReason=${this.#lastStopReason} runtimeKnown=${runtime.runtimeKnown} lastEventType=${eventType}${runtime.programCounter == null ? "" : ` pc=$${runtime.programCounter.toString(16).padStart(4, "0")}`}`
|
|
3060
|
+
);
|
|
3061
|
+
this.#scheduleIdleAutoResume();
|
|
3062
|
+
}
|
|
3063
|
+
#stopReasonFromMonitorEvent(eventType) {
|
|
3064
|
+
switch (eventType) {
|
|
3065
|
+
case "resumed":
|
|
3066
|
+
this.#pendingCheckpointHit = null;
|
|
3067
|
+
return "none";
|
|
3068
|
+
case "stopped":
|
|
3069
|
+
if (this.#pendingCheckpointHit && Date.now() - this.#pendingCheckpointHit.observedAt <= CHECKPOINT_HIT_SETTLE_MS) {
|
|
3070
|
+
this.#lastCheckpointHit = this.#pendingCheckpointHit;
|
|
3071
|
+
const checkpointReason = this.#pendingCheckpointHit.kind === "exec" ? "breakpoint" : this.#pendingCheckpointHit.kind === "read" ? "watchpoint_read" : "watchpoint_write";
|
|
3072
|
+
this.#pendingCheckpointHit = null;
|
|
3073
|
+
return checkpointReason;
|
|
3074
|
+
}
|
|
3075
|
+
if (this.#lastExecutionIntent === "unknown" && !this.#checkpointQueryPending) {
|
|
3076
|
+
void this.#scheduleCheckpointHitQuery();
|
|
3077
|
+
}
|
|
3078
|
+
return this.#lastExecutionIntent;
|
|
3079
|
+
case "jam":
|
|
3080
|
+
this.#pendingCheckpointHit = null;
|
|
3081
|
+
return "error";
|
|
3082
|
+
case "unknown":
|
|
3083
|
+
this.#pendingCheckpointHit = null;
|
|
3084
|
+
return "unknown";
|
|
3085
|
+
}
|
|
3086
|
+
}
|
|
3087
|
+
async #scheduleCheckpointHitQuery() {
|
|
3088
|
+
if (this.#checkpointQueryPending) {
|
|
3089
|
+
return;
|
|
3090
|
+
}
|
|
3091
|
+
this.#checkpointQueryPending = true;
|
|
3092
|
+
try {
|
|
3093
|
+
await sleep(200);
|
|
3094
|
+
if (this.#lastStopReason !== "unknown" && this.#lastStopReason !== this.#lastExecutionIntent) {
|
|
3095
|
+
return;
|
|
3096
|
+
}
|
|
3097
|
+
this.#writeProcessLogLine("[checkpoint-query] probing for hit checkpoint after ambiguous stop");
|
|
3098
|
+
const response = await this.#client.listBreakpoints();
|
|
3099
|
+
const hit = response.checkpoints.find((cp) => cp.currentlyHit);
|
|
3100
|
+
if (hit) {
|
|
3101
|
+
this.#lastCheckpointHit = {
|
|
3102
|
+
id: hit.id,
|
|
3103
|
+
kind: hit.kind,
|
|
3104
|
+
observedAt: Date.now()
|
|
3105
|
+
};
|
|
3106
|
+
const reason = hit.kind === "exec" ? "breakpoint" : hit.kind === "read" ? "watchpoint_read" : "watchpoint_write";
|
|
3107
|
+
this.#lastStopReason = reason;
|
|
3108
|
+
this.#writeProcessLogLine(`[checkpoint-query] retroactively identified stop reason: ${reason} (id=${hit.id})`);
|
|
3109
|
+
}
|
|
3110
|
+
} catch (error) {
|
|
3111
|
+
this.#writeProcessLogLine(`[checkpoint-query] failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
3112
|
+
} finally {
|
|
3113
|
+
this.#checkpointQueryPending = false;
|
|
3114
|
+
}
|
|
3115
|
+
}
|
|
3116
|
+
#autoResumeAllowed() {
|
|
3117
|
+
return !this.#shuttingDown && this.#client.connected && !this.#explicitPauseActive;
|
|
3118
|
+
}
|
|
3119
|
+
#scheduleIdleAutoResume() {
|
|
3120
|
+
if (this.#executionState !== "stopped" || !this.#autoResumeAllowed()) {
|
|
3121
|
+
this.#clearIdleAutoResume();
|
|
3122
|
+
return;
|
|
3123
|
+
}
|
|
3124
|
+
this.#stoppedAt = Date.now();
|
|
3125
|
+
if (this.#autoResumeTimer) {
|
|
3126
|
+
clearTimeout(this.#autoResumeTimer);
|
|
3127
|
+
}
|
|
3128
|
+
this.#autoResumeTimer = setTimeout(() => {
|
|
3129
|
+
this.#autoResumeTimer = null;
|
|
3130
|
+
void this.#autoResumeDueToIdle();
|
|
3131
|
+
}, STOPPED_IDLE_TIMEOUT_MS);
|
|
3132
|
+
}
|
|
3133
|
+
#clearIdleAutoResume() {
|
|
3134
|
+
this.#stoppedAt = null;
|
|
3135
|
+
if (this.#autoResumeTimer) {
|
|
3136
|
+
clearTimeout(this.#autoResumeTimer);
|
|
3137
|
+
this.#autoResumeTimer = null;
|
|
3138
|
+
}
|
|
3139
|
+
}
|
|
3140
|
+
async #autoResumeDueToIdle() {
|
|
3141
|
+
if (!this.#autoResumeAllowed() || this.#executionState !== "stopped") {
|
|
3142
|
+
this.#stoppedAt = null;
|
|
3143
|
+
return;
|
|
3144
|
+
}
|
|
3145
|
+
const stoppedMs = this.#stoppedAt ? Date.now() - this.#stoppedAt : STOPPED_IDLE_TIMEOUT_MS;
|
|
3146
|
+
if (stoppedMs < STOPPED_IDLE_TIMEOUT_MS) {
|
|
3147
|
+
this.#scheduleIdleAutoResume();
|
|
3148
|
+
return;
|
|
3149
|
+
}
|
|
3150
|
+
this.#writeProcessLogLine(`[auto-resume] emulator stopped for ${STOPPED_IDLE_TIMEOUT_MS}ms, resuming to stay responsive`);
|
|
3151
|
+
try {
|
|
3152
|
+
this.#lastExecutionIntent = "unknown";
|
|
3153
|
+
await this.#client.continueExecution();
|
|
3154
|
+
} catch (error) {
|
|
3155
|
+
this.#writeProcessLogLine(
|
|
3156
|
+
`[auto-resume] resume attempt failed: ${error instanceof Error ? error.message : String(error)}`
|
|
3157
|
+
);
|
|
3158
|
+
} finally {
|
|
3159
|
+
this.#stoppedAt = null;
|
|
3160
|
+
}
|
|
3161
|
+
this.#syncMonitorRuntimeState();
|
|
3162
|
+
}
|
|
3163
|
+
};
|
|
3164
|
+
function splitCommandLine(input) {
|
|
3165
|
+
const result = [];
|
|
3166
|
+
let current = "";
|
|
3167
|
+
let quote = null;
|
|
3168
|
+
for (const char of input) {
|
|
3169
|
+
if (quote) {
|
|
3170
|
+
if (char === quote) {
|
|
3171
|
+
quote = null;
|
|
3172
|
+
} else {
|
|
3173
|
+
current += char;
|
|
3174
|
+
}
|
|
3175
|
+
continue;
|
|
3176
|
+
}
|
|
3177
|
+
if (char === '"' || char === "'") {
|
|
3178
|
+
quote = char;
|
|
3179
|
+
continue;
|
|
3180
|
+
}
|
|
3181
|
+
if (/\s/.test(char)) {
|
|
3182
|
+
if (current) {
|
|
3183
|
+
result.push(current);
|
|
3184
|
+
current = "";
|
|
3185
|
+
}
|
|
3186
|
+
continue;
|
|
3187
|
+
}
|
|
3188
|
+
current += char;
|
|
3189
|
+
}
|
|
3190
|
+
if (current) {
|
|
3191
|
+
result.push(current);
|
|
3192
|
+
}
|
|
3193
|
+
return result;
|
|
3194
|
+
}
|
|
3195
|
+
async function waitForMonitor(host2, port2, timeoutMs) {
|
|
3196
|
+
const deadline = Date.now() + timeoutMs;
|
|
3197
|
+
while (Date.now() < deadline) {
|
|
3198
|
+
if (!await isPortAvailable(host2, port2)) {
|
|
3199
|
+
return;
|
|
3200
|
+
}
|
|
3201
|
+
await sleep(100);
|
|
3202
|
+
}
|
|
3203
|
+
throw new ViceMcpError("monitor_timeout", `Debugger monitor did not open on ${host2}:${port2}`, "timeout", true, {
|
|
3204
|
+
host: host2,
|
|
3205
|
+
port: port2
|
|
3206
|
+
});
|
|
3207
|
+
}
|
|
3208
|
+
async function waitForProcessExit(process2, timeoutMs) {
|
|
3209
|
+
if (process2.exitCode != null) {
|
|
3210
|
+
return;
|
|
3211
|
+
}
|
|
3212
|
+
await new Promise((resolve, reject) => {
|
|
3213
|
+
const timer = setTimeout(() => reject(new Error("Timed out waiting for process exit")), timeoutMs);
|
|
3214
|
+
process2.once("exit", () => {
|
|
3215
|
+
clearTimeout(timer);
|
|
3216
|
+
resolve();
|
|
3217
|
+
});
|
|
3218
|
+
});
|
|
3219
|
+
}
|
|
3220
|
+
|
|
3221
|
+
// src/server.ts
|
|
3222
|
+
var c64Session = new ViceSession();
|
|
3223
|
+
var noInputSchema = import_zod4.z.object({}).strict();
|
|
3224
|
+
function createViceTool(options) {
|
|
3225
|
+
return (0, import_tools.createTool)({
|
|
3226
|
+
id: options.id,
|
|
3227
|
+
description: options.description,
|
|
3228
|
+
...options.inputSchema ? { inputSchema: options.inputSchema } : {},
|
|
3229
|
+
outputSchema: toolOutputSchema(options.dataSchema),
|
|
3230
|
+
mcp: options.mcp,
|
|
3231
|
+
execute: async (input) => {
|
|
3232
|
+
try {
|
|
3233
|
+
const data = await options.execute(input);
|
|
3234
|
+
return {
|
|
3235
|
+
meta: c64Session.takeResponseMeta(),
|
|
3236
|
+
data
|
|
3237
|
+
};
|
|
3238
|
+
} catch (error) {
|
|
3239
|
+
throw normalizeToolError(error);
|
|
3240
|
+
}
|
|
3241
|
+
}
|
|
3242
|
+
});
|
|
3243
|
+
}
|
|
3244
|
+
function normalizeBreakpoint(breakpoint) {
|
|
3245
|
+
return {
|
|
3246
|
+
id: breakpoint.id,
|
|
3247
|
+
address: breakpoint.start,
|
|
3248
|
+
length: breakpoint.end - breakpoint.start + 1,
|
|
3249
|
+
enabled: breakpoint.enabled,
|
|
3250
|
+
temporary: breakpoint.temporary,
|
|
3251
|
+
hasCondition: breakpoint.hasCondition,
|
|
3252
|
+
kind: breakpoint.kind,
|
|
3253
|
+
label: breakpoint.label ?? null
|
|
3254
|
+
};
|
|
3255
|
+
}
|
|
3256
|
+
var getMonitorStateTool = createViceTool({
|
|
3257
|
+
id: "get_monitor_state",
|
|
3258
|
+
description: "Returns whether the C64 is running or stopped, along with the current stop reason and program counter when available.",
|
|
3259
|
+
inputSchema: noInputSchema,
|
|
3260
|
+
dataSchema: monitorStateSchema,
|
|
3261
|
+
execute: async () => await c64Session.getMonitorState()
|
|
3262
|
+
});
|
|
3263
|
+
var getSessionStateTool = createViceTool({
|
|
3264
|
+
id: "get_session_state",
|
|
3265
|
+
description: "Returns richer emulator session state including transport/process status, auto-resume state, and the most recent hit checkpoint when known.",
|
|
3266
|
+
inputSchema: noInputSchema,
|
|
3267
|
+
dataSchema: sessionStateResultSchema,
|
|
3268
|
+
execute: async () => c64Session.snapshot()
|
|
3269
|
+
});
|
|
3270
|
+
var getRegistersTool = createViceTool({
|
|
3271
|
+
id: "get_registers",
|
|
3272
|
+
description: "Returns the current C64 register snapshot. This requires the emulator to already be stopped.",
|
|
3273
|
+
inputSchema: noInputSchema,
|
|
3274
|
+
dataSchema: import_zod4.z.object({
|
|
3275
|
+
registers: c64RegisterValueSchema
|
|
3276
|
+
}),
|
|
3277
|
+
execute: async () => await c64Session.getRegisters()
|
|
3278
|
+
});
|
|
3279
|
+
var setRegistersTool = createViceTool({
|
|
3280
|
+
id: "set_registers",
|
|
3281
|
+
description: "Sets one or more C64 registers by field name.",
|
|
3282
|
+
inputSchema: import_zod4.z.object({
|
|
3283
|
+
registers: c64PartialRegisterValueSchema
|
|
3284
|
+
}),
|
|
3285
|
+
dataSchema: import_zod4.z.object({
|
|
3286
|
+
updated: c64PartialRegisterValueSchema,
|
|
3287
|
+
executionState: executionStateSchema
|
|
3288
|
+
}),
|
|
3289
|
+
execute: async (input) => await c64Session.setRegisters(input.registers)
|
|
3290
|
+
});
|
|
3291
|
+
var readMemoryTool = createViceTool({
|
|
3292
|
+
id: "memory_read",
|
|
3293
|
+
description: "Reads a memory chunk by start address and length and returns raw bytes as a JSON array.",
|
|
3294
|
+
inputSchema: import_zod4.z.object({
|
|
3295
|
+
address: address16Schema.describe("Start address in the 16-bit C64 address space"),
|
|
3296
|
+
length: import_zod4.z.number().int().positive().max(65535).describe("Size of the data chunk to read in bytes")
|
|
3297
|
+
}).refine((input) => input.address + input.length <= 65536, {
|
|
3298
|
+
message: "address + length must stay within the 64K address space",
|
|
3299
|
+
path: ["length"]
|
|
3300
|
+
}),
|
|
3301
|
+
dataSchema: import_zod4.z.object({
|
|
3302
|
+
address: address16Schema.describe("Start address of the returned memory chunk"),
|
|
3303
|
+
length: import_zod4.z.number().int().min(0).describe("Number of bytes returned"),
|
|
3304
|
+
data: byteArraySchema.describe("Raw bytes returned from memory")
|
|
3305
|
+
}),
|
|
3306
|
+
execute: async (input) => {
|
|
3307
|
+
const result = await c64Session.readMemory(input.address, input.address + input.length - 1);
|
|
3308
|
+
return {
|
|
3309
|
+
address: input.address,
|
|
3310
|
+
length: result.length,
|
|
3311
|
+
data: result.data
|
|
3312
|
+
};
|
|
3313
|
+
}
|
|
3314
|
+
});
|
|
3315
|
+
var writeMemoryTool = createViceTool({
|
|
3316
|
+
id: "memory_write",
|
|
3317
|
+
description: "Writes raw byte values into the active C64 memory space.",
|
|
3318
|
+
inputSchema: import_zod4.z.object({
|
|
3319
|
+
address: address16Schema.describe("Start address in the 16-bit C64 address space"),
|
|
3320
|
+
data: byteArraySchema.min(1).describe("Raw bytes to write into memory")
|
|
3321
|
+
}).refine((input) => input.address + input.data.length - 1 <= 65535, {
|
|
3322
|
+
message: "address + data.length must stay within the 16-bit address space",
|
|
3323
|
+
path: ["data"]
|
|
3324
|
+
}),
|
|
3325
|
+
dataSchema: import_zod4.z.object({
|
|
3326
|
+
worked: import_zod4.z.boolean().describe("Whether the write operation completed successfully"),
|
|
3327
|
+
address: address16Schema.describe("Start address where the bytes were written"),
|
|
3328
|
+
length: import_zod4.z.number().int().min(1).describe("Number of bytes written")
|
|
3329
|
+
}).extend(debugStateSchema.shape),
|
|
3330
|
+
execute: async (input) => await c64Session.writeMemory(input.address, input.data)
|
|
3331
|
+
});
|
|
3332
|
+
var executeTool = createViceTool({
|
|
3333
|
+
id: "execute",
|
|
3334
|
+
description: "Controls execution with pause, resume, step, step_over, step_out, or reset. Resume can optionally wait until running becomes stable.",
|
|
3335
|
+
inputSchema: import_zod4.z.object({
|
|
3336
|
+
action: import_zod4.z.enum(["pause", "resume", "step", "step_over", "step_out", "reset"]),
|
|
3337
|
+
count: import_zod4.z.number().int().positive().default(1).describe("Instruction count for step and step_over actions"),
|
|
3338
|
+
resetMode: resetModeSchema.default("soft").describe("Reset mode when action is reset"),
|
|
3339
|
+
waitUntilRunningStable: import_zod4.z.boolean().default(false).describe("When action is resume, wait until running becomes stable before returning")
|
|
3340
|
+
}),
|
|
3341
|
+
dataSchema: debugStateSchema.extend({
|
|
3342
|
+
stepsExecuted: import_zod4.z.number().int().positive().optional(),
|
|
3343
|
+
warnings: import_zod4.z.array(warningSchema)
|
|
3344
|
+
}),
|
|
3345
|
+
execute: async (input) => await c64Session.execute(input.action, input.count, input.resetMode, input.waitUntilRunningStable)
|
|
3346
|
+
});
|
|
3347
|
+
var waitForStateTool = createViceTool({
|
|
3348
|
+
id: "wait_for_state",
|
|
3349
|
+
description: "Waits for the emulator to reach a target execution state and optionally remain there for a stability window.",
|
|
3350
|
+
inputSchema: import_zod4.z.object({
|
|
3351
|
+
executionState: import_zod4.z.enum(["running", "stopped"]).describe("Target execution state to wait for"),
|
|
3352
|
+
timeoutMs: import_zod4.z.number().int().positive().default(5e3).describe("Maximum time to wait before returning"),
|
|
3353
|
+
stableMs: import_zod4.z.number().int().nonnegative().optional().describe("Optional stability window the target state must remain true before returning")
|
|
3354
|
+
}),
|
|
3355
|
+
dataSchema: waitForStateResultSchema,
|
|
3356
|
+
execute: async (input) => await c64Session.waitForState(input.executionState, input.timeoutMs, input.stableMs)
|
|
3357
|
+
});
|
|
3358
|
+
var listBreakpointsTool = createViceTool({
|
|
3359
|
+
id: "list_breakpoints",
|
|
3360
|
+
description: "Lists current breakpoints and watchpoints.",
|
|
3361
|
+
inputSchema: import_zod4.z.object({
|
|
3362
|
+
includeDisabled: import_zod4.z.boolean().default(true)
|
|
3363
|
+
}),
|
|
3364
|
+
dataSchema: import_zod4.z.object({
|
|
3365
|
+
breakpoints: import_zod4.z.array(breakpointSchema)
|
|
3366
|
+
}),
|
|
3367
|
+
execute: async (input) => {
|
|
3368
|
+
const result = await c64Session.listBreakpoints(input.includeDisabled);
|
|
3369
|
+
return {
|
|
3370
|
+
breakpoints: result.breakpoints.map((breakpoint) => normalizeBreakpoint(breakpoint))
|
|
3371
|
+
};
|
|
3372
|
+
}
|
|
3373
|
+
});
|
|
3374
|
+
var breakpointSetTool = createViceTool({
|
|
3375
|
+
id: "breakpoint_set",
|
|
3376
|
+
description: "Creates an execution breakpoint or read/write watchpoint.",
|
|
3377
|
+
inputSchema: import_zod4.z.object({
|
|
3378
|
+
kind: breakpointKindSchema,
|
|
3379
|
+
address: address16Schema.describe("Start address of the breakpoint range"),
|
|
3380
|
+
length: import_zod4.z.number().int().positive().default(1).describe("Size of the breakpoint range in bytes"),
|
|
3381
|
+
condition: import_zod4.z.string().optional(),
|
|
3382
|
+
label: import_zod4.z.string().optional(),
|
|
3383
|
+
temporary: import_zod4.z.boolean().default(false),
|
|
3384
|
+
enabled: import_zod4.z.boolean().default(true)
|
|
3385
|
+
}),
|
|
3386
|
+
dataSchema: import_zod4.z.object({
|
|
3387
|
+
breakpoint: breakpointSchema,
|
|
3388
|
+
executionState: executionStateSchema,
|
|
3389
|
+
lastStopReason: stopReasonSchema,
|
|
3390
|
+
programCounter: address16Schema.nullable(),
|
|
3391
|
+
registers: c64PartialRegisterValueSchema.nullable()
|
|
3392
|
+
}),
|
|
3393
|
+
execute: async (input) => {
|
|
3394
|
+
const result = await c64Session.breakpointSet(input);
|
|
3395
|
+
return {
|
|
3396
|
+
breakpoint: normalizeBreakpoint(result.breakpoint),
|
|
3397
|
+
executionState: result.executionState,
|
|
3398
|
+
lastStopReason: result.lastStopReason,
|
|
3399
|
+
programCounter: result.programCounter,
|
|
3400
|
+
registers: result.registers
|
|
3401
|
+
};
|
|
3402
|
+
}
|
|
3403
|
+
});
|
|
3404
|
+
var breakpointClearTool = createViceTool({
|
|
3405
|
+
id: "breakpoint_clear",
|
|
3406
|
+
description: "Deletes a breakpoint by numeric id.",
|
|
3407
|
+
inputSchema: import_zod4.z.object({
|
|
3408
|
+
breakpointId: import_zod4.z.number().int().nonnegative()
|
|
3409
|
+
}),
|
|
3410
|
+
dataSchema: import_zod4.z.object({
|
|
3411
|
+
executionState: executionStateSchema,
|
|
3412
|
+
lastStopReason: stopReasonSchema,
|
|
3413
|
+
programCounter: address16Schema.nullable(),
|
|
3414
|
+
registers: c64PartialRegisterValueSchema.nullable(),
|
|
3415
|
+
cleared: import_zod4.z.boolean(),
|
|
3416
|
+
breakpointId: import_zod4.z.number().int()
|
|
3417
|
+
}),
|
|
3418
|
+
execute: async (input) => await c64Session.breakpointClear(input.breakpointId)
|
|
3419
|
+
});
|
|
3420
|
+
var programLoadTool = createViceTool({
|
|
3421
|
+
id: "program_load",
|
|
3422
|
+
description: "Loads a C64 program and optionally starts it.",
|
|
3423
|
+
inputSchema: import_zod4.z.object({
|
|
3424
|
+
filePath: import_zod4.z.string(),
|
|
3425
|
+
autoStart: import_zod4.z.boolean().default(true).describe("Whether the loaded program should be started immediately after loading"),
|
|
3426
|
+
fileIndex: import_zod4.z.number().int().nonnegative().default(0).describe("Autostart file index inside the image, when applicable")
|
|
3427
|
+
}),
|
|
3428
|
+
dataSchema: programLoadResultSchema,
|
|
3429
|
+
execute: async (input) => await c64Session.programLoad(input)
|
|
3430
|
+
});
|
|
3431
|
+
var captureDisplayTool = createViceTool({
|
|
3432
|
+
id: "capture_display",
|
|
3433
|
+
description: "Captures the current screen to a PNG file and returns the saved image path. If the emulator was running, the server restores running state before returning, subject to timeout.",
|
|
3434
|
+
inputSchema: import_zod4.z.object({
|
|
3435
|
+
useVic: import_zod4.z.boolean().default(true).describe("Whether to capture the VIC-II display when supported")
|
|
3436
|
+
}),
|
|
3437
|
+
dataSchema: captureDisplayResultSchema,
|
|
3438
|
+
execute: async (input) => await c64Session.captureDisplay(input.useVic)
|
|
3439
|
+
});
|
|
3440
|
+
var getDisplayStateTool = createViceTool({
|
|
3441
|
+
id: "get_display_state",
|
|
3442
|
+
description: "Returns screen RAM, color RAM, the current graphics mode, screen memory addresses, and the current border and background colors. If the emulator was running, the server restores running state before returning, subject to timeout.",
|
|
3443
|
+
inputSchema: noInputSchema,
|
|
3444
|
+
dataSchema: displayStateResultSchema,
|
|
3445
|
+
execute: async () => await c64Session.getDisplayState()
|
|
3446
|
+
});
|
|
3447
|
+
var getDisplayTextTool = createViceTool({
|
|
3448
|
+
id: "get_display_text",
|
|
3449
|
+
description: "Returns the current text screen as readable text when the C64 is in a text mode. If the emulator was running, the server restores running state before returning, subject to timeout.",
|
|
3450
|
+
inputSchema: noInputSchema,
|
|
3451
|
+
dataSchema: displayTextResultSchema,
|
|
3452
|
+
execute: async () => await c64Session.getDisplayText()
|
|
3453
|
+
});
|
|
3454
|
+
var writeTextTool = createViceTool({
|
|
3455
|
+
id: "write_text",
|
|
3456
|
+
description: "Types text into the C64 while it is running. Supports escaped characters and PETSCII brace tokens like {RETURN}, {CLR}, {HOME}, {PI}, and color names; limit each request to 64 bytes.",
|
|
3457
|
+
inputSchema: import_zod4.z.object({
|
|
3458
|
+
text: import_zod4.z.string()
|
|
3459
|
+
}),
|
|
3460
|
+
dataSchema: import_zod4.z.object({
|
|
3461
|
+
sent: import_zod4.z.boolean(),
|
|
3462
|
+
length: import_zod4.z.number().int()
|
|
3463
|
+
}),
|
|
3464
|
+
execute: async (input) => await c64Session.writeText(input.text)
|
|
3465
|
+
});
|
|
3466
|
+
var keyboardInputTool = createViceTool({
|
|
3467
|
+
id: "keyboard_input",
|
|
3468
|
+
description: "Sends one to four keys or PETSCII tokens to the C64 while it is running. Use this for key presses, releases, and taps. If the emulator transiently stops, the server restores running state before returning, subject to timeout.",
|
|
3469
|
+
inputSchema: import_zod4.z.object({
|
|
3470
|
+
action: inputActionSchema.describe("Use tap for a single key event or press/release for repeated buffered input"),
|
|
3471
|
+
keys: import_zod4.z.array(import_zod4.z.string().min(1)).min(1).max(4).describe("One to four literal keys or PETSCII token names such as RETURN, CLR, HOME, PI, LEFT, RED, or F1"),
|
|
3472
|
+
durationMs: import_zod4.z.number().int().positive().optional().describe("Tap duration in milliseconds")
|
|
3473
|
+
}),
|
|
3474
|
+
dataSchema: keyboardInputResultSchema,
|
|
3475
|
+
execute: async (input) => await c64Session.keyboardInput(input.action, input.keys, input.durationMs)
|
|
3476
|
+
});
|
|
3477
|
+
var joystickInputTool = createViceTool({
|
|
3478
|
+
id: "joystick_input",
|
|
3479
|
+
description: "Sends joystick input to C64 joystick port 1 or 2 while the C64 is running. If the emulator transiently stops, the server restores running state before returning, subject to timeout.",
|
|
3480
|
+
inputSchema: import_zod4.z.object({
|
|
3481
|
+
port: joystickPortSchema.describe("Joystick port number"),
|
|
3482
|
+
action: inputActionSchema.describe("Joystick action to apply"),
|
|
3483
|
+
control: joystickControlSchema.describe("Joystick direction or fire control"),
|
|
3484
|
+
durationMs: import_zod4.z.number().int().optional().describe("Tap duration in milliseconds (will be clamped to reasonable range)")
|
|
3485
|
+
}),
|
|
3486
|
+
dataSchema: joystickInputResultSchema,
|
|
3487
|
+
execute: async (input) => await c64Session.joystickInput(input.port, input.action, input.control, input.durationMs)
|
|
3488
|
+
});
|
|
3489
|
+
var c64DebugServer = new import_mcp.MCPServer({
|
|
3490
|
+
id: "c64-debug-mcp",
|
|
3491
|
+
name: "c64 debugger",
|
|
3492
|
+
version: "0.1.0",
|
|
3493
|
+
description: "MCP server for C64 debugging and interaction.",
|
|
3494
|
+
instructions: "This server is for C64 debugging. Use the tools directly to inspect, control, and interact with the C64.",
|
|
3495
|
+
tools: {
|
|
3496
|
+
get_monitor_state: getMonitorStateTool,
|
|
3497
|
+
get_session_state: getSessionStateTool,
|
|
3498
|
+
get_registers: getRegistersTool,
|
|
3499
|
+
set_registers: setRegistersTool,
|
|
3500
|
+
memory_read: readMemoryTool,
|
|
3501
|
+
memory_write: writeMemoryTool,
|
|
3502
|
+
execute: executeTool,
|
|
3503
|
+
wait_for_state: waitForStateTool,
|
|
3504
|
+
list_breakpoints: listBreakpointsTool,
|
|
3505
|
+
breakpoint_set: breakpointSetTool,
|
|
3506
|
+
breakpoint_clear: breakpointClearTool,
|
|
3507
|
+
program_load: programLoadTool,
|
|
3508
|
+
capture_display: captureDisplayTool,
|
|
3509
|
+
get_display_state: getDisplayStateTool,
|
|
3510
|
+
get_display_text: getDisplayTextTool,
|
|
3511
|
+
write_text: writeTextTool,
|
|
3512
|
+
keyboard_input: keyboardInputTool,
|
|
3513
|
+
joystick_input: joystickInputTool
|
|
3514
|
+
}
|
|
3515
|
+
});
|
|
3516
|
+
|
|
3517
|
+
// src/http.ts
|
|
3518
|
+
var host = process.env.C64_DEBUG_HTTP_HOST?.trim() || "127.0.0.1";
|
|
3519
|
+
var port = Number.parseInt(process.env.C64_DEBUG_HTTP_PORT?.trim() || "39080", 10);
|
|
3520
|
+
var mcpPath = process.env.C64_DEBUG_HTTP_PATH?.trim() || "/mcp";
|
|
3521
|
+
var healthPath = process.env.C64_DEBUG_HTTP_HEALTH_PATH?.trim() || "/healthz";
|
|
3522
|
+
var shuttingDown = false;
|
|
3523
|
+
var httpServer = import_node_http.default.createServer(async (req, res) => {
|
|
3524
|
+
const url = new URL(req.url || "/", `http://${host}:${port}`);
|
|
3525
|
+
if (url.pathname === healthPath) {
|
|
3526
|
+
res.writeHead(200, { "content-type": "application/json" });
|
|
3527
|
+
res.end(JSON.stringify({ ok: true }));
|
|
3528
|
+
return;
|
|
3529
|
+
}
|
|
3530
|
+
if (url.pathname !== mcpPath) {
|
|
3531
|
+
res.writeHead(404, { "content-type": "application/json" });
|
|
3532
|
+
res.end(JSON.stringify({ error: "not_found" }));
|
|
3533
|
+
return;
|
|
3534
|
+
}
|
|
3535
|
+
try {
|
|
3536
|
+
await c64DebugServer.startHTTP({
|
|
3537
|
+
url,
|
|
3538
|
+
httpPath: mcpPath,
|
|
3539
|
+
req,
|
|
3540
|
+
res,
|
|
3541
|
+
options: {
|
|
3542
|
+
enableJsonResponse: true
|
|
3543
|
+
}
|
|
3544
|
+
});
|
|
3545
|
+
} catch (error) {
|
|
3546
|
+
if (!res.headersSent) {
|
|
3547
|
+
res.writeHead(500, { "content-type": "application/json" });
|
|
3548
|
+
res.end(
|
|
3549
|
+
JSON.stringify({
|
|
3550
|
+
error: "mcp_http_start_failed",
|
|
3551
|
+
message: error instanceof Error ? error.message : String(error)
|
|
3552
|
+
})
|
|
3553
|
+
);
|
|
3554
|
+
return;
|
|
3555
|
+
}
|
|
3556
|
+
res.end();
|
|
3557
|
+
}
|
|
3558
|
+
});
|
|
3559
|
+
async function closeHttpServer() {
|
|
3560
|
+
await new Promise((resolve, reject) => {
|
|
3561
|
+
httpServer.close((error) => {
|
|
3562
|
+
if (error) {
|
|
3563
|
+
reject(error);
|
|
3564
|
+
return;
|
|
3565
|
+
}
|
|
3566
|
+
resolve();
|
|
3567
|
+
});
|
|
3568
|
+
});
|
|
3569
|
+
}
|
|
3570
|
+
async function shutdown(exitCode = 0, error) {
|
|
3571
|
+
if (shuttingDown) {
|
|
3572
|
+
return;
|
|
3573
|
+
}
|
|
3574
|
+
shuttingDown = true;
|
|
3575
|
+
if (error) {
|
|
3576
|
+
console.error("C64 Debug MCP HTTP server failed:", error);
|
|
3577
|
+
}
|
|
3578
|
+
try {
|
|
3579
|
+
await closeHttpServer().catch(() => void 0);
|
|
3580
|
+
await c64DebugServer.close().catch(() => void 0);
|
|
3581
|
+
await c64Session.shutdown();
|
|
3582
|
+
} catch (shutdownError) {
|
|
3583
|
+
console.error("C64 Debug MCP HTTP shutdown failed:", shutdownError);
|
|
3584
|
+
exitCode = exitCode === 0 ? 1 : exitCode;
|
|
3585
|
+
} finally {
|
|
3586
|
+
process.exit(exitCode);
|
|
3587
|
+
}
|
|
3588
|
+
}
|
|
3589
|
+
process.once("SIGINT", () => {
|
|
3590
|
+
void shutdown(0);
|
|
3591
|
+
});
|
|
3592
|
+
process.once("SIGTERM", () => {
|
|
3593
|
+
void shutdown(0);
|
|
3594
|
+
});
|
|
3595
|
+
process.once("uncaughtException", (error) => {
|
|
3596
|
+
void shutdown(1, error);
|
|
3597
|
+
});
|
|
3598
|
+
process.once("unhandledRejection", (reason) => {
|
|
3599
|
+
void shutdown(1, reason);
|
|
3600
|
+
});
|
|
3601
|
+
httpServer.listen(port, host, () => {
|
|
3602
|
+
console.error(`C64 Debug MCP HTTP listening on http://${host}:${port}${mcpPath}`);
|
|
3603
|
+
});
|