@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
|
@@ -3,33 +3,59 @@
|
|
|
3
3
|
// wrote and then executed: the run-time evidence that also finds a simple
|
|
4
4
|
// cruncher whose bytes look like code. "capture" runs the program in an
|
|
5
5
|
// emulator until it reaches a given address (the unpacked entry point) and
|
|
6
|
-
// writes the memory into the project.
|
|
6
|
+
// writes the memory into the project. "loads" captures each part of a
|
|
7
|
+
// multi-load program (D53). No native unpack tool is used.
|
|
7
8
|
|
|
8
|
-
import { mkdirSync, writeFileSync } from "node:fs";
|
|
9
|
-
import { dirname } from "node:path";
|
|
10
9
|
import { parseArgs } from "node:util";
|
|
11
10
|
|
|
12
|
-
import { formatC64Address, parseC64Address, textToPetscii } from "#src/c64.ts";
|
|
11
|
+
import { EXECUTE, formatC64Address, parseC64Address, READ, textToPetscii, WRITE } from "#src/c64.ts";
|
|
12
|
+
import { autostartLoaded, LOAD_LIMIT_FRAMES } from "#src/host-client/autostart.ts";
|
|
13
|
+
import { executedIn, readCoverage, writtenBlocks } from "#src/host-client/coverage.ts";
|
|
14
|
+
import { checkActionSteps, runActionStep, type ActionStep } from "#src/host-client/steps.ts";
|
|
13
15
|
import { WireFailure } from "#src/host-client/tools.ts";
|
|
14
|
-
import { readProjectFile } from "#src/host-client/transfer.ts";
|
|
16
|
+
import { readProjectFile, writeProjectFile } from "#src/host-client/transfer.ts";
|
|
15
17
|
import { ViceSessionClient } from "#src/host-client/vice-session.ts";
|
|
16
18
|
import { resolveProjectPath } from "#src/project.ts";
|
|
17
19
|
import { runScript, UsageError } from "#src/script.ts";
|
|
18
20
|
import { inspect, PackingError, readMemoryMap, underRom, writtenThenExecuted } from "./evidence.ts";
|
|
21
|
+
import { loads, readLoadSteps } from "./loads.ts";
|
|
22
|
+
import { entryState, IO_AREAS, json, machineState, readRam, READY_FRAMES, vectorsOf } from "./machine.ts";
|
|
19
23
|
|
|
20
24
|
const USAGE = `unpack.ts inspect <program.prg>
|
|
21
|
-
unpack.ts trace <program.prg> [--frames <n>] [--entry <address>]
|
|
22
|
-
unpack.ts capture <program.prg> --until <address> --out <
|
|
25
|
+
unpack.ts trace <program.prg> [--frames <n>] [--entry <address>] [--steps <file>]
|
|
26
|
+
unpack.ts capture <program.prg> --until <address> --out <dir> [--range <start> <end>] [--timeout-frames <n>] [--steps <file>] [--min-stage <bytes>]
|
|
27
|
+
unpack.ts stages <program.prg> [--frames <n>] [--entry <address>] [--steps <file>] [--min-stage <bytes>]
|
|
28
|
+
unpack.ts loads <program> --steps <file> --out <dir> [--drive] [--min-block <bytes>]
|
|
23
29
|
|
|
24
30
|
inspect packing evidence: the BASIC start and the entropy of the bytes
|
|
25
31
|
trace run the program for <n> frames (default 3000) and report the memory
|
|
26
32
|
that it wrote and then executed; --entry starts it there instead of RUN
|
|
27
|
-
capture run the program until the CPU gets to <address>,
|
|
28
|
-
|
|
33
|
+
capture run the program until the CPU gets to <address>, and write into the
|
|
34
|
+
directory --out: image.bin (64 KiB of RAM), image.json (the entry,
|
|
35
|
+
the segments that the stages wrote, the vectors, the stages), a PRG
|
|
36
|
+
for each segment, and entry_state.json (registers, $00/$01, pages
|
|
37
|
+
0-3, VIC, SID and CIA registers, vectors, and what differs from a
|
|
38
|
+
clean reset). --range gives one segment in place of the found ones.
|
|
39
|
+
|
|
40
|
+
stages run the program for <n> frames (default 3000) and report each stage:
|
|
41
|
+
where it starts, the memory it wrote, and where the next one starts.
|
|
42
|
+
A stage starts when the CPU executes a block of at least
|
|
43
|
+
--min-stage bytes (default 64) that the stage before wrote.
|
|
44
|
+
|
|
45
|
+
loads run a multi-load program (a PRG, or a disk, tape or cartridge image)
|
|
46
|
+
with the steps file; each capture step {"capture": "<name>"} writes
|
|
47
|
+
<dir>/<name>/ with image.bin, image.json, entry_state.json and a PRG
|
|
48
|
+
for each block of at least --min-block bytes (default 64) that the C64
|
|
49
|
+
side changed since the previous capture. --drive also writes the 2 KiB
|
|
50
|
+
drive RAM as drive8.prg, for knowledge space drive8.
|
|
51
|
+
|
|
52
|
+
--steps <file> a JSON list of input steps that run right after the program
|
|
53
|
+
starts: write, registers, joystick, type, frames and runUntil, as in
|
|
54
|
+
c64-testing scenarios, with $ addresses only. Use it to press a key
|
|
55
|
+
in an intro or to get through a menu.
|
|
29
56
|
|
|
30
57
|
Addresses are $ and four hex digits. The result is one JSON object.`;
|
|
31
58
|
|
|
32
|
-
const MAX_READ = 4096;
|
|
33
59
|
|
|
34
60
|
function address(text: string, option: string): number {
|
|
35
61
|
try {
|
|
@@ -41,13 +67,36 @@ function address(text: string, option: string): number {
|
|
|
41
67
|
|
|
42
68
|
/** Written-then-executed bytes from this many on are unpacking, decrypting or relocating, not self-changing code. */
|
|
43
69
|
const UNPACKED_BYTES = 256;
|
|
44
|
-
const LOAD_POLL_FRAMES = 25;
|
|
45
|
-
const LOAD_LIMIT_FRAMES = 1500;
|
|
46
70
|
const MAX_ADVANCE = 10_000;
|
|
47
71
|
/** The most ranges one memory map read gives. */
|
|
48
72
|
const MAX_RANGES = 1000;
|
|
49
73
|
|
|
50
|
-
|
|
74
|
+
/** Reads and checks a steps file: action steps with $ addresses only (D43). */
|
|
75
|
+
function readSteps(path: string): ActionStep[] {
|
|
76
|
+
let value: unknown;
|
|
77
|
+
try {
|
|
78
|
+
value = JSON.parse(readProjectFile(path).bytes.toString("utf8"));
|
|
79
|
+
} catch (error) {
|
|
80
|
+
if (error instanceof WireFailure) throw error;
|
|
81
|
+
throw new UsageError(`--steps ${path} is not JSON: ${(error as Error).message}`);
|
|
82
|
+
}
|
|
83
|
+
return checkActionSteps(value, (message) => {
|
|
84
|
+
throw new UsageError(`--steps ${path}: ${message}`);
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Runs the steps; the reason when a runUntil step did not reach its target. */
|
|
89
|
+
async function runSteps(client: ViceSessionClient, steps: ActionStep[]): Promise<string | undefined> {
|
|
90
|
+
for (const [index, step] of steps.entries()) {
|
|
91
|
+
const notReached = await runActionStep(client, step, parseC64Address);
|
|
92
|
+
if (notReached !== undefined && "runUntil" in step) {
|
|
93
|
+
return `Step ${index + 1} did not reach its target in ${step.timeoutFrames} frames. It stopped by ${notReached.stopReason} at ${formatC64Address(notReached.pc)}.`;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
return undefined;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async function trace(program: string, options: { frames: number; entry?: number; steps: ActionStep[] }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
51
100
|
const { bytes } = readProjectFile(program);
|
|
52
101
|
if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
|
|
53
102
|
const load = bytes[0]! | (bytes[1]! << 8);
|
|
@@ -56,17 +105,12 @@ async function trace(program: string, options: { frames: number; entry?: number
|
|
|
56
105
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
57
106
|
try {
|
|
58
107
|
// Loaded but not started, so the memory map can start empty: the KERNAL start-up writes all of RAM.
|
|
59
|
-
await client
|
|
60
|
-
const head = bytes.subarray(2, Math.min(bytes.length, 2 + 64)).toString("hex");
|
|
61
|
-
let loaded = false;
|
|
62
|
-
for (let frames = 0; frames < LOAD_LIMIT_FRAMES && !loaded; frames += LOAD_POLL_FRAMES) {
|
|
63
|
-
await client.execution({ action: "advance-frames", count: LOAD_POLL_FRAMES, space: "c64" });
|
|
64
|
-
loaded = (await client.memoryRead({ address: load, size: head.length / 2, space: "c64", view: "ram" })).data === head;
|
|
65
|
-
}
|
|
66
|
-
if (!loaded) return { output: { traced: false, reason: `The program was not in memory at ${formatC64Address(load)} after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
|
|
108
|
+
if (!(await autostartLoaded(client, program, bytes))) return { output: { traced: false, reason: `The program was not in memory at ${formatC64Address(load)} after ${LOAD_LIMIT_FRAMES} frames.` }, failed: true };
|
|
67
109
|
await client.memmap({ action: "clear" });
|
|
68
110
|
if (options.entry !== undefined) await client.registersSet("c64", { pc: options.entry });
|
|
69
111
|
else await client.keyboard(textToPetscii("RUN\n"));
|
|
112
|
+
const stepFailed = await runSteps(client, options.steps);
|
|
113
|
+
if (stepFailed !== undefined) return { output: { traced: false, reason: stepFailed }, failed: true };
|
|
70
114
|
let ran = 0;
|
|
71
115
|
while (ran < options.frames) {
|
|
72
116
|
const step = await client.execution({ action: "advance-frames", count: Math.min(MAX_ADVANCE, options.frames - ran), space: "c64" });
|
|
@@ -75,11 +119,12 @@ async function trace(program: string, options: { frames: number; entry?: number
|
|
|
75
119
|
if (advanced === 0) break;
|
|
76
120
|
}
|
|
77
121
|
const { pc } = await client.registersGet("c64");
|
|
78
|
-
const ranges = await readMemoryMap(async (start
|
|
79
|
-
const map = await client.memmap({ action: "read", start, end, maxRanges: MAX_RANGES });
|
|
122
|
+
const ranges = await readMemoryMap(async (start) => {
|
|
123
|
+
const map = await client.memmap({ action: "read", start, end: 0xffff, maxRanges: MAX_RANGES });
|
|
80
124
|
if (!("ranges" in map)) throw new WireFailure("operation-failed", "The emulator did not give the memory map.");
|
|
81
|
-
|
|
82
|
-
|
|
125
|
+
const ranges = map.ranges.map(({ start: from, end, flags }) => ({ start: from, end, read: (flags & READ) !== 0, write: (flags & WRITE) !== 0, execute: (flags & EXECUTE) !== 0 }));
|
|
126
|
+
return map.next === undefined ? { ranges } : { ranges, next: map.next };
|
|
127
|
+
});
|
|
83
128
|
const { written, bytes: count, running } = writtenThenExecuted(ranges, pc);
|
|
84
129
|
const packing = count >= UNPACKED_BYTES || running !== undefined ? "likely" : count > 0 ? "unclear" : "unlikely";
|
|
85
130
|
return {
|
|
@@ -112,37 +157,214 @@ async function trace(program: string, options: { frames: number; entry?: number
|
|
|
112
157
|
}
|
|
113
158
|
}
|
|
114
159
|
|
|
115
|
-
|
|
160
|
+
/** Frames between two looks at the memory map while stages are followed. */
|
|
161
|
+
const STAGE_CHUNK_FRAMES = 25;
|
|
162
|
+
const MAX_STAGES = 16;
|
|
163
|
+
const STAGE_SNAPSHOT = "stage-chunk";
|
|
164
|
+
|
|
165
|
+
interface Stage {
|
|
166
|
+
entry: number;
|
|
167
|
+
firstFrame: number;
|
|
168
|
+
frames: number;
|
|
169
|
+
wrote: Array<{ start: number; end: number }>;
|
|
170
|
+
next?: number;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
interface Followed {
|
|
174
|
+
stages: Stage[];
|
|
175
|
+
frames: number;
|
|
176
|
+
/** True when the CPU got to `until`. */
|
|
177
|
+
reached: boolean;
|
|
178
|
+
/** Why the program did not start, when it did not. */
|
|
179
|
+
failed?: string;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Starts the program and follows its stages (D43). The memory map holds what
|
|
184
|
+
* the current stage wrote; when the CPU executed a block of at least
|
|
185
|
+
* `minStage` written bytes during a chunk of frames, the chunk runs again from
|
|
186
|
+
* its snapshot with exec breakpoints over those blocks, which stop at the
|
|
187
|
+
* first instruction of the next stage. Steps run in the tracked time: a frames
|
|
188
|
+
* step is tracked, the others act between two chunks. With `until`, the
|
|
189
|
+
* run ends when the CPU gets there.
|
|
190
|
+
*/
|
|
191
|
+
async function follow(
|
|
192
|
+
client: ViceSessionClient,
|
|
193
|
+
program: string,
|
|
194
|
+
options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number; until?: number },
|
|
195
|
+
): Promise<Followed> {
|
|
196
|
+
const { bytes } = readProjectFile(program);
|
|
197
|
+
if (bytes.length < 3) throw new UsageError("the file is too short for a PRG");
|
|
198
|
+
const load = bytes[0]! | (bytes[1]! << 8);
|
|
199
|
+
const sys = inspect(bytes).basicStart?.sys;
|
|
200
|
+
if (options.entry === undefined && sys === undefined) throw new UsageError("the program has no BASIC start line; give --entry <address>");
|
|
201
|
+
if (!(await autostartLoaded(client, program, bytes))) return { stages: [], frames: 0, reached: false, failed: `The program was not in memory at ${formatC64Address(load)} after ${LOAD_LIMIT_FRAMES} frames.` };
|
|
202
|
+
await client.memmap({ action: "clear" });
|
|
203
|
+
if (options.entry !== undefined) await client.registersSet("c64", { pc: options.entry });
|
|
204
|
+
else await client.keyboard(textToPetscii("RUN\n"));
|
|
205
|
+
const target = options.until === undefined ? undefined : await client.breakpoint({ action: "add", address: options.until, space: "c64" });
|
|
206
|
+
const found: Stage[] = [];
|
|
207
|
+
let entry = options.entry ?? parseC64Address(sys!);
|
|
208
|
+
let firstFrame = 0;
|
|
209
|
+
let total = 0;
|
|
210
|
+
let ended = false;
|
|
211
|
+
let reached = false;
|
|
212
|
+
const atTarget = (pc: number) => options.until !== undefined && pc === options.until;
|
|
213
|
+
const track = async (frames: number): Promise<void> => {
|
|
214
|
+
let left = frames;
|
|
215
|
+
while (left > 0 && !ended && !reached && found.length < MAX_STAGES) {
|
|
216
|
+
await client.snapshot({ action: "save", name: STAGE_SNAPSHOT });
|
|
217
|
+
const chunk = Math.min(STAGE_CHUNK_FRAMES, left);
|
|
218
|
+
const first = await client.execution({ action: "advance-frames", count: chunk, space: "c64" });
|
|
219
|
+
const firstPc = first.pc ?? (await client.registersGet("c64")).pc;
|
|
220
|
+
const map = await readCoverage(client);
|
|
221
|
+
const entered = writtenBlocks(map, options.minStage).filter((block) => executedIn(map, block));
|
|
222
|
+
if (entered.length === 0) {
|
|
223
|
+
const advanced = first.advancedFrames ?? 0;
|
|
224
|
+
total += advanced;
|
|
225
|
+
left -= advanced;
|
|
226
|
+
if (atTarget(firstPc)) {
|
|
227
|
+
// The stop at until comes before its instruction runs: a target in written memory starts a stage too.
|
|
228
|
+
const blocks = writtenBlocks(map, options.minStage);
|
|
229
|
+
if (blocks.some((block) => firstPc >= block.start && firstPc <= block.end)) {
|
|
230
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: blocks, next: firstPc });
|
|
231
|
+
entry = firstPc;
|
|
232
|
+
firstFrame = total;
|
|
233
|
+
await client.memmap({ action: "clear" });
|
|
234
|
+
}
|
|
235
|
+
reached = true;
|
|
236
|
+
} else if (advanced === 0) ended = true;
|
|
237
|
+
continue;
|
|
238
|
+
}
|
|
239
|
+
// Again from the start of the chunk, with breakpoints over the blocks: the stop is the next entry.
|
|
240
|
+
await client.snapshot({ action: "restore", name: STAGE_SNAPSHOT });
|
|
241
|
+
const points = [];
|
|
242
|
+
for (const block of entered) points.push(await client.breakpoint({ action: "add", address: block.start, size: block.end - block.start + 1, space: "c64" }));
|
|
243
|
+
const rerun = await client.execution({ action: "advance-frames", count: chunk, space: "c64" });
|
|
244
|
+
for (const point of points) await client.breakpoint({ action: "remove", id: point.id });
|
|
245
|
+
const next = rerun.pc ?? (await client.registersGet("c64")).pc;
|
|
246
|
+
const used = rerun.advancedFrames ?? 0;
|
|
247
|
+
total += used;
|
|
248
|
+
left -= used;
|
|
249
|
+
if (entered.some((block) => next >= block.start && next <= block.end)) {
|
|
250
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(await readCoverage(client), options.minStage), next });
|
|
251
|
+
entry = next;
|
|
252
|
+
firstFrame = total;
|
|
253
|
+
await client.memmap({ action: "clear" });
|
|
254
|
+
} else if (!atTarget(next)) {
|
|
255
|
+
throw new WireFailure("operation-failed", `The run from the snapshot did not enter the written memory again; it stopped at ${formatC64Address(next)}.`);
|
|
256
|
+
}
|
|
257
|
+
if (atTarget(next)) reached = true;
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
for (const step of options.steps) {
|
|
261
|
+
if ("frames" in step) await track(step.frames);
|
|
262
|
+
else await runActionStep(client, step, parseC64Address);
|
|
263
|
+
}
|
|
264
|
+
await track(Math.max(0, options.frames - total));
|
|
265
|
+
if (target !== undefined) await client.breakpoint({ action: "remove", id: target.id });
|
|
266
|
+
await client.snapshot({ action: "discard", name: STAGE_SNAPSHOT }).catch(() => undefined);
|
|
267
|
+
found.push({ entry, firstFrame, frames: total - firstFrame, wrote: writtenBlocks(await readCoverage(client), options.minStage) });
|
|
268
|
+
return { stages: found, frames: total, reached };
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const showRange = (range: { start: number; end: number }) => ({ start: formatC64Address(range.start), end: formatC64Address(range.end) });
|
|
272
|
+
const showStages = (stages: Stage[]) =>
|
|
273
|
+
stages.map((stage, index) => ({
|
|
274
|
+
stage: index,
|
|
275
|
+
entry: formatC64Address(stage.entry),
|
|
276
|
+
frames: stage.frames,
|
|
277
|
+
wrote: stage.wrote.map(showRange),
|
|
278
|
+
...(stage.next === undefined ? {} : { next: formatC64Address(stage.next) }),
|
|
279
|
+
}));
|
|
280
|
+
|
|
281
|
+
async function stages(program: string, options: { frames: number; entry?: number; steps: ActionStep[]; minStage: number }): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
282
|
+
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
283
|
+
try {
|
|
284
|
+
const followed = await follow(client, program, options);
|
|
285
|
+
if (followed.failed !== undefined) return { output: { followed: false, reason: followed.failed }, failed: true };
|
|
286
|
+
return {
|
|
287
|
+
output: {
|
|
288
|
+
followed: true,
|
|
289
|
+
frames: followed.frames,
|
|
290
|
+
stages: showStages(followed.stages),
|
|
291
|
+
pc: formatC64Address((await client.registersGet("c64")).pc),
|
|
292
|
+
...(followed.stages.length > MAX_STAGES ? { moreStages: true } : {}),
|
|
293
|
+
},
|
|
294
|
+
failed: false,
|
|
295
|
+
};
|
|
296
|
+
} finally {
|
|
297
|
+
await client.close();
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
async function capture(
|
|
303
|
+
program: string,
|
|
304
|
+
options: { until: number; out: string; range?: [number, number]; timeoutFrames: number; steps: ActionStep[]; minStage: number },
|
|
305
|
+
): Promise<{ output: Record<string, unknown>; failed: boolean }> {
|
|
116
306
|
const client = await ViceSessionClient.open({ videoStandard: "pal" });
|
|
117
307
|
try {
|
|
118
|
-
await client
|
|
119
|
-
|
|
120
|
-
|
|
308
|
+
const followed = await follow(client, program, { frames: options.timeoutFrames, steps: options.steps, minStage: options.minStage, until: options.until });
|
|
309
|
+
if (followed.failed !== undefined) return { output: { captured: false, reason: followed.failed }, failed: true };
|
|
310
|
+
const registers = await client.registersGet("c64");
|
|
311
|
+
if (!followed.reached) {
|
|
121
312
|
return {
|
|
122
|
-
output: { captured: false, reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It
|
|
313
|
+
output: { captured: false, reason: `The program did not get to ${formatC64Address(options.until)} in ${options.timeoutFrames} frames. It is at ${formatC64Address(registers.pc)}.`, stages: showStages(followed.stages) },
|
|
123
314
|
failed: true,
|
|
124
315
|
};
|
|
125
316
|
}
|
|
126
|
-
|
|
127
|
-
const memory =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
}
|
|
134
|
-
const
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
317
|
+
// The RAM view: unpacked code is often under the ROMs or the I/O area.
|
|
318
|
+
const memory = await readRam(client, 0x0000, 0x10000);
|
|
319
|
+
const state = await machineState(client);
|
|
320
|
+
// The segments: what the stages wrote, without the blocks that hold the code of an earlier stage.
|
|
321
|
+
const earlier = followed.stages.slice(0, -1).map((stage) => stage.entry);
|
|
322
|
+
const written = followed.stages.flatMap((stage, index) => stage.wrote.map((range) => ({ ...range, writtenBy: index })));
|
|
323
|
+
const segments = options.range !== undefined ? [{ start: options.range[0], end: options.range[1] }] : written.filter((range) => !earlier.some((entry) => entry >= range.start && entry <= range.end));
|
|
324
|
+
const file = (path: string) => `${options.out}/${path}`;
|
|
325
|
+
const write = (path: string, bytes: Uint8Array) => writeProjectFile(file(path), bytes, { what: "--out" });
|
|
326
|
+
write("image.bin", memory);
|
|
327
|
+
const files = segments.map((segment) => {
|
|
328
|
+
const name = `segment-${segment.start.toString(16).padStart(4, "0")}-${segment.end.toString(16).padStart(4, "0")}.prg`;
|
|
329
|
+
write(name, Buffer.concat([Buffer.from([segment.start & 0xff, segment.start >> 8]), memory.subarray(segment.start, segment.end + 1)]));
|
|
330
|
+
return name;
|
|
331
|
+
});
|
|
332
|
+
const vectors = vectorsOf(state, memory);
|
|
333
|
+
write(
|
|
334
|
+
"image.json",
|
|
335
|
+
Buffer.from(
|
|
336
|
+
`${JSON.stringify(
|
|
337
|
+
{
|
|
338
|
+
entry: formatC64Address(registers.pc),
|
|
339
|
+
segments: segments.map((segment, index) => ({ ...showRange(segment), ...("writtenBy" in segment ? { writtenBy: segment.writtenBy } : {}), file: files[index] })),
|
|
340
|
+
vectors,
|
|
341
|
+
stages: showStages(followed.stages),
|
|
342
|
+
image: "image.bin",
|
|
343
|
+
},
|
|
344
|
+
null,
|
|
345
|
+
2,
|
|
346
|
+
)}\n`,
|
|
347
|
+
),
|
|
348
|
+
);
|
|
349
|
+
// A clean machine at the READY prompt shows what the loader and the stages changed.
|
|
350
|
+
await client.reset({ mode: "hard", run: true });
|
|
351
|
+
await client.execution({ action: "advance-frames", count: READY_FRAMES, space: "c64" });
|
|
352
|
+
const clean = await machineState(client);
|
|
353
|
+
const differs: Array<{ address: string; entry: number; reset: number }> = [];
|
|
354
|
+
const compare = (base: number, entry: Buffer, reset: Buffer) => entry.forEach((value, offset) => value !== reset[offset] && differs.push({ address: formatC64Address(base + offset), entry: value, reset: reset[offset]! }));
|
|
355
|
+
compare(0x0000, state.port, clean.port);
|
|
356
|
+
compare(0x0002, state.pages.subarray(2), clean.pages.subarray(2));
|
|
357
|
+
for (const [name, [address]] of Object.entries(IO_AREAS)) compare(address, state.io[name as keyof typeof IO_AREAS], clean.io[name as keyof typeof IO_AREAS]);
|
|
358
|
+
write("entry_state.json", json(entryState(registers, state, vectors, differs)));
|
|
139
359
|
return {
|
|
140
360
|
output: {
|
|
141
361
|
captured: true,
|
|
142
362
|
pc: formatC64Address(registers.pc),
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
363
|
+
out: options.out,
|
|
364
|
+
files: ["image.json", "image.bin", "entry_state.json", ...files],
|
|
365
|
+
segments: segments.map(showRange),
|
|
366
|
+
stages: followed.stages.length,
|
|
367
|
+
differsFromReset: differs.length,
|
|
146
368
|
},
|
|
147
369
|
failed: false,
|
|
148
370
|
};
|
|
@@ -163,6 +385,10 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
163
385
|
"timeout-frames": { type: "string" },
|
|
164
386
|
frames: { type: "string" },
|
|
165
387
|
entry: { type: "string" },
|
|
388
|
+
steps: { type: "string" },
|
|
389
|
+
"min-stage": { type: "string" },
|
|
390
|
+
"min-block": { type: "string" },
|
|
391
|
+
drive: { type: "boolean" },
|
|
166
392
|
help: { type: "boolean" },
|
|
167
393
|
},
|
|
168
394
|
});
|
|
@@ -183,8 +409,33 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
183
409
|
if (rest.length > 0) throw new UsageError("trace takes one program");
|
|
184
410
|
const frames = values.frames === undefined ? 3000 : Number(values.frames);
|
|
185
411
|
if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
|
|
186
|
-
|
|
412
|
+
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
413
|
+
return trace(program, { frames, steps, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
|
|
414
|
+
}
|
|
415
|
+
if (command === "stages") {
|
|
416
|
+
if (rest.length > 0) throw new UsageError("stages takes one program");
|
|
417
|
+
const frames = values.frames === undefined ? 3000 : Number(values.frames);
|
|
418
|
+
if (!Number.isInteger(frames) || frames < 1 || frames > 30_000) throw new UsageError("--frames must be from 1 to 30000");
|
|
419
|
+
const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
|
|
420
|
+
if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
|
|
421
|
+
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
422
|
+
// A runUntil step cannot be tracked in chunks of frames; capture takes it.
|
|
423
|
+
if (steps.some((step) => "runUntil" in step)) throw new UsageError("stages takes no runUntil step; use frames steps");
|
|
424
|
+
return stages(program, { frames, steps, minStage, ...(values.entry === undefined ? {} : { entry: address(values.entry, "--entry") }) });
|
|
425
|
+
}
|
|
426
|
+
if (command === "loads") {
|
|
427
|
+
if (rest.length > 0) throw new UsageError("loads takes one program");
|
|
428
|
+
if (values.steps === undefined || values.out === undefined) throw new UsageError("loads needs --steps and --out");
|
|
429
|
+
try {
|
|
430
|
+
resolveProjectPath(values.out);
|
|
431
|
+
} catch {
|
|
432
|
+
throw new UsageError(`--out must be a path relative to the project directory that stays inside it, not ${values.out}`);
|
|
433
|
+
}
|
|
434
|
+
const minBlock = values["min-block"] === undefined ? 64 : Number(values["min-block"]);
|
|
435
|
+
if (!Number.isInteger(minBlock) || minBlock < 1 || minBlock > 0x10000) throw new UsageError("--min-block must be from 1 to 65536");
|
|
436
|
+
return loads(program, { out: values.out, steps: readLoadSteps(values.steps), drive: values.drive === true, minBlock });
|
|
187
437
|
}
|
|
438
|
+
if (values.drive !== undefined || values["min-block"] !== undefined) throw new UsageError("--drive and --min-block work only with loads");
|
|
188
439
|
if (command !== "capture") throw new UsageError(`unknown command ${command}`);
|
|
189
440
|
if (values.until === undefined || values.out === undefined) throw new UsageError("capture needs --until and --out");
|
|
190
441
|
// --range takes two values: the end is the next positional argument.
|
|
@@ -203,7 +454,10 @@ async function run(argv: string[]): Promise<{ output: unknown; failed: boolean }
|
|
|
203
454
|
}
|
|
204
455
|
const timeoutFrames = values["timeout-frames"] === undefined ? 3000 : Number(values["timeout-frames"]);
|
|
205
456
|
if (!Number.isInteger(timeoutFrames) || timeoutFrames < 1 || timeoutFrames > 30_000) throw new UsageError("--timeout-frames must be from 1 to 30000");
|
|
206
|
-
|
|
457
|
+
const steps = values.steps === undefined ? [] : readSteps(values.steps);
|
|
458
|
+
const minStage = values["min-stage"] === undefined ? 64 : Number(values["min-stage"]);
|
|
459
|
+
if (!Number.isInteger(minStage) || minStage < 1 || minStage > 0x10000) throw new UsageError("--min-stage must be from 1 to 65536");
|
|
460
|
+
return capture(program, { until: address(values.until, "--until"), out: values.out, ...(range === undefined ? {} : { range }), timeoutFrames, steps, minStage });
|
|
207
461
|
}
|
|
208
462
|
|
|
209
463
|
await runScript(
|
package/src/c64.ts
CHANGED
|
@@ -174,6 +174,34 @@ export function parseTokenized(program: Uint8Array): TokenizedProgram {
|
|
|
174
174
|
}
|
|
175
175
|
}
|
|
176
176
|
|
|
177
|
+
/** BASIC V2 keywords for the tokens $80 to $cb, in token order. */
|
|
178
|
+
const KEYWORDS = [
|
|
179
|
+
"END", "FOR", "NEXT", "DATA", "INPUT#", "INPUT", "DIM", "READ", "LET", "GOTO", "RUN", "IF", "RESTORE", "GOSUB", "RETURN", "REM",
|
|
180
|
+
"STOP", "ON", "WAIT", "LOAD", "SAVE", "VERIFY", "DEF", "POKE", "PRINT#", "PRINT", "CONT", "LIST", "CLR", "CMD", "SYS", "OPEN",
|
|
181
|
+
"CLOSE", "GET", "NEW", "TAB(", "TO", "FN", "SPC(", "THEN", "NOT", "STEP", "+", "-", "*", "/", "^", "AND",
|
|
182
|
+
"OR", ">", "=", "<", "SGN", "INT", "ABS", "USR", "FRE", "POS", "SQR", "RND", "LOG", "EXP", "COS", "SIN",
|
|
183
|
+
"TAN", "ATN", "PEEK", "LEN", "STR$", "VAL", "ASC", "CHR$", "LEFT$", "RIGHT$", "MID$", "GO",
|
|
184
|
+
] as const;
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* One BASIC line as text: keywords for tokens, text in quotes and after REM
|
|
188
|
+
* as it is, and {$xx} for bytes that are no character. For evidence, not a listing.
|
|
189
|
+
*/
|
|
190
|
+
export function basicLineText(line: TokenizedLine): string {
|
|
191
|
+
let text = `${line.number} `;
|
|
192
|
+
let literal = false;
|
|
193
|
+
for (const byte of line.tokens) {
|
|
194
|
+
if (byte === QUOTE) literal = !literal;
|
|
195
|
+
if (!literal && byte >= 0x80 && byte < 0x80 + KEYWORDS.length) {
|
|
196
|
+
text += KEYWORDS[byte - 0x80];
|
|
197
|
+
if (byte === TOKEN.rem) literal = true;
|
|
198
|
+
} else {
|
|
199
|
+
text += petsciiNameToText(Uint8Array.of(byte));
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
return text;
|
|
203
|
+
}
|
|
204
|
+
|
|
177
205
|
/**
|
|
178
206
|
* Evaluates a SYS argument that holds only numbers, + - * / and brackets.
|
|
179
207
|
* Returns undefined for anything that needs run-time values.
|
|
@@ -279,3 +307,66 @@ export function findHandoffs(lines: TokenizedLine[]): BasicHandoff[] {
|
|
|
279
307
|
}
|
|
280
308
|
return handoffs;
|
|
281
309
|
}
|
|
310
|
+
|
|
311
|
+
// Execution coverage (D40): the byte format of a coverage map, and the 6502
|
|
312
|
+
// instruction lengths that turn the instruction starts in it into code ranges.
|
|
313
|
+
// VICE marks only the opcode byte of an executed instruction as executed.
|
|
314
|
+
|
|
315
|
+
/** One byte per address: the access flags as VICE reports them, RAM, ROM and I/O apart. */
|
|
316
|
+
export const COVERAGE = {
|
|
317
|
+
ramRead: 1 << 0,
|
|
318
|
+
ramWrite: 1 << 1,
|
|
319
|
+
ramExecute: 1 << 2,
|
|
320
|
+
romRead: 1 << 3,
|
|
321
|
+
romExecute: 1 << 4,
|
|
322
|
+
ioRead: 1 << 5,
|
|
323
|
+
ioWrite: 1 << 6,
|
|
324
|
+
ioExecute: 1 << 7,
|
|
325
|
+
} as const;
|
|
326
|
+
|
|
327
|
+
export const EXECUTE = COVERAGE.ramExecute | COVERAGE.romExecute | COVERAGE.ioExecute;
|
|
328
|
+
export const READ = COVERAGE.ramRead | COVERAGE.romRead | COVERAGE.ioRead;
|
|
329
|
+
export const WRITE = COVERAGE.ramWrite | COVERAGE.ioWrite;
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* The length in bytes of the NMOS 6502/6510 instruction with this opcode, for
|
|
333
|
+
* all 256 opcodes with the undocumented ones. The opcode is aaabbbcc: bbb
|
|
334
|
+
* selects the addressing mode within a group cc. JAM (KIL) and BRK count one byte.
|
|
335
|
+
*/
|
|
336
|
+
export function instructionLength(opcode: number): 1 | 2 | 3 {
|
|
337
|
+
const group = opcode & 0b11;
|
|
338
|
+
const mode = (opcode >> 2) & 0b111;
|
|
339
|
+
if (group === 0b00) {
|
|
340
|
+
if (mode === 0b000) return opcode === 0x20 ? 3 : opcode === 0x00 || opcode === 0x40 || opcode === 0x60 ? 1 : 2;
|
|
341
|
+
return ([2, 2, 1, 3, 2, 2, 1, 3] as const)[mode]!;
|
|
342
|
+
}
|
|
343
|
+
if (group === 0b10) {
|
|
344
|
+
if (mode === 0b000) return opcode === 0x02 || opcode === 0x22 || opcode === 0x42 || opcode === 0x62 ? 1 : 2;
|
|
345
|
+
return ([2, 2, 1, 3, 1, 2, 1, 3] as const)[mode]!;
|
|
346
|
+
}
|
|
347
|
+
// Groups 01 and 11 share their addressing modes.
|
|
348
|
+
return ([2, 2, 2, 3, 2, 2, 3, 3] as const)[mode]!;
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
export interface CodeRange {
|
|
352
|
+
start: number;
|
|
353
|
+
end: number;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The executed code in RAM as ranges: each RAM instruction start in `map`
|
|
358
|
+
* with its operand bytes, by the opcode in `memory` (64 KiB from $0000).
|
|
359
|
+
* `map` starts at `base`. Adjacent and overlapping instructions merge.
|
|
360
|
+
*/
|
|
361
|
+
export function executedRamCode(map: Uint8Array, base: number, memory: Uint8Array): CodeRange[] {
|
|
362
|
+
const ranges: CodeRange[] = [];
|
|
363
|
+
for (let offset = 0; offset < map.length; offset++) {
|
|
364
|
+
if ((map[offset]! & COVERAGE.ramExecute) === 0) continue;
|
|
365
|
+
const start = base + offset;
|
|
366
|
+
const end = Math.min(0xffff, start + instructionLength(memory[start]!) - 1);
|
|
367
|
+
const last = ranges.at(-1);
|
|
368
|
+
if (last !== undefined && start <= last.end + 1) last.end = Math.max(last.end, end);
|
|
369
|
+
else ranges.push({ start, end });
|
|
370
|
+
}
|
|
371
|
+
return ranges;
|
|
372
|
+
}
|
package/src/cli/status.ts
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
// Runtime with its tools.
|
|
3
3
|
|
|
4
4
|
import { hostTools, type ToolCallOptions } from "../host-client/tools.ts";
|
|
5
|
+
import { STOP_AND_ASK } from "../protocol/messages.ts";
|
|
5
6
|
import type { ToolStatus } from "../protocol/tools.ts";
|
|
6
7
|
|
|
7
8
|
/** One line per tool under `heading`: its version, or why it does not run. */
|
|
@@ -28,7 +29,8 @@ export async function hostStatusLines(options: ToolCallOptions = {}): Promise<st
|
|
|
28
29
|
try {
|
|
29
30
|
tools = await hostTools(options);
|
|
30
31
|
} catch (error) {
|
|
31
|
-
|
|
32
|
+
// status is read by a person: the instruction for an agent (D37) stays out.
|
|
33
|
+
return [`Host Runtime: not reachable. ${(error as Error).message.replace(` ${STOP_AND_ASK}`, "")}`];
|
|
32
34
|
}
|
|
33
35
|
return ["Host Runtime: reachable", ...toolLines("Tools on the host:", tools)];
|
|
34
36
|
}
|
package/src/host/main.ts
CHANGED
|
@@ -5,7 +5,7 @@ import { parseArgs } from "node:util";
|
|
|
5
5
|
import { DEFAULT_HOST_PORT } from "../protocol/messages.ts";
|
|
6
6
|
import { ProcessSupervisor } from "../native/processes.ts";
|
|
7
7
|
import { startTrace, trace } from "../trace.ts";
|
|
8
|
-
import {
|
|
8
|
+
import { ListenError, parsePort, startHostServer, stopSignal } from "./server.ts";
|
|
9
9
|
import { createToolDispatcher } from "./tools/index.ts";
|
|
10
10
|
import { checkViceStarts, findVice } from "./vice/process.ts";
|
|
11
11
|
import { viceSessionFactory } from "./vice/session.ts";
|
|
@@ -13,22 +13,19 @@ import { viceSessionFactory } from "./vice/session.ts";
|
|
|
13
13
|
const HELP = `c64-re-tools-host
|
|
14
14
|
|
|
15
15
|
Usage:
|
|
16
|
-
npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host [--port <port>]
|
|
16
|
+
npx -y --package=@henols/c64-re-tools@latest c64-re-tools-host [--port <port>]
|
|
17
17
|
c64-re-tools-host --help
|
|
18
18
|
|
|
19
19
|
Runs the c64-re-tools Host Runtime in the foreground. It first starts VICE
|
|
20
20
|
once (x64sc on PATH, or C64RT_VICE) and does not start if VICE cannot run.
|
|
21
|
-
It listens on
|
|
21
|
+
It listens on port ${DEFAULT_HOST_PORT} on every address, so a dev container
|
|
22
|
+
reaches it as host.docker.internal. It starts one VICE emulator for each
|
|
22
23
|
connected c64-re-tools-mcp process, and runs the tools that come with VICE
|
|
23
24
|
(c1541, petcat) for skill scripts. Stop it with Ctrl+C; that stops every
|
|
24
25
|
emulator and tool it started.
|
|
25
26
|
|
|
26
27
|
Options:
|
|
27
28
|
--port <port> Listen on this port instead (0 picks a free port).
|
|
28
|
-
--listen <address> Also listen on this address, on the same port, for
|
|
29
|
-
example the container bridge (172.17.0.1). An address
|
|
30
|
-
that is not loopback needs C64RT_HOST_TOKEN: set it to
|
|
31
|
-
the same secret for this program and for the clients.
|
|
32
29
|
--help Show this help.`;
|
|
33
30
|
|
|
34
31
|
function log(line: string): void {
|
|
@@ -37,9 +34,9 @@ function log(line: string): void {
|
|
|
37
34
|
}
|
|
38
35
|
|
|
39
36
|
async function main(): Promise<number> {
|
|
40
|
-
let values: { help?: boolean; port?: string
|
|
37
|
+
let values: { help?: boolean; port?: string };
|
|
41
38
|
try {
|
|
42
|
-
({ values } = parseArgs({ options: { help: { type: "boolean" }, port: { type: "string" }
|
|
39
|
+
({ values } = parseArgs({ options: { help: { type: "boolean" }, port: { type: "string" } }, strict: true }));
|
|
43
40
|
} catch (error) {
|
|
44
41
|
process.stderr.write(`${error instanceof Error ? error.message : String(error)}\n\n${HELP}\n`);
|
|
45
42
|
return 2;
|
|
@@ -56,13 +53,6 @@ async function main(): Promise<number> {
|
|
|
56
53
|
return 2;
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
const extraHosts = values.listen ?? [];
|
|
60
|
-
const token = process.env.C64RT_HOST_TOKEN;
|
|
61
|
-
if (extraHosts.some((address) => !isLoopback(address)) && (token === undefined || token.length < 16)) {
|
|
62
|
-
process.stderr.write("--listen with an address that is not loopback needs C64RT_HOST_TOKEN with at least 16 characters, so that only your clients can connect.\n");
|
|
63
|
-
return 2;
|
|
64
|
-
}
|
|
65
|
-
|
|
66
56
|
// Without an emulator the runtime is useless: refuse to start, by name and with the remedy.
|
|
67
57
|
try {
|
|
68
58
|
findVice(process.env);
|
|
@@ -90,9 +80,8 @@ async function main(): Promise<number> {
|
|
|
90
80
|
let server;
|
|
91
81
|
try {
|
|
92
82
|
server = await startHostServer({
|
|
83
|
+
host: "0.0.0.0",
|
|
93
84
|
port,
|
|
94
|
-
extraHosts,
|
|
95
|
-
...(token === undefined || token === "" ? {} : { token }),
|
|
96
85
|
log,
|
|
97
86
|
createViceSession: viceSessionFactory({ supervisor, log }),
|
|
98
87
|
tools: createToolDispatcher({ supervisor, log }),
|
|
@@ -102,15 +91,11 @@ async function main(): Promise<number> {
|
|
|
102
91
|
process.stderr.write(`Port ${error.port} on ${error.address} is in use. Is another c64-re-tools-host running?\n`);
|
|
103
92
|
return 1;
|
|
104
93
|
}
|
|
105
|
-
if (error instanceof ListenError && error.code === "EADDRNOTAVAIL") {
|
|
106
|
-
process.stderr.write(`The address ${error.address} in --listen does not belong to this machine.\n`);
|
|
107
|
-
return 1;
|
|
108
|
-
}
|
|
109
94
|
throw error;
|
|
110
95
|
}
|
|
111
|
-
const
|
|
112
|
-
process.stdout.write(`c64-re-tools-host listening on ${
|
|
113
|
-
trace().event("host.listening", { addresses });
|
|
96
|
+
const address = `${server.host}:${server.port}`;
|
|
97
|
+
process.stdout.write(`c64-re-tools-host listening on ${address}\n`);
|
|
98
|
+
trace().event("host.listening", { addresses: [address] });
|
|
114
99
|
|
|
115
100
|
trace().event("host.stop", { signal: await stopSignal(log) });
|
|
116
101
|
await server.close();
|