@henols/c64-re-tools 2.0.0-rc.2 → 2.0.0-rc.4
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 +19 -59
- package/package.json +1 -1
- package/skills/c64-assembler/SKILL.md +5 -2
- package/skills/c64-assembler/scripts/assemble.ts +8 -1
- package/skills/c64-assembler/scripts/compare.ts +84 -0
- package/skills/c64-basic/SKILL.md +1 -2
- package/skills/c64-disk/SKILL.md +20 -6
- package/skills/c64-disk/scripts/containers.ts +124 -0
- package/skills/c64-disk/scripts/disk.ts +59 -5
- package/skills/c64-emulator/SKILL.md +11 -4
- package/skills/c64-knowledge/SKILL.md +53 -3
- package/skills/c64-knowledge/scripts/knowledge.ts +258 -29
- package/skills/c64-memory-map/SKILL.md +4 -1
- package/skills/c64-memory-map/scripts/memmap.ts +14 -3
- package/skills/c64-provenance/SKILL.md +37 -9
- package/skills/c64-provenance/scripts/compare.ts +60 -29
- package/skills/c64-provenance/scripts/provenance.ts +248 -19
- package/skills/c64-provenance/scripts/runtime.ts +179 -0
- package/skills/c64-provenance/scripts/separate.ts +132 -0
- package/skills/c64-reverse-engineering/SKILL.md +65 -11
- package/skills/c64-source/SKILL.md +119 -0
- package/skills/c64-source/scripts/generate.ts +402 -0
- package/skills/c64-source/scripts/loader.ts +320 -0
- package/skills/c64-source/scripts/opcodes.ts +61 -0
- package/skills/c64-source/scripts/source.ts +558 -0
- package/skills/c64-static-analysis/SKILL.md +27 -4
- package/skills/c64-static-analysis/scripts/analyze.ts +102 -13
- package/skills/c64-static-analysis/scripts/coverage.ts +78 -0
- package/skills/c64-static-analysis/scripts/findings.ts +6 -4
- package/skills/c64-static-analysis/scripts/idioms.ts +144 -0
- package/skills/c64-static-analysis/scripts/instructions.ts +105 -0
- package/skills/c64-testing/SKILL.md +27 -3
- package/skills/c64-testing/references/scenario-format.md +43 -3
- package/skills/c64-testing/scripts/run.ts +189 -0
- package/skills/c64-testing/scripts/scenario.ts +153 -135
- package/skills/c64-testing/scripts/test.ts +16 -6
- package/skills/c64-unpacker/SKILL.md +65 -14
- package/skills/c64-unpacker/scripts/evidence.ts +86 -21
- package/skills/c64-unpacker/scripts/loads.ts +174 -0
- package/skills/c64-unpacker/scripts/machine.ts +68 -0
- package/skills/c64-unpacker/scripts/unpack.ts +302 -48
- package/src/c64.ts +91 -0
- package/src/cli/status.ts +3 -1
- package/src/host/main.ts +10 -25
- package/src/host/server.ts +8 -40
- package/src/host/vice/adapter.ts +25 -10
- package/src/host/vice/session.ts +64 -20
- package/src/host-client/autostart.ts +45 -0
- package/src/host-client/connect.ts +4 -5
- package/src/host-client/coverage.ts +59 -0
- package/src/host-client/steps.ts +170 -0
- package/src/host-client/transfer.ts +29 -3
- package/src/host-client/vice-session.ts +42 -13
- package/src/knowledge/history.ts +40 -13
- package/src/knowledge/import.ts +72 -56
- package/src/knowledge/read.ts +203 -52
- package/src/knowledge/schema.ts +94 -12
- package/src/knowledge/write.ts +432 -123
- package/src/mcp/main.ts +1 -4
- package/src/mcp/output.ts +32 -0
- package/src/mcp/server.ts +4 -2
- package/src/mcp/tools/debug.ts +110 -24
- package/src/mcp/tools/input.ts +1 -0
- package/src/mcp/tools/media.ts +22 -6
- package/src/mcp/tools/memory.ts +57 -10
- package/src/mcp/tools/video.ts +9 -3
- package/src/native/discover.ts +5 -2
- package/src/native/ghidra/index.ts +19 -3
- package/src/native/ghidra/scripts/C64Export.java +33 -1
- package/src/native/status.ts +15 -3
- package/src/native/types.ts +30 -1
- package/{skills/c64-memory-map/scripts → src}/platform.ts +18 -2
- package/src/protocol/messages.ts +17 -10
- package/src/protocol/vice.ts +72 -18
- package/src/script.ts +3 -2
package/src/host/server.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import { createHash, timingSafeEqual } from "node:crypto";
|
|
2
1
|
import { createServer, type Server, type Socket } from "node:net";
|
|
3
2
|
|
|
4
3
|
import { encodeFrame, MessageReader } from "../protocol/framing.ts";
|
|
@@ -12,9 +11,10 @@ export interface ViceSessionHandle {
|
|
|
12
11
|
/**
|
|
13
12
|
* Runs one operation. The session serializes its own work. Throws
|
|
14
13
|
* WireFailure for domain failures. `attachments` holds the bytes the
|
|
15
|
-
* operation takes (see attachmentCount)
|
|
14
|
+
* operation takes (see attachmentCount); bytes the operation sends back go
|
|
15
|
+
* into `reply`.
|
|
16
16
|
*/
|
|
17
|
-
handle<O extends ViceOperation>(op: O, params: ViceOperations[O]["params"], attachments?: Buffer[]): Promise<ViceOperations[O]["result"]>;
|
|
17
|
+
handle<O extends ViceOperation>(op: O, params: ViceOperations[O]["params"], attachments?: Buffer[], reply?: Buffer[]): Promise<ViceOperations[O]["result"]>;
|
|
18
18
|
/** Stops the emulator and drops queued work. Safe to call more than once. */
|
|
19
19
|
close(): Promise<void>;
|
|
20
20
|
}
|
|
@@ -37,16 +37,8 @@ export interface HostServerOptions {
|
|
|
37
37
|
createViceSession: ViceSessionFactory;
|
|
38
38
|
/** Native-tool requests on "tool" connections; without it they are refused. */
|
|
39
39
|
tools?: ToolDispatcher;
|
|
40
|
-
/** Defaults to 127.0.0.1.
|
|
40
|
+
/** Defaults to 127.0.0.1; 0.0.0.0 listens on every address, so a container reaches it too. */
|
|
41
41
|
host?: string;
|
|
42
|
-
/** Extra addresses to listen on, on the same port as `host`. */
|
|
43
|
-
extraHosts?: string[];
|
|
44
|
-
/**
|
|
45
|
-
* The shared secret. A connection that arrives on an address that is
|
|
46
|
-
* not loopback must send it in its hello; without a token such a
|
|
47
|
-
* connection is refused.
|
|
48
|
-
*/
|
|
49
|
-
token?: string;
|
|
50
42
|
/** 0 picks a free port. */
|
|
51
43
|
port: number;
|
|
52
44
|
/** How long a new connection may stay silent before it must send hello. */
|
|
@@ -67,10 +59,6 @@ const DEFAULT_HANDSHAKE_TIMEOUT_MS = 10_000;
|
|
|
67
59
|
/** How long a refused client may keep its side of the connection open after the host has ended its side. */
|
|
68
60
|
const REFUSED_CLOSE_MS = 1_000;
|
|
69
61
|
|
|
70
|
-
function digest(text: string): Buffer {
|
|
71
|
-
return createHash("sha256").update(text, "utf8").digest();
|
|
72
|
-
}
|
|
73
|
-
|
|
74
62
|
/** A caught value for the log, with its stack when it has one. */
|
|
75
63
|
function describe(error: unknown): string {
|
|
76
64
|
return error instanceof Error ? (error.stack ?? error.message) : String(error);
|
|
@@ -95,18 +83,6 @@ class Connection {
|
|
|
95
83
|
#isTool = false;
|
|
96
84
|
readonly #reader = new MessageReader();
|
|
97
85
|
|
|
98
|
-
/** A connection on a loopback address needs no token; any other needs the host's token. */
|
|
99
|
-
#checkToken(token: string | undefined): WireError | undefined {
|
|
100
|
-
if (isLoopback(this.#socket.localAddress ?? "")) return undefined;
|
|
101
|
-
const expected = this.#options.token;
|
|
102
|
-
// Equal-length digests: the comparison takes the same time for any token, also one with multibyte characters.
|
|
103
|
-
const matches = expected !== undefined && token !== undefined && timingSafeEqual(digest(token), digest(expected));
|
|
104
|
-
if (matches) return undefined;
|
|
105
|
-
return {
|
|
106
|
-
code: "installation-incomplete",
|
|
107
|
-
message: "The c64-re-tools host runtime refused this client: a connection from outside the host needs C64RT_HOST_TOKEN with the same value on both sides.",
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
86
|
readonly #socket: Socket;
|
|
111
87
|
readonly #options: HostServerOptions;
|
|
112
88
|
readonly #log: (line: string) => void;
|
|
@@ -191,7 +167,7 @@ class Connection {
|
|
|
191
167
|
if (this.#state === "handshake") {
|
|
192
168
|
if (message.type !== "hello") throw new ProtocolError("first message is not hello");
|
|
193
169
|
clearTimeout(this.#handshakeTimer);
|
|
194
|
-
const mismatch = checkHello(message)
|
|
170
|
+
const mismatch = checkHello(message);
|
|
195
171
|
if (mismatch !== undefined) {
|
|
196
172
|
this.#log(`refusing a client: ${mismatch.message}`);
|
|
197
173
|
trace().event("connection.refused", { connection: this.#id, role: message.role, ...mismatch });
|
|
@@ -270,10 +246,10 @@ class Connection {
|
|
|
270
246
|
const session = this.#session;
|
|
271
247
|
if (session === undefined || !isOneOf(VICE_OPERATIONS, op)) throw new WireFailure("invalid-input", `unknown operation: ${op}`);
|
|
272
248
|
const params = validateViceParams(op, request.params);
|
|
273
|
-
if (attachments.length !== attachmentCount(op)) {
|
|
274
|
-
throw new WireFailure("invalid-input", `${op} takes ${attachmentCount(op)} file attachment(s)`);
|
|
249
|
+
if (attachments.length !== attachmentCount(op, params)) {
|
|
250
|
+
throw new WireFailure("invalid-input", `${op} takes ${attachmentCount(op, params)} file attachment(s)`);
|
|
275
251
|
}
|
|
276
|
-
result = await session.handle(op, params, attachments);
|
|
252
|
+
result = await session.handle(op, params, attachments, replyAttachments);
|
|
277
253
|
}
|
|
278
254
|
this.#send({ type: "reply", id: request.id, result }, replyAttachments);
|
|
279
255
|
record({ ok: true, result });
|
|
@@ -308,12 +284,6 @@ class Connection {
|
|
|
308
284
|
}
|
|
309
285
|
}
|
|
310
286
|
|
|
311
|
-
/** True for 127.0.0.0/8 and ::1, also in their IPv4-mapped IPv6 form. */
|
|
312
|
-
export function isLoopback(address: string): boolean {
|
|
313
|
-
const plain = address.startsWith("::ffff:") ? address.slice(7) : address;
|
|
314
|
-
return plain === "::1" || /^127\.\d+\.\d+\.\d+$/.test(plain);
|
|
315
|
-
}
|
|
316
|
-
|
|
317
287
|
/** A port as the user gives it: decimal digits only, 0 to 65535. Anything else gives undefined. */
|
|
318
288
|
export function parsePort(text: string): number | undefined {
|
|
319
289
|
if (!/^\d{1,5}$/.test(text)) return undefined;
|
|
@@ -386,8 +356,6 @@ export async function startHostServer(options: HostServerOptions): Promise<HostS
|
|
|
386
356
|
let port: number;
|
|
387
357
|
try {
|
|
388
358
|
port = await listen(host, options.port);
|
|
389
|
-
// The extra addresses share the port, so one endpoint setting reaches every address.
|
|
390
|
-
for (const extra of options.extraHosts ?? []) await listen(extra, port);
|
|
391
359
|
} catch (error) {
|
|
392
360
|
await Promise.all(servers.map((server) => new Promise<void>((resolve) => server.close(() => resolve()))));
|
|
393
361
|
throw error;
|
package/src/host/vice/adapter.ts
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
|
|
4
4
|
import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
5
5
|
import { type Comparison, type Condition, type BacktraceFrame, type HistoryEntry, type Instruction, type MemmapRange, type ProfileEntry, type JoystickState, type MemoryView, type RegisterValues, type Registers, type Space } from "../../protocol/vice.ts";
|
|
6
|
-
import { formatC64Address, formatC64Byte } from "../../c64.ts";
|
|
6
|
+
import { COVERAGE, formatC64Address, formatC64Byte } from "../../c64.ts";
|
|
7
7
|
import { decodeDisplay, decodePalette, visibleFrame, type IndexedFrame } from "./screen.ts";
|
|
8
8
|
import type { TextMonitor } from "./text-monitor.ts";
|
|
9
9
|
import {
|
|
@@ -258,12 +258,27 @@ function unknownForm(what: string, lines: string[]): WireFailure {
|
|
|
258
258
|
return new WireFailure("operation-failed", `VICE printed the ${what} in a form that c64-re-tools does not read: ${JSON.stringify(lines.slice(0, 3))}`);
|
|
259
259
|
}
|
|
260
260
|
|
|
261
|
+
/** The coverage byte (D40) of one memmap row's I/O, ROM and RAM columns, each "rwx" with "-" for no access. */
|
|
262
|
+
export function memmapFlags(io: string, rom: string, ram: string): number {
|
|
263
|
+
let flags = 0;
|
|
264
|
+
if (ram[0] === "r") flags |= COVERAGE.ramRead;
|
|
265
|
+
if (ram[1] === "w") flags |= COVERAGE.ramWrite;
|
|
266
|
+
if (ram[2] === "x") flags |= COVERAGE.ramExecute;
|
|
267
|
+
if (rom[0] === "r") flags |= COVERAGE.romRead;
|
|
268
|
+
if (rom[2] === "x") flags |= COVERAGE.romExecute;
|
|
269
|
+
if (io[0] === "r") flags |= COVERAGE.ioRead;
|
|
270
|
+
if (io[1] === "w") flags |= COVERAGE.ioWrite;
|
|
271
|
+
if (io[2] === "x") flags |= COVERAGE.ioExecute;
|
|
272
|
+
return flags;
|
|
273
|
+
}
|
|
274
|
+
|
|
261
275
|
/**
|
|
262
276
|
* Parses memmap rows such as "00c2: --- --- rw- (uninitialized read)": the I/O,
|
|
263
277
|
* ROM and RAM access flags of one address. Adjacent addresses with the same
|
|
264
|
-
*
|
|
278
|
+
* flags merge into one range; at most `maxRanges` come back. When more ranges
|
|
279
|
+
* follow, `next` is the first touched address that is not reported.
|
|
265
280
|
*/
|
|
266
|
-
export function parseMemmap(answer: string, maxRanges: number): MemmapRange[] {
|
|
281
|
+
export function parseMemmap(answer: string, maxRanges: number): { ranges: MemmapRange[]; next?: number } {
|
|
267
282
|
const ranges: MemmapRange[] = [];
|
|
268
283
|
const unknown: string[] = [];
|
|
269
284
|
for (const line of answer.split("\n")) {
|
|
@@ -273,18 +288,18 @@ export function parseMemmap(answer: string, maxRanges: number): MemmapRange[] {
|
|
|
273
288
|
continue;
|
|
274
289
|
}
|
|
275
290
|
const address = Number.parseInt(match[1]!, 16);
|
|
276
|
-
const flags = match[2]
|
|
277
|
-
|
|
291
|
+
const flags = memmapFlags(match[2]!, match[3]!, match[4]!);
|
|
292
|
+
if (flags === 0) continue;
|
|
278
293
|
const last = ranges.at(-1);
|
|
279
|
-
if (last !== undefined && last.end === address - 1 && last.
|
|
294
|
+
if (last !== undefined && last.end === address - 1 && last.flags === flags) {
|
|
280
295
|
last.end = address;
|
|
281
296
|
continue;
|
|
282
297
|
}
|
|
283
|
-
if (ranges.length >= maxRanges)
|
|
284
|
-
ranges.push({ start: address, end: address,
|
|
298
|
+
if (ranges.length >= maxRanges) return { ranges, next: address };
|
|
299
|
+
ranges.push({ start: address, end: address, flags });
|
|
285
300
|
}
|
|
286
301
|
if (ranges.length === 0 && unknown.length > 0) throw unknownForm("memory map", unknown);
|
|
287
|
-
return ranges;
|
|
302
|
+
return { ranges };
|
|
288
303
|
}
|
|
289
304
|
|
|
290
305
|
/**
|
|
@@ -739,7 +754,7 @@ export class ViceAdapter {
|
|
|
739
754
|
}
|
|
740
755
|
|
|
741
756
|
/** How the C64 CPU accessed [start, end] since the memmap was last cleared, as merged ranges. */
|
|
742
|
-
async memmap(start: number, end: number, maxRanges: number): Promise<MemmapRange[]> {
|
|
757
|
+
async memmap(start: number, end: number, maxRanges: number): Promise<{ ranges: MemmapRange[]; next?: number }> {
|
|
743
758
|
// The first argument is the access mask ("ioRWXrwx"); ff shows every kind of access.
|
|
744
759
|
return parseMemmap(await this.#computerText(`mmsh ff ${hex4(start)} ${hex4(end)}`, 60_000), maxRanges);
|
|
745
760
|
}
|
package/src/host/vice/session.ts
CHANGED
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
// and back; breakpoints, watchpoints and the rest go along.
|
|
5
5
|
|
|
6
6
|
import { WireFailure, type VideoStandard } from "../../protocol/messages.ts";
|
|
7
|
-
import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
|
|
8
|
-
import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
7
|
+
import { MAX_BASELINES, MAX_SNAPSHOTS, MAX_COMPARE_DIFFERENCES, RASTER, type Breakpoint, type BreakpointParams, type PointRemoved, type Condition, type ExecutionParams, type HistoryEntry, type RunTarget, type RunUntilResult, type ScreenComparison, type CiaSelection, type CiaState, type ObserveParams, type ObserveResult, type SidState, type SpriteState, type ViciiState, type JoystickState, type Watchpoint, type WatchpointParams, type ExecutionResult, type MachineStatus, type RunState, type ViceOperation, type ViceOperations, type WindowResult } from "../../protocol/vice.ts";
|
|
8
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
9
9
|
import { tmpdir } from "node:os";
|
|
10
10
|
import { join } from "node:path";
|
|
11
11
|
import { setTimeout as sleep } from "node:timers/promises";
|
|
@@ -283,9 +283,10 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
283
283
|
op: O,
|
|
284
284
|
params: ViceOperations[O]["params"],
|
|
285
285
|
attachments: Buffer[] = [],
|
|
286
|
+
reply: Buffer[] = [],
|
|
286
287
|
): Promise<ViceOperations[O]["result"]> {
|
|
287
288
|
// A window request for the mode the session is in sends nothing to VICE, so a held VICE does not stop it.
|
|
288
|
-
return this.#enqueue(() => this.#run(op, params, attachments), { whileHeld: op === "window" }) as Promise<ViceOperations[O]["result"]>;
|
|
289
|
+
return this.#enqueue(() => this.#run(op, params, attachments, reply), { whileHeld: op === "window" }) as Promise<ViceOperations[O]["result"]>;
|
|
289
290
|
}
|
|
290
291
|
|
|
291
292
|
async close(): Promise<void> {
|
|
@@ -303,10 +304,15 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
303
304
|
return this.#adapter;
|
|
304
305
|
}
|
|
305
306
|
|
|
306
|
-
async #run(op: ViceOperation, params: unknown, attachments: Buffer[]): Promise<unknown> {
|
|
307
|
+
async #run(op: ViceOperation, params: unknown, attachments: Buffer[], reply: Buffer[]): Promise<unknown> {
|
|
307
308
|
switch (op) {
|
|
308
309
|
case "status":
|
|
309
310
|
return this.#status();
|
|
311
|
+
case "ioRead": {
|
|
312
|
+
const read = params as ViceOperations["ioRead"]["params"];
|
|
313
|
+
const bytes = await this.#observe(() => this.#machine.readIo(read.address, read.size));
|
|
314
|
+
return { address: read.address, data: Buffer.from(bytes).toString("hex") };
|
|
315
|
+
}
|
|
310
316
|
case "memoryRead": {
|
|
311
317
|
const read = params as ViceOperations["memoryRead"]["params"];
|
|
312
318
|
const data = await this.#observe(() => this.#machine.readMemory(read));
|
|
@@ -394,6 +400,8 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
394
400
|
}
|
|
395
401
|
case "snapshot":
|
|
396
402
|
return this.#snapshot(params as ViceOperations["snapshot"]["params"]);
|
|
403
|
+
case "snapshotFile":
|
|
404
|
+
return this.#snapshotFile(params as ViceOperations["snapshotFile"]["params"], attachments, reply);
|
|
397
405
|
case "cpuHistory":
|
|
398
406
|
return this.#cpuHistory(params as ViceOperations["cpuHistory"]["params"]);
|
|
399
407
|
case "backtrace": {
|
|
@@ -422,7 +430,7 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
422
430
|
await this.#observe(() => this.#machine.clearMemmap());
|
|
423
431
|
return { cleared: true };
|
|
424
432
|
}
|
|
425
|
-
return
|
|
433
|
+
return await this.#observe(() => this.#machine.memmap(request.start, request.end, request.maxRanges));
|
|
426
434
|
}
|
|
427
435
|
case "vicii":
|
|
428
436
|
return this.#observe(() => this.#vicii());
|
|
@@ -716,34 +724,44 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
716
724
|
async #changePoint<T extends { checkpoint: number; enabled: boolean }>(
|
|
717
725
|
registry: Map<number, T>,
|
|
718
726
|
kind: string,
|
|
719
|
-
params: { action: "
|
|
727
|
+
params: { action: "enable" | "disable"; id: number },
|
|
720
728
|
): Promise<T> {
|
|
721
|
-
const point = registry
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
registry.delete(params.id);
|
|
726
|
-
} else {
|
|
727
|
-
const enabled = params.action === "enable";
|
|
728
|
-
await this.#observe(() => this.#machine.toggleCheckpoint(point.checkpoint, enabled));
|
|
729
|
-
point.enabled = enabled;
|
|
730
|
-
}
|
|
729
|
+
const point = this.#point(registry, kind, params.id);
|
|
730
|
+
const enabled = params.action === "enable";
|
|
731
|
+
await this.#observe(() => this.#machine.toggleCheckpoint(point.checkpoint, enabled));
|
|
732
|
+
point.enabled = enabled;
|
|
731
733
|
return point;
|
|
732
734
|
}
|
|
733
735
|
|
|
734
|
-
async #
|
|
736
|
+
async #removePoint(registry: Map<number, { checkpoint: number }>, kind: string, id: number): Promise<PointRemoved> {
|
|
737
|
+
const point = this.#point(registry, kind, id);
|
|
738
|
+
await this.#observe(() => this.#machine.deleteCheckpoint(point.checkpoint));
|
|
739
|
+
registry.delete(id);
|
|
740
|
+
return { removed: true, id };
|
|
741
|
+
}
|
|
742
|
+
|
|
743
|
+
#point<T>(registry: Map<number, T>, kind: string, id: number): T {
|
|
744
|
+
const point = registry.get(id);
|
|
745
|
+
if (point === undefined) throw new WireFailure("not-found", `There is no ${kind} with id ${id}.`);
|
|
746
|
+
return point;
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
async #breakpoint(params: BreakpointParams): Promise<Breakpoint | PointRemoved | { breakpoints: Breakpoint[] }> {
|
|
735
750
|
const publicView = ({ checkpoint: _checkpoint, expression: _expression, ...point }: Breakpoint & { checkpoint: number; expression?: string }): Breakpoint =>
|
|
736
751
|
point;
|
|
737
752
|
if (params.action === "list") return { breakpoints: [...this.#breakpoints.values()].map(publicView) };
|
|
753
|
+
if (params.action === "remove") return this.#removePoint(this.#breakpoints, "breakpoint", params.id);
|
|
738
754
|
if (params.action !== "add") return publicView(await this.#changePoint(this.#breakpoints, "breakpoint", params));
|
|
755
|
+
// A breakpoint over a range stops at the first instruction executed in it: exec, not a watch (D43).
|
|
739
756
|
const added = await this.#addPoint({
|
|
740
|
-
...pointRange({ address: params.address }),
|
|
757
|
+
...(params.size === undefined ? pointRange({ address: params.address }) : { start: params.address, end: params.address + params.size - 1, operation: 0x04 }),
|
|
741
758
|
space: params.space,
|
|
742
759
|
...(params.condition === undefined ? {} : { condition: params.condition }),
|
|
743
760
|
});
|
|
744
761
|
const point = {
|
|
745
762
|
id: this.#nextPointId++,
|
|
746
763
|
address: params.address,
|
|
764
|
+
...(params.size === undefined ? {} : { size: params.size }),
|
|
747
765
|
space: params.space,
|
|
748
766
|
enabled: true,
|
|
749
767
|
...(params.condition === undefined ? {} : { condition: params.condition }),
|
|
@@ -753,10 +771,11 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
753
771
|
return publicView(point);
|
|
754
772
|
}
|
|
755
773
|
|
|
756
|
-
async #watchpoint(params: WatchpointParams): Promise<Watchpoint | { watchpoints: Watchpoint[] }> {
|
|
774
|
+
async #watchpoint(params: WatchpointParams): Promise<Watchpoint | PointRemoved | { watchpoints: Watchpoint[] }> {
|
|
757
775
|
const publicView = ({ checkpoint: _checkpoint, expression: _expression, ...point }: Watchpoint & { checkpoint: number; expression?: string }): Watchpoint =>
|
|
758
776
|
point;
|
|
759
777
|
if (params.action === "list") return { watchpoints: [...this.#watchpoints.values()].map(publicView) };
|
|
778
|
+
if (params.action === "remove") return this.#removePoint(this.#watchpoints, "watchpoint", params.id);
|
|
760
779
|
if (params.action !== "add") return publicView(await this.#changePoint(this.#watchpoints, "watchpoint", params));
|
|
761
780
|
const added = await this.#addPoint({
|
|
762
781
|
...pointRange({ address: params.address, size: params.size, access: params.access }),
|
|
@@ -910,15 +929,40 @@ export class ViceSession implements ViceSessionHandle {
|
|
|
910
929
|
this.#snapshots.delete(params.name);
|
|
911
930
|
return { discarded: true };
|
|
912
931
|
}
|
|
932
|
+
return this.#restoreFrom(existing);
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** Restores the machine from a .vsf in the session directory; the CPU stays stopped. */
|
|
936
|
+
async #restoreFrom(file: string): Promise<{ restored: true; state: "stopped" }> {
|
|
913
937
|
await this.#stop();
|
|
914
938
|
const elapsed = (await this.#machine.clock()) - this.#timingStart;
|
|
915
|
-
await this.#machine.restoreSnapshot(
|
|
939
|
+
await this.#machine.restoreSnapshot(file);
|
|
916
940
|
this.#state = "stopped";
|
|
917
941
|
// The cycle clock jumped with the restore; the stopwatch keeps its reading, as a window move does.
|
|
918
942
|
this.#timingStart = (await this.#machine.clock()) - elapsed;
|
|
919
943
|
return { restored: true, state: "stopped" };
|
|
920
944
|
}
|
|
921
945
|
|
|
946
|
+
/** A snapshot as bytes (D41): export a named one into `reply`, or restore from the attached .vsf. */
|
|
947
|
+
async #snapshotFile(params: ViceOperations["snapshotFile"]["params"], attachments: Buffer[], reply: Buffer[]): Promise<ViceOperations["snapshotFile"]["result"]> {
|
|
948
|
+
if (params.action === "export") {
|
|
949
|
+
const file = this.#snapshots.get(params.name);
|
|
950
|
+
if (file === undefined) throw new WireFailure("not-found", `There is no snapshot named ${params.name}.`);
|
|
951
|
+
const bytes = readFileSync(file);
|
|
952
|
+
reply.push(bytes);
|
|
953
|
+
return { exported: true, bytes: bytes.length };
|
|
954
|
+
}
|
|
955
|
+
const file = this.#stage(onlyAttachment(attachments), "vsf");
|
|
956
|
+
try {
|
|
957
|
+
return await this.#restoreFrom(file);
|
|
958
|
+
} catch (error) {
|
|
959
|
+
if (error instanceof WireFailure) throw error;
|
|
960
|
+
throw new WireFailure("invalid-input", "The emulator did not restore this snapshot file. A snapshot file restores only in the emulator version and with the machine settings that saved it.");
|
|
961
|
+
} finally {
|
|
962
|
+
rmSync(file, { force: true });
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
922
966
|
/**
|
|
923
967
|
* Moves the machine into a VICE of the other mode: a snapshot of this
|
|
924
968
|
* one, restored in a new one, with the user points, joysticks, warp and the
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// Autostart without the autostart's own RUN: the caller decides when the
|
|
2
|
+
// program starts, so keys it types later never mix with VICE's typing.
|
|
3
|
+
|
|
4
|
+
import { textToPetscii } from "../c64.ts";
|
|
5
|
+
import type { ViceSessionClient } from "./vice-session.ts";
|
|
6
|
+
|
|
7
|
+
/** How often, and for how long, a load is checked. */
|
|
8
|
+
export const LOAD_POLL_FRAMES = 25;
|
|
9
|
+
export const LOAD_LIMIT_FRAMES = 1500;
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Autostarts the PRG at `path` with run false and advances frames until its
|
|
13
|
+
* first bytes are at its load address. Returns false when they do not arrive
|
|
14
|
+
* within LOAD_LIMIT_FRAMES.
|
|
15
|
+
*/
|
|
16
|
+
export async function autostartLoaded(client: ViceSessionClient, path: string, prg: Uint8Array): Promise<boolean> {
|
|
17
|
+
const load = prg[0]! | (prg[1]! << 8);
|
|
18
|
+
const head = Buffer.from(prg.subarray(2, Math.min(prg.length, 2 + 64))).toString("hex");
|
|
19
|
+
await client.autostart({ path, index: 0, run: false });
|
|
20
|
+
for (let frames = 0; frames < LOAD_LIMIT_FRAMES; frames += LOAD_POLL_FRAMES) {
|
|
21
|
+
await client.execution({ action: "advance-frames", count: LOAD_POLL_FRAMES, space: "c64" });
|
|
22
|
+
if ((await client.memoryRead({ address: load, size: head.length / 2, space: "c64", view: "ram" })).data === head) return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** CURLIN high byte: $ff while BASIC waits in direct mode, a line number's high byte while a program runs. */
|
|
28
|
+
const CURLIN_HIGH = 0x003a;
|
|
29
|
+
export const RUN_LIMIT_FRAMES = 300;
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Autostart that returns only when the program runs: load without VICE's
|
|
33
|
+
* own RUN, type RUN, and wait until BASIC runs a program line. A key that a
|
|
34
|
+
* later step types then reaches the program, never the READY prompt. Returns
|
|
35
|
+
* why it did not start, or undefined when it runs.
|
|
36
|
+
*/
|
|
37
|
+
export async function runProgram(client: ViceSessionClient, path: string, prg: Uint8Array): Promise<"not-loaded" | "not-started" | undefined> {
|
|
38
|
+
if (!(await autostartLoaded(client, path, prg))) return "not-loaded";
|
|
39
|
+
await client.keyboard(textToPetscii("RUN\n"));
|
|
40
|
+
const started = await client.runUntil({
|
|
41
|
+
target: { kind: "memory", address: CURLIN_HIGH, operator: "ne", value: 0xff, space: "c64", view: "ram" },
|
|
42
|
+
timeoutFrames: RUN_LIMIT_FRAMES,
|
|
43
|
+
});
|
|
44
|
+
return started.reached ? undefined : "not-started";
|
|
45
|
+
}
|
|
@@ -4,7 +4,7 @@ import { connect as connectTcp, type Socket } from "node:net";
|
|
|
4
4
|
|
|
5
5
|
import { encodeFrame, MessageReader } from "../protocol/framing.ts";
|
|
6
6
|
import { elapsedMs, trace } from "../trace.ts";
|
|
7
|
-
import { DEFAULT_HOST_PORT, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, HOST_PROTOCOL_ID, HOST_PROTOCOL_VERSION, parseHostMessage, ProtocolError, WireFailure, type ErrorCode, type Hello, type HostMessage, type Ping, type Request, type Role, type VideoStandard } from "../protocol/messages.ts";
|
|
7
|
+
import { DEFAULT_HOST_PORT, HEARTBEAT_INTERVAL_MS, HEARTBEAT_TIMEOUT_MS, HOST_PROTOCOL_ID, HOST_PROTOCOL_VERSION, parseHostMessage, ProtocolError, WireFailure, withStopAndAsk, type ErrorCode, type Hello, type HostMessage, type Ping, type Request, type Role, type VideoStandard } from "../protocol/messages.ts";
|
|
8
8
|
|
|
9
9
|
export interface Endpoint {
|
|
10
10
|
host: string;
|
|
@@ -40,7 +40,9 @@ function unreachable(role: Role): WireFailure {
|
|
|
40
40
|
const code: ErrorCode = role === "vice-session" ? "machine-unavailable" : "operation-failed";
|
|
41
41
|
return new WireFailure(
|
|
42
42
|
code,
|
|
43
|
-
|
|
43
|
+
withStopAndAsk(
|
|
44
|
+
"The c64-re-tools host runtime is not running or cannot be reached. Start it on the host (npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host) and try again.",
|
|
45
|
+
),
|
|
44
46
|
);
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -150,9 +152,6 @@ export class HostConnection {
|
|
|
150
152
|
const connection = new HostConnection(socket);
|
|
151
153
|
const hello: Hello = { type: "hello", protocol: HOST_PROTOCOL_ID, version: HOST_PROTOCOL_VERSION, role: options.role };
|
|
152
154
|
if (options.videoStandard !== undefined) hello.videoStandard = options.videoStandard;
|
|
153
|
-
// A host that listens beyond loopback checks this shared secret.
|
|
154
|
-
const token = (options.env ?? process.env).C64RT_HOST_TOKEN;
|
|
155
|
-
if (token !== undefined && token !== "") hello.token = token;
|
|
156
155
|
|
|
157
156
|
const stop = () => void connection.close();
|
|
158
157
|
signal?.addEventListener("abort", stop, { once: true });
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
// Reads the emulator's memory access map as coverage bytes (D40): one byte
|
|
2
|
+
// per address, RAM, ROM and I/O apart. The MCP tool and the skill scripts share it.
|
|
3
|
+
|
|
4
|
+
import { COVERAGE, type CodeRange } from "../c64.ts";
|
|
5
|
+
import { MAX_MEMMAP_RANGES, type MemmapRange } from "../protocol/vice.ts";
|
|
6
|
+
import { readProjectFile } from "./transfer.ts";
|
|
7
|
+
import type { ViceSessionClient } from "./vice-session.ts";
|
|
8
|
+
|
|
9
|
+
/** A full coverage file: offset n is address n (D40). */
|
|
10
|
+
export const FULL_COVERAGE = 0x10000;
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Reads full coverage files from the project and joins them: an address has
|
|
14
|
+
* every flag that any run gave it. `fail` refuses a file of another size.
|
|
15
|
+
*/
|
|
16
|
+
export function readCoverageFiles(paths: string[], fail: (message: string) => never): Uint8Array | undefined {
|
|
17
|
+
if (paths.length === 0) return undefined;
|
|
18
|
+
const joined = new Uint8Array(FULL_COVERAGE);
|
|
19
|
+
for (const path of paths) {
|
|
20
|
+
const { bytes } = readProjectFile(path);
|
|
21
|
+
if (bytes.length !== FULL_COVERAGE) {
|
|
22
|
+
fail(`--coverage ${path} has ${bytes.length} bytes; a full coverage file has 65536, one for each address. Write it with c64_memmap action read and out, without start and end.`);
|
|
23
|
+
}
|
|
24
|
+
for (let address = 0; address < FULL_COVERAGE; address++) joined[address]! |= bytes[address]!;
|
|
25
|
+
}
|
|
26
|
+
return joined;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** The coverage bytes of start..end, read from the session in parts. */
|
|
30
|
+
export async function readCoverage(session: Pick<ViceSessionClient, "memmap">, start = 0x0000, end = 0xffff): Promise<Uint8Array> {
|
|
31
|
+
const map = new Uint8Array(end - start + 1);
|
|
32
|
+
for (let from: number | undefined = start; from !== undefined; ) {
|
|
33
|
+
const part: { ranges: MemmapRange[]; next?: number } = await session.memmap({ action: "read", start: from, end, maxRanges: MAX_MEMMAP_RANGES });
|
|
34
|
+
for (const range of part.ranges) map.fill(range.flags, range.start - start, range.end - start + 1);
|
|
35
|
+
from = part.next;
|
|
36
|
+
}
|
|
37
|
+
return map;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/** Runs of RAM that a program wrote, at least `minimum` bytes each, in a map from $0000. */
|
|
41
|
+
export function writtenBlocks(map: Uint8Array, minimum: number): CodeRange[] {
|
|
42
|
+
const blocks: CodeRange[] = [];
|
|
43
|
+
let start = -1;
|
|
44
|
+
for (let address = 0; address <= map.length; address++) {
|
|
45
|
+
const written = address < map.length && (map[address]! & COVERAGE.ramWrite) !== 0;
|
|
46
|
+
if (written && start < 0) start = address;
|
|
47
|
+
if (!written && start >= 0) {
|
|
48
|
+
if (address - start >= minimum) blocks.push({ start, end: address - 1 });
|
|
49
|
+
start = -1;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
return blocks;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** True when the CPU executed an instruction in RAM somewhere in the range. */
|
|
56
|
+
export function executedIn(map: Uint8Array, range: CodeRange): boolean {
|
|
57
|
+
for (let address = range.start; address <= range.end; address++) if ((map[address]! & COVERAGE.ramExecute) !== 0) return true;
|
|
58
|
+
return false;
|
|
59
|
+
}
|