@henols/vice-mcp 0.2.2 → 0.2.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 +2 -2
- package/THIRD-PARTY-NOTICES.md +422 -1
- package/anno-bank.ts +171 -0
- package/anno-cli.ts +1736 -163
- package/anno-confidence.ts +2 -2
- package/anno-derive.ts +6 -6
- package/anno-details.ts +4 -4
- package/anno-enum-gen.ts +416 -30
- package/anno-export-asm.ts +1211 -126
- package/anno-graphics.ts +338 -0
- package/anno-hazard-report.ts +1367 -0
- package/anno-import.ts +495 -0
- package/anno-index.ts +8 -8
- package/anno-join.ts +480 -0
- package/anno-memmap-render.ts +22 -21
- package/anno-provenance-ledger.ts +472 -0
- package/anno-regbits-gen.ts +13 -13
- package/anno-register.ts +159 -0
- package/anno-store-export.ts +661 -0
- package/anno-store.ts +635 -124
- package/anno-symbols.ts +7 -7
- package/anno-tools.ts +1169 -16
- package/anno-types.ts +313 -40
- package/backend-detect.mts +124 -312
- package/build.ts +3 -1
- package/capture-predicate.ts +597 -0
- package/channel-lock.ts +349 -0
- package/evid-ingest.ts +217 -0
- package/evid-reconcile.ts +316 -0
- package/host-tool-client.ts +430 -0
- package/incident-record.ts +23 -12
- package/install-resources.ts +29 -13
- package/memmap-lookup.ts +285 -0
- package/package.json +27 -8
- package/prg-image.ts +1 -2
- package/repo-root.ts +87 -3
- package/resources/backend-detect.mjs +98 -236
- package/resources/broker-control.mjs +220 -54
- package/resources/broker-epoch.mjs +7 -8
- package/resources/broker-kill.mjs +36 -31
- package/resources/broker-launch.mjs +511 -374
- package/resources/broker-state.mjs +69 -24
- package/resources/container-guard.mjs +1 -1
- package/resources/ghidra-project.mjs +790 -0
- package/resources/host-tool.mjs +2533 -0
- package/resources/vice-broker.mjs +434 -290
- package/resources/vice-launcher.sh +127 -9
- package/stock-address.ts +1 -1
- package/stock-condition.ts +1 -1
- package/stock-connect.ts +9 -5
- package/stock-derived.ts +29 -37
- package/stock-diagnose.ts +200 -36
- package/stock-dispatch.ts +179 -77
- package/stock-handler.ts +1 -1
- package/stock-paths.ts +18 -14
- package/stock-petscii.ts +1 -1
- package/stock-protocol.ts +1 -1
- package/stock-recycle.ts +83 -2
- package/stock-reproducible-run.ts +811 -0
- package/stock-run-until.ts +100 -1
- package/stock-symbols.ts +4 -4
- package/stock-timing.ts +1 -1
- package/stop-oracle.ts +167 -0
- package/text-capability-probe.ts +660 -0
- package/text-connect.ts +157 -0
- package/text-protocol.ts +810 -0
- package/text-tools.ts +778 -0
- package/textmon-backtrace.ts +385 -0
- package/textmon-cpuhistory.ts +335 -0
- package/textmon-memmap.ts +494 -0
- package/textmon-profile.ts +458 -0
- package/textmon-registers.ts +748 -0
- package/tools-manifest.stock.json +864 -3
- package/vice-broker-client.ts +253 -108
- package/vice-errors.ts +268 -0
- package/vice-proxy.ts +339 -2144
- package/vsf-slice.ts +640 -0
- package/anno-d64.ts +0 -310
- package/capability-registry.ts +0 -390
- package/refresh-manifest.ts +0 -124
- package/tools-manifest.json +0 -1223
- package/vice-probe.ts +0 -278
- package/vice-sync.ts +0 -336
- package/vice.ts +0 -772
package/backend-detect.mts
CHANGED
|
@@ -1,54 +1,46 @@
|
|
|
1
1
|
// backend-detect.mts
|
|
2
2
|
//
|
|
3
|
-
//
|
|
4
|
-
//
|
|
5
|
-
//
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
// happen the moment one copy is updated and the other is not (mcpHost()'s
|
|
11
|
-
// own "three inlined copies" incident is the exact regression this file
|
|
12
|
-
// exists to keep from recurring here).
|
|
3
|
+
// FORKRM-01 (plan 52-06): this file used to be the ONE place that decided
|
|
4
|
+
// which VICE build a binary is (the fork's `-mcpserver` flag versus stock's
|
|
5
|
+
// `-binarymonitor`-only surface) and the ONE reader of an environment
|
|
6
|
+
// override naming a backend, anywhere in this tree. There is only one
|
|
7
|
+
// backend now, so there is nothing left to detect between: the `--help`
|
|
8
|
+
// probe (its own spawn, its fork/stock/unknown classifier) and the
|
|
9
|
+
// environment override are both deleted outright, not merely unreachable.
|
|
13
10
|
//
|
|
14
|
-
//
|
|
15
|
-
// -
|
|
16
|
-
//
|
|
17
|
-
//
|
|
18
|
-
//
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
//
|
|
22
|
-
//
|
|
23
|
-
//
|
|
24
|
-
//
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
//
|
|
30
|
-
//
|
|
31
|
-
//
|
|
32
|
-
//
|
|
33
|
-
//
|
|
11
|
+
// What SURVIVES, and why:
|
|
12
|
+
// - Binary-path resolution (`resolveBinPath`/`binPathFields`, WR-05) --
|
|
13
|
+
// `vice_ping`'s `resolvedBinaryPath` field and host-tool.mts's sibling
|
|
14
|
+
// binary resolution (`c1541`/`petcat`) both still need "which file did
|
|
15
|
+
// you actually mean", independent of any backend concept.
|
|
16
|
+
// - The on-disk identity/capability cache (BACK-04) -- it still records
|
|
17
|
+
// something worth caching once the backend verdict is gone: the version
|
|
18
|
+
// quad and CPU-history capability a live connect handshake (plan 02-08,
|
|
19
|
+
// stock-connect.ts) attaches to a resolved binary's identity
|
|
20
|
+
// (`resolvedPath`/`mtimeMs`/`sizeBytes`). `readCapabilityRecord()`/
|
|
21
|
+
// `writeCapabilityRecord()` are unchanged in shape; only the cache
|
|
22
|
+
// record's `backend` field is gone, since it never meant anything once
|
|
23
|
+
// there was nothing to distinguish.
|
|
24
|
+
//
|
|
25
|
+
// `ViceBackend` is NARROWED to a single literal (`"stock"`), not deleted:
|
|
26
|
+
// text-capability-probe.ts and stock-connect.ts (both out of this plan's
|
|
27
|
+
// scope -- plan 52-07 owns text-capability-probe.ts's own registry-shaped
|
|
28
|
+
// fork references) still import it as a type-only import, and deleting it
|
|
29
|
+
// outright would break their compile for no benefit this plan is scoped to
|
|
30
|
+
// deliver.
|
|
34
31
|
//
|
|
35
|
-
//
|
|
36
|
-
//
|
|
37
|
-
//
|
|
38
|
-
//
|
|
39
|
-
//
|
|
40
|
-
//
|
|
41
|
-
//
|
|
42
|
-
//
|
|
43
|
-
//
|
|
44
|
-
//
|
|
45
|
-
//
|
|
46
|
-
//
|
|
47
|
-
// resolve it, and no fixture string anywhere in this tree should ever be
|
|
48
|
-
// presented as real captured output from either build. See the follow-up
|
|
49
|
-
// todo tracked under .planning/todos/pending/ for what a real-hardware run
|
|
50
|
-
// must still confirm.
|
|
51
|
-
import { spawnSync } from "node:child_process";
|
|
32
|
+
// WHAT NOT TO DO:
|
|
33
|
+
// - Do not reintroduce an environment-variable backend override, a
|
|
34
|
+
// --help probe, or any other backend-discrimination mechanism. There is
|
|
35
|
+
// one binary shape now; a second detection mechanism would be solving a
|
|
36
|
+
// problem that no longer exists.
|
|
37
|
+
// - Do not call resolvedBackend() from inside broker-launch.mts's
|
|
38
|
+
// `inFlight` single-owner launch guard. This still performs filesystem
|
|
39
|
+
// I/O (a stat call and, on a fresh or changed identity, a cache write);
|
|
40
|
+
// anything that can block inside that synchronous check-and-set window
|
|
41
|
+
// is the exact failure class the 2026-08-01 triple-launch outage came
|
|
42
|
+
// from (D-03/T-02-25) -- unchanged reasoning from before this plan, only
|
|
43
|
+
// the mechanism inside resolvedBackend() changed.
|
|
52
44
|
import {
|
|
53
45
|
existsSync,
|
|
54
46
|
readFileSync,
|
|
@@ -60,98 +52,10 @@ import {
|
|
|
60
52
|
} from "node:fs";
|
|
61
53
|
import { join, resolve as resolvePath } from "node:path";
|
|
62
54
|
|
|
63
|
-
/**
|
|
64
|
-
*
|
|
65
|
-
* own
|
|
66
|
-
|
|
67
|
-
export type ViceBackend = "fork" | "stock";
|
|
68
|
-
|
|
69
|
-
// ---------------------------------------------------------------------------
|
|
70
|
-
// classifyHelpOutput() -- pure string classification, no I/O at all.
|
|
71
|
-
// ---------------------------------------------------------------------------
|
|
72
|
-
|
|
73
|
-
/** Matches on the literal flag tokens D-02 names as the discriminator: the
|
|
74
|
-
* fork's `-mcpserver` flag versus stock's `-binarymonitor`-only surface.
|
|
75
|
-
* `"fork"` wins when BOTH tokens appear (the fork's own VICE tree is a 3.10
|
|
76
|
-
* checkout and accepts both flags) -- checked FIRST, deliberately, so a
|
|
77
|
-
* build that advertises both is classified by the flag that actually makes
|
|
78
|
-
* it the fork, not merely "also has stock's flag too". `"unknown"` when
|
|
79
|
-
* NEITHER token appears -- a real --help transcript that does not match
|
|
80
|
-
* either shape, a probe that spawned nothing at all (empty text), or
|
|
81
|
-
* anything else this function was never taught to recognise. Never throws;
|
|
82
|
-
* pure function of the text it is given. */
|
|
83
|
-
export function classifyHelpOutput(text: string): "fork" | "stock" | "unknown" {
|
|
84
|
-
const hasFork = text.includes("-mcpserver");
|
|
85
|
-
const hasStock = text.includes("-binarymonitor");
|
|
86
|
-
if (hasFork) return "fork";
|
|
87
|
-
if (hasStock) return "stock";
|
|
88
|
-
return "unknown";
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
// ---------------------------------------------------------------------------
|
|
92
|
-
// probeBackend() -- the --help probe. spawnSync only, argv array, shell:
|
|
93
|
-
// false, never a shell string and never string interpolation of binPath
|
|
94
|
-
// into a command line (T-02-03's mitigation). Bounded by a 5000ms timeout
|
|
95
|
-
// with kill-on-timeout so a hostile or hung binary cannot stall broker
|
|
96
|
-
// startup (T-02-25's second mitigation half).
|
|
97
|
-
// ---------------------------------------------------------------------------
|
|
98
|
-
|
|
99
|
-
const PROBE_TIMEOUT_MS = 5000;
|
|
100
|
-
|
|
101
|
-
/** `--help` first, falling back to `-help` then `-?` ONLY when a run exits
|
|
102
|
-
* non-zero with EMPTY combined output -- a run that exits non-zero but
|
|
103
|
-
* still printed something (some builds write usage to stderr and exit 1) is
|
|
104
|
-
* already usable and is not retried further. */
|
|
105
|
-
const HELP_FLAG_CANDIDATES: readonly string[] = ["--help", "-help", "-?"];
|
|
106
|
-
|
|
107
|
-
export interface SpawnHelpResult {
|
|
108
|
-
text: string;
|
|
109
|
-
exitedZero: boolean;
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
export interface ProbeBackendDeps {
|
|
113
|
-
/** Runs ONE candidate flag against `binPath` and returns its combined
|
|
114
|
-
* stdout+stderr text (VICE writes usage to either, depending on build)
|
|
115
|
-
* plus whether the process exited zero. Injected so no test in this tree
|
|
116
|
-
* ever spawns a real binary -- this environment has none, and per this
|
|
117
|
-
* file's own environment-constraint note above, no test may execute one.
|
|
118
|
-
* Defaults to a real, argv-array, shell:false spawnSync call bounded by
|
|
119
|
-
* PROBE_TIMEOUT_MS with kill-on-timeout; never throws -- a spawn failure
|
|
120
|
-
* (ENOENT, EACCES, a real timeout) collapses to `{ text: "", exitedZero:
|
|
121
|
-
* false }`, which classifyHelpOutput() reads as "unknown" like any other
|
|
122
|
-
* unrecognised output. */
|
|
123
|
-
spawnHelp?: (binPath: string, flag: string) => SpawnHelpResult;
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
function defaultSpawnHelp(binPath: string, flag: string): SpawnHelpResult {
|
|
127
|
-
try {
|
|
128
|
-
const result = spawnSync(binPath, [flag], {
|
|
129
|
-
encoding: "utf8",
|
|
130
|
-
timeout: PROBE_TIMEOUT_MS,
|
|
131
|
-
killSignal: "SIGKILL",
|
|
132
|
-
});
|
|
133
|
-
const text = `${result.stdout ?? ""}${result.stderr ?? ""}`;
|
|
134
|
-
return { text, exitedZero: result.status === 0 };
|
|
135
|
-
} catch {
|
|
136
|
-
return { text: "", exitedZero: false };
|
|
137
|
-
}
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/** Runs the fallback ladder above against `binPath` and classifies whatever
|
|
141
|
-
* text the LAST attempted flag produced. Never throws -- every failure mode
|
|
142
|
-
* (spawn error, timeout, empty output, an exit code the caller does not
|
|
143
|
-
* recognise) flows through to classifyHelpOutput() as ordinary text, which
|
|
144
|
-
* itself never throws either. */
|
|
145
|
-
export function probeBackend(binPath: string, deps: ProbeBackendDeps = {}): "fork" | "stock" | "unknown" {
|
|
146
|
-
const spawnHelp = deps.spawnHelp ?? defaultSpawnHelp;
|
|
147
|
-
let text = "";
|
|
148
|
-
for (const flag of HELP_FLAG_CANDIDATES) {
|
|
149
|
-
const outcome = spawnHelp(binPath, flag);
|
|
150
|
-
text = outcome.text;
|
|
151
|
-
if (outcome.exitedZero || text.trim() !== "") break;
|
|
152
|
-
}
|
|
153
|
-
return classifyHelpOutput(text);
|
|
154
|
-
}
|
|
55
|
+
/** The one shape this tree ever launches or speaks to. FORKRM-01 narrowed
|
|
56
|
+
* this from a two-member union ("fork" | "stock") to this single literal --
|
|
57
|
+
* narrowed rather than deleted; see this file's own header for why. */
|
|
58
|
+
export type ViceBackend = "stock";
|
|
155
59
|
|
|
156
60
|
// ---------------------------------------------------------------------------
|
|
157
61
|
// The on-disk cache -- `join(supervisorDir, "backend.json")`. `supervisorDir`
|
|
@@ -177,11 +81,11 @@ export function probeBackend(binPath: string, deps: ProbeBackendDeps = {}): "for
|
|
|
177
81
|
//
|
|
178
82
|
// When `supervisorDir` is omitted entirely, every cache read/write below is a
|
|
179
83
|
// no-op (a miss on read, silently skipped on write) -- this module still
|
|
180
|
-
// answers correctly (
|
|
181
|
-
//
|
|
182
|
-
//
|
|
183
|
-
//
|
|
184
|
-
//
|
|
84
|
+
// answers correctly (identity resolution only), it just never persists
|
|
85
|
+
// anything across process restarts. This is a graceful degradation, not an
|
|
86
|
+
// error: a caller that has not yet resolved a supervisor directory (or
|
|
87
|
+
// genuinely has none) gets a working, if unpersisted, answer rather than a
|
|
88
|
+
// thrown exception or a guessed path.
|
|
185
89
|
// ---------------------------------------------------------------------------
|
|
186
90
|
|
|
187
91
|
export interface BackendCacheRecord {
|
|
@@ -189,11 +93,10 @@ export interface BackendCacheRecord {
|
|
|
189
93
|
resolvedPath: string;
|
|
190
94
|
mtimeMs: number;
|
|
191
95
|
sizeBytes: number;
|
|
192
|
-
backend: ViceBackend;
|
|
193
96
|
probedAt: string;
|
|
194
97
|
/** BACK-04: filled in later, by a connect handshake (plan 02-08), never by
|
|
195
|
-
* this file's own
|
|
196
|
-
*
|
|
98
|
+
* this file's own resolution -- resolvedBackend() only ever initialises
|
|
99
|
+
* the identity fields below; it has no way to observe a version quad. */
|
|
197
100
|
versionQuad?: string;
|
|
198
101
|
cpuHistoryAvailable?: boolean;
|
|
199
102
|
/** CR-01 (07-REVIEW.md re-review): the CLIENT-side schema version that
|
|
@@ -253,8 +156,7 @@ function readCacheRecord(supervisorDir: string): BackendCacheRecord | null {
|
|
|
253
156
|
if (
|
|
254
157
|
typeof parsed.resolvedPath !== "string" ||
|
|
255
158
|
typeof parsed.mtimeMs !== "number" ||
|
|
256
|
-
typeof parsed.sizeBytes !== "number"
|
|
257
|
-
(parsed.backend !== "fork" && parsed.backend !== "stock")
|
|
159
|
+
typeof parsed.sizeBytes !== "number"
|
|
258
160
|
) {
|
|
259
161
|
return null;
|
|
260
162
|
}
|
|
@@ -263,7 +165,6 @@ function readCacheRecord(supervisorDir: string): BackendCacheRecord | null {
|
|
|
263
165
|
resolvedPath: parsed.resolvedPath,
|
|
264
166
|
mtimeMs: parsed.mtimeMs,
|
|
265
167
|
sizeBytes: parsed.sizeBytes,
|
|
266
|
-
backend: parsed.backend,
|
|
267
168
|
probedAt: typeof parsed.probedAt === "string" ? parsed.probedAt : "",
|
|
268
169
|
};
|
|
269
170
|
if (typeof parsed.versionQuad === "string") record.versionQuad = parsed.versionQuad;
|
|
@@ -273,10 +174,10 @@ function readCacheRecord(supervisorDir: string): BackendCacheRecord | null {
|
|
|
273
174
|
}
|
|
274
175
|
|
|
275
176
|
/** Tmp-sibling -> chmod 0600 -> content -> rename, the SAME atomic-write
|
|
276
|
-
* discipline
|
|
277
|
-
*
|
|
278
|
-
*
|
|
279
|
-
*
|
|
177
|
+
* discipline vice-broker.mts's writeBrokerRecordFile() already uses -- a
|
|
178
|
+
* crash mid-write can only ever leave a stray tmp sibling behind, never a
|
|
179
|
+
* truncated or empty file at the real cache path that a later read would
|
|
180
|
+
* wrongly accept. */
|
|
280
181
|
function writeCacheRecordAtomic(supervisorDir: string, record: BackendCacheRecord): void {
|
|
281
182
|
mkdirSync(supervisorDir, { recursive: true });
|
|
282
183
|
const finalPath = cachePathFor(supervisorDir);
|
|
@@ -289,13 +190,10 @@ function writeCacheRecordAtomic(supervisorDir: string, record: BackendCacheRecor
|
|
|
289
190
|
|
|
290
191
|
// ---------------------------------------------------------------------------
|
|
291
192
|
// Binary identity -- resolve a possibly-bare command name (e.g. "x64sc") to
|
|
292
|
-
// an absolute path (for cache KEYING and stat only
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
-
//
|
|
296
|
-
// half: `{ resolvedPath, mtimeMs, sizeBytes }`, which catches a binary
|
|
297
|
-
// replaced in place (an `apt upgrade`, a manual `cp`) without hashing a
|
|
298
|
-
// multi-megabyte file on every broker start.
|
|
193
|
+
// an absolute path (for cache KEYING and stat only), and stat it for
|
|
194
|
+
// mtimeMs/sizeBytes -- the cache key: `{ resolvedPath, mtimeMs, sizeBytes }`,
|
|
195
|
+
// which catches a binary replaced in place (an `apt upgrade`, a manual `cp`)
|
|
196
|
+
// without hashing a multi-megabyte file on every broker start.
|
|
299
197
|
// ---------------------------------------------------------------------------
|
|
300
198
|
|
|
301
199
|
export interface BinaryIdentity {
|
|
@@ -326,34 +224,21 @@ function defaultStat(resolvedPath: string): BinaryIdentity | null {
|
|
|
326
224
|
}
|
|
327
225
|
}
|
|
328
226
|
|
|
329
|
-
function defaultLog(line: string): void {
|
|
330
|
-
process.stderr.write(`${line}\n`);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
227
|
// ---------------------------------------------------------------------------
|
|
334
228
|
// resolvedBackend() -- the public entry point.
|
|
335
229
|
// ---------------------------------------------------------------------------
|
|
336
230
|
|
|
337
|
-
export type ResolvedBackendSource = "override" | "cache" | "probe" | "indeterminate";
|
|
338
|
-
|
|
339
231
|
export interface ResolvedBackendResult {
|
|
340
232
|
backend: ViceBackend;
|
|
341
|
-
source: ResolvedBackendSource;
|
|
342
233
|
/**
|
|
343
234
|
* WR-05: the ABSOLUTE path this binary resolved to when it could be resolved,
|
|
344
|
-
* falling back to the configured name otherwise.
|
|
345
|
-
*
|
|
346
|
-
*
|
|
347
|
-
*
|
|
348
|
-
* (StockDispatchDeps.resolvedBinaryPath, and BACK-03's `vice_ping` answer),
|
|
349
|
-
* so `vice_ping` on stock reported `"x64sc"` -- a name that, inside a
|
|
350
|
-
* container, resolves to nothing at all.
|
|
235
|
+
* falling back to the configured name otherwise. Two consumers read this as
|
|
236
|
+
* a resolved path in their own doc comments (StockDispatchDeps.resolvedBinaryPath,
|
|
237
|
+
* and BACK-03's `vice_ping` answer), so a bare configured name (e.g. `"x64sc"`)
|
|
238
|
+
* would report a name that, inside a container, resolves to nothing at all.
|
|
351
239
|
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
354
|
-
* as it would for a real invocation. This field is what a HUMAN or an agent
|
|
355
|
-
* is shown, and "which file did you actually mean" is the question it has to
|
|
356
|
-
* answer.
|
|
240
|
+
* This field is what a HUMAN or an agent is shown, and "which file did you
|
|
241
|
+
* actually mean" is the question it has to answer.
|
|
357
242
|
*/
|
|
358
243
|
binPath: string;
|
|
359
244
|
/** WR-05: `true` only when `binPath` above is a real resolved absolute path;
|
|
@@ -362,119 +247,71 @@ export interface ResolvedBackendResult {
|
|
|
362
247
|
* the two it has, instead of a reader having to guess from whether the string
|
|
363
248
|
* happens to contain a slash. */
|
|
364
249
|
binPathResolved: boolean;
|
|
365
|
-
note?: string;
|
|
366
250
|
}
|
|
367
251
|
|
|
368
252
|
export interface ResolvedBackendDeps {
|
|
369
253
|
env?: NodeJS.ProcessEnv;
|
|
370
254
|
/** Which binary to detect against -- defaults to VICE_BIN or "x64sc",
|
|
371
255
|
* matching broker-launch.mts's own spawnAndRecordInstance() default
|
|
372
|
-
* exactly (one broker, one binary, one verdict
|
|
256
|
+
* exactly (one broker, one binary, one verdict). */
|
|
373
257
|
viceBin?: string;
|
|
374
258
|
/** See this module's own header comment on the cache section above --
|
|
375
259
|
* NEVER defaulted here. Omitted entirely disables the on-disk cache
|
|
376
|
-
* (
|
|
260
|
+
* (identity resolution only, never persisted). */
|
|
377
261
|
supervisorDir?: string;
|
|
378
262
|
resolveBinPath?: (bin: string, env: NodeJS.ProcessEnv) => string | null;
|
|
379
263
|
stat?: (resolvedPath: string) => BinaryIdentity | null;
|
|
380
|
-
probe?: (binPath: string) => "fork" | "stock" | "unknown";
|
|
381
264
|
now?: () => number;
|
|
382
|
-
log?: (line: string) => void;
|
|
383
265
|
}
|
|
384
266
|
|
|
385
|
-
// Memoised answer
|
|
386
|
-
//
|
|
387
|
-
//
|
|
388
|
-
//
|
|
389
|
-
// many scenarios; the detected-backend answer for a fixed binary cannot).
|
|
390
|
-
// This is what makes resolvedBackend() answer "once per long-running
|
|
391
|
-
// process" for the case that actually spawns something, while never
|
|
392
|
-
// requiring a caller to somehow signal "this is a fresh scenario" the way
|
|
393
|
-
// container-guard.mts's isInsideContainer() asks callers to pass explicit
|
|
394
|
-
// deps to bypass ITS OWN memo -- here, the override/no-override distinction
|
|
395
|
-
// already IS that signal.
|
|
267
|
+
// Memoised answer -- a long-running process (the real broker) resolves once
|
|
268
|
+
// per process lifetime; a test suite driving many scenarios calls
|
|
269
|
+
// resetResolvedBackendForTests() between them (see that function's own
|
|
270
|
+
// comment).
|
|
396
271
|
let memoisedResult: ResolvedBackendResult | null = null;
|
|
397
272
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
if (warnedBackendUnset) return;
|
|
406
|
-
warnedBackendUnset = true;
|
|
407
|
-
log(
|
|
408
|
-
`vice-broker: detected backend "${result.backend}" for ${viceBin} (source: ${result.source}) -- ` +
|
|
409
|
-
`set VICE_BACKEND=stock or VICE_BACKEND=fork to override this detection explicitly`,
|
|
410
|
-
);
|
|
411
|
-
}
|
|
412
|
-
|
|
413
|
-
/** Test-only escape hatch: clears the in-process memo and the D-06
|
|
414
|
-
* one-time-note gate. Never called by any real production code path --
|
|
415
|
-
* vice-broker.mts calls resolvedBackend() exactly once per real process
|
|
416
|
-
* lifetime and has no reason to ever reset it; this exists solely so
|
|
417
|
-
* backend-detect.test.ts can drive many distinct scenarios (cache hit, cache
|
|
418
|
-
* miss, indeterminate, ...) in one shared test process without one
|
|
419
|
-
* scenario's memoised answer contaminating the next -- mirroring
|
|
420
|
-
* broker-launch.test.ts's own discipline of restoring module-level state
|
|
421
|
-
* between test cases, made explicit here rather than left to careful test
|
|
422
|
-
* ordering, since this module's memo (unlike buildViceArgs()'s one-time
|
|
423
|
-
* note) has no natural "always widens the same way" ordering to exploit. */
|
|
273
|
+
/** Test-only escape hatch: clears the in-process memo. Never called by any
|
|
274
|
+
* real production code path -- vice-broker.mts calls resolvedBackend()
|
|
275
|
+
* exactly once per real process lifetime and has no reason to ever reset it;
|
|
276
|
+
* this exists solely so backend-detect.test.ts can drive many distinct
|
|
277
|
+
* scenarios (fresh identity, matching cached identity, changed identity, ...)
|
|
278
|
+
* in one shared test process without one scenario's memoised answer
|
|
279
|
+
* contaminating the next. */
|
|
424
280
|
export function resetResolvedBackendForTests(): void {
|
|
425
281
|
memoisedResult = null;
|
|
426
|
-
warnedBackendUnset = false;
|
|
427
282
|
}
|
|
428
283
|
|
|
429
|
-
/**
|
|
430
|
-
*
|
|
431
|
-
*
|
|
432
|
-
*
|
|
433
|
-
* `{ resolvedPath, mtimeMs, sizeBytes }` all match the stored record); on a
|
|
434
|
-
* miss, probes via probeBackend() and writes the cache. Memoises the
|
|
435
|
-
* probe/cache answer in a module-level variable so a long-running process
|
|
436
|
-
* resolves once (see the memo's own comment above for what "once" means
|
|
437
|
-
* here). Never throws: a probe that classifies "unknown" (including a
|
|
438
|
-
* spawn failure or a timeout, both of which probeBackend() already reduces
|
|
439
|
-
* to "unknown") returns a defined `{ backend: "fork", source:
|
|
440
|
-
* "indeterminate", ... }` outcome instead -- "fork" because that is the
|
|
441
|
-
* pre-Phase-2 behaviour every existing install already has, so an
|
|
442
|
-
* undetectable binary degrades to what already worked rather than to
|
|
443
|
-
* nothing. */
|
|
444
|
-
/** WR-05: the ONE place `binPath`/`binPathResolved` are derived, so the four
|
|
445
|
-
* return paths below cannot disagree about what "the binary" means. A resolved
|
|
446
|
-
* absolute path when there is one; the configured name, flagged as unresolved,
|
|
447
|
-
* when there is not. */
|
|
284
|
+
/** WR-05: the ONE place `binPath`/`binPathResolved` are derived, so a
|
|
285
|
+
* resolved absolute path when there is one, or the configured name flagged
|
|
286
|
+
* as unresolved when there is not, is computed identically everywhere this
|
|
287
|
+
* file returns it. */
|
|
448
288
|
function binPathFields(resolvedPath: string | null, viceBin: string): { binPath: string; binPathResolved: boolean } {
|
|
449
289
|
return resolvedPath !== null ? { binPath: resolvedPath, binPathResolved: true } : { binPath: viceBin, binPathResolved: false };
|
|
450
290
|
}
|
|
451
291
|
|
|
292
|
+
/** Resolves the emulator binary's identity and (re)initialises the on-disk
|
|
293
|
+
* identity/capability cache record for it when a `supervisorDir` is given.
|
|
294
|
+
* Memoises the answer in a module-level variable so a long-running process
|
|
295
|
+
* resolves once (see the memo's own comment above for what "once" means
|
|
296
|
+
* here). Never throws.
|
|
297
|
+
*
|
|
298
|
+
* FORKRM-01: there is nothing left to detect -- `backend` is always
|
|
299
|
+
* `"stock"`. What this function still does is identity resolution (WR-05's
|
|
300
|
+
* `binPath`/`binPathResolved`) and cache bookkeeping for BACK-04's capability
|
|
301
|
+
* record: when the resolved binary's identity (`resolvedPath`/`mtimeMs`/
|
|
302
|
+
* `sizeBytes`) does not match whatever is already on file -- no record at
|
|
303
|
+
* all, or a record describing a DIFFERENT binary (replaced in place, or a
|
|
304
|
+
* fresh install) -- a fresh identity record is written, with no stale
|
|
305
|
+
* capability fields carried over from a different binary's answer. A
|
|
306
|
+
* matching identity is left untouched, so a capability answer already
|
|
307
|
+
* recorded for THIS binary survives. */
|
|
452
308
|
export function resolvedBackend(deps: ResolvedBackendDeps = {}): ResolvedBackendResult {
|
|
309
|
+
if (memoisedResult !== null) return memoisedResult;
|
|
310
|
+
|
|
453
311
|
const env = deps.env ?? process.env;
|
|
454
312
|
const viceBin = deps.viceBin ?? env.VICE_BIN ?? "x64sc";
|
|
455
|
-
// A direct read of the real environment on the right of this ternary
|
|
456
|
-
// (rather than the generic `env` local above) is deliberate: this file is
|
|
457
|
-
// grep-gated, tree-wide, as the ONE place that ever names this variable
|
|
458
|
-
// directly against the real environment -- `deps.env` (the test-injection
|
|
459
|
-
// seam) still takes precedence when supplied, exactly like every other
|
|
460
|
-
// field on this options object.
|
|
461
|
-
const override = deps.env ? deps.env.VICE_BACKEND : process.env.VICE_BACKEND;
|
|
462
313
|
const resolveBinPath = deps.resolveBinPath ?? defaultResolveBinPath;
|
|
463
|
-
|
|
464
|
-
if (override === "stock" || override === "fork") {
|
|
465
|
-
// WR-05: an explicit backend override still resolves the PATH, so
|
|
466
|
-
// `vice_ping` reports a real file rather than the bare name. This is a
|
|
467
|
-
// filesystem lookup only -- existsSync per PATH entry -- and NEVER a spawn,
|
|
468
|
-
// so the override path keeps its "answered fresh, straight from the
|
|
469
|
-
// environment, spawns nothing" property.
|
|
470
|
-
return { backend: override, source: "override", ...binPathFields(resolveBinPath(viceBin, env), viceBin) };
|
|
471
|
-
}
|
|
472
|
-
|
|
473
|
-
if (memoisedResult !== null) return memoisedResult;
|
|
474
|
-
|
|
475
|
-
const log = deps.log ?? defaultLog;
|
|
476
314
|
const stat = deps.stat ?? defaultStat;
|
|
477
|
-
const probe = deps.probe ?? ((bin: string) => probeBackend(bin));
|
|
478
315
|
const now = deps.now ?? ((): number => Date.now());
|
|
479
316
|
|
|
480
317
|
const resolvedPath = resolveBinPath(viceBin, env);
|
|
@@ -482,56 +319,34 @@ export function resolvedBackend(deps: ResolvedBackendDeps = {}): ResolvedBackend
|
|
|
482
319
|
const cacheEligible = resolvedPath !== null && identity !== null && typeof deps.supervisorDir === "string";
|
|
483
320
|
|
|
484
321
|
if (cacheEligible) {
|
|
485
|
-
const
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
) {
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
322
|
+
const existing = readCacheRecord(deps.supervisorDir as string);
|
|
323
|
+
const identityMatches =
|
|
324
|
+
existing !== null &&
|
|
325
|
+
existing.resolvedPath === resolvedPath &&
|
|
326
|
+
existing.mtimeMs === identity!.mtimeMs &&
|
|
327
|
+
existing.sizeBytes === identity!.sizeBytes;
|
|
328
|
+
if (!identityMatches) {
|
|
329
|
+
writeCacheRecordAtomic(deps.supervisorDir as string, {
|
|
330
|
+
version: 1,
|
|
331
|
+
resolvedPath: resolvedPath as string,
|
|
332
|
+
mtimeMs: identity!.mtimeMs,
|
|
333
|
+
sizeBytes: identity!.sizeBytes,
|
|
334
|
+
probedAt: new Date(now()).toISOString(),
|
|
335
|
+
});
|
|
496
336
|
}
|
|
497
337
|
}
|
|
498
338
|
|
|
499
|
-
const
|
|
500
|
-
|
|
501
|
-
if (verdict === "unknown") {
|
|
502
|
-
const note =
|
|
503
|
-
`vice-broker: could not determine whether ${viceBin} is the stock or fork VICE build -- ` +
|
|
504
|
-
`its --help output matched neither the -mcpserver nor the -binarymonitor discriminator. ` +
|
|
505
|
-
`Set VICE_BACKEND=stock or VICE_BACKEND=fork explicitly.`;
|
|
506
|
-
log(note);
|
|
507
|
-
const result: ResolvedBackendResult = { backend: "fork", source: "indeterminate", ...binPathFields(resolvedPath, viceBin), note };
|
|
508
|
-
memoisedResult = result;
|
|
509
|
-
return result;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
if (cacheEligible) {
|
|
513
|
-
writeCacheRecordAtomic(deps.supervisorDir as string, {
|
|
514
|
-
version: 1,
|
|
515
|
-
resolvedPath: resolvedPath as string,
|
|
516
|
-
mtimeMs: identity!.mtimeMs,
|
|
517
|
-
sizeBytes: identity!.sizeBytes,
|
|
518
|
-
backend: verdict,
|
|
519
|
-
probedAt: new Date(now()).toISOString(),
|
|
520
|
-
});
|
|
521
|
-
}
|
|
522
|
-
|
|
523
|
-
const result: ResolvedBackendResult = { backend: verdict, source: "probe", ...binPathFields(resolvedPath, viceBin) };
|
|
339
|
+
const result: ResolvedBackendResult = { backend: "stock", ...binPathFields(resolvedPath, viceBin) };
|
|
524
340
|
memoisedResult = result;
|
|
525
|
-
emitDetectedNote(result, viceBin, log);
|
|
526
341
|
return result;
|
|
527
342
|
}
|
|
528
343
|
|
|
529
344
|
// ---------------------------------------------------------------------------
|
|
530
345
|
// BACK-04: the capability record. Same cache file, same identity match --
|
|
531
346
|
// filled in by plan 02-08's connect handshake, once per binary, never once
|
|
532
|
-
// per connect. This file's own
|
|
533
|
-
//
|
|
534
|
-
//
|
|
347
|
+
// per connect. This file's own resolution never populates these fields; it
|
|
348
|
+
// only ever reads or updates them against an identity record this file
|
|
349
|
+
// already wrote.
|
|
535
350
|
// ---------------------------------------------------------------------------
|
|
536
351
|
|
|
537
352
|
export interface CapabilityRecordResult {
|
|
@@ -601,15 +416,13 @@ export function readCapabilityRecord(binPath: string, deps: CapabilityDeps = {})
|
|
|
601
416
|
return result;
|
|
602
417
|
}
|
|
603
418
|
|
|
604
|
-
/** Attaches `{ versionQuad, cpuHistoryAvailable }` to the EXISTING
|
|
605
|
-
*
|
|
606
|
-
*
|
|
419
|
+
/** Attaches `{ versionQuad, cpuHistoryAvailable }` to the EXISTING identity
|
|
420
|
+
* record already on file for `binPath`'s resolved identity -- a no-op, never
|
|
421
|
+
* a throw, when there is no such matching record yet (no supervisorDir
|
|
607
422
|
* given, the binary cannot be resolved or stat'd, or the cache names a
|
|
608
|
-
* different binary or has no
|
|
609
|
-
*
|
|
610
|
-
* already wrote
|
|
611
|
-
* quad and this function must not silently fabricate the field it did not
|
|
612
|
-
* observe either. */
|
|
423
|
+
* different binary or has no record at all). This function never invents an
|
|
424
|
+
* identity record of its own: it can only EXTEND a record resolvedBackend()
|
|
425
|
+
* already wrote. */
|
|
613
426
|
export function writeCapabilityRecord(
|
|
614
427
|
binPath: string,
|
|
615
428
|
capability: { versionQuad: string; cpuHistoryAvailable: boolean },
|
|
@@ -633,7 +446,6 @@ export function writeCapabilityRecord(
|
|
|
633
446
|
resolvedPath,
|
|
634
447
|
mtimeMs: identity.mtimeMs,
|
|
635
448
|
sizeBytes: identity.sizeBytes,
|
|
636
|
-
backend: existing.backend,
|
|
637
449
|
probedAt: existing.probedAt,
|
|
638
450
|
versionQuad: capability.versionQuad,
|
|
639
451
|
cpuHistoryAvailable: capability.cpuHistoryAvailable,
|
package/build.ts
CHANGED
|
@@ -48,6 +48,8 @@ export const HOST_BOUND_ARTIFACTS: string[] = [
|
|
|
48
48
|
"broker-epoch.mjs",
|
|
49
49
|
"broker-control.mjs",
|
|
50
50
|
"backend-detect.mjs",
|
|
51
|
+
"host-tool.mjs",
|
|
52
|
+
"ghidra-project.mjs",
|
|
51
53
|
];
|
|
52
54
|
|
|
53
55
|
/** The generated-file banner (01.6-RESEARCH.md §F), a function of the
|
|
@@ -61,7 +63,7 @@ export function GENERATED_BANNER(relSourcePath: string): string {
|
|
|
61
63
|
`// Compiled by \`tsc\` from ${relSourcePath}. Edit the TypeScript source and rebuild;\n` +
|
|
62
64
|
"// changes made directly to this file are silently overwritten by the next build, and are never\n" +
|
|
63
65
|
"// deployed to the host on their own -- install-resources.mjs copies THIS file's on-disk contents\n" +
|
|
64
|
-
"// verbatim to tools/, so an edit made only here reaches the host but is lost on the very next\n" +
|
|
66
|
+
"// verbatim to .c64-re-tools/bin/, so an edit made only here reaches the host but is lost on the very next\n" +
|
|
65
67
|
"// rebuild.\n"
|
|
66
68
|
);
|
|
67
69
|
}
|