@henols/vice-mcp 0.1.11 → 0.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/backend-detect.mts +58 -8
- package/capability-registry.ts +388 -0
- package/disasm-decoder.ts +28 -4
- package/package.json +12 -1
- package/resources/backend-detect.mjs +30 -2
- package/resources/broker-launch.mjs +166 -34
- package/resources/vice-broker.mjs +25 -4
- package/stock-cia.ts +598 -0
- package/stock-connect.ts +137 -20
- package/stock-derived.ts +67 -14
- package/stock-diagnose.ts +994 -0
- package/stock-dispatch.ts +105 -4
- package/stock-handler.ts +29 -0
- package/stock-memory-search.ts +441 -0
- package/stock-memory.ts +92 -13
- package/stock-protocol.ts +328 -0
- package/stock-recycle.ts +500 -0
- package/stock-run-until.ts +400 -0
- package/stock-runstate.ts +46 -2
- package/stock-sprites.ts +712 -0
- package/stock-symbols.ts +431 -0
- package/stock-timing.ts +562 -0
- package/stock-vicii.ts +318 -0
- package/tools-manifest.stock.json +3031 -223
- package/version.ts +279 -0
- package/vice-proxy.ts +49 -6
|
@@ -26,8 +26,9 @@
|
|
|
26
26
|
// deliberately-killed instance, and writes the per-instance boot/crash log
|
|
27
27
|
// D-23 preserves at the exact path shape the retiring bash supervisor used.
|
|
28
28
|
import { spawn as nodeSpawn } from "node:child_process";
|
|
29
|
-
import { mkdirSync, openSync, closeSync, existsSync } from "node:fs";
|
|
29
|
+
import { mkdirSync, mkdtempSync, openSync, closeSync, existsSync } from "node:fs";
|
|
30
30
|
import { join, basename } from "node:path";
|
|
31
|
+
import { tmpdir } from "node:os";
|
|
31
32
|
// Module-level: this file, not the caller, owns the single boolean --
|
|
32
33
|
// synchronous check, synchronous set, released in a finally, with no
|
|
33
34
|
// `await` between the check and the set.
|
|
@@ -124,7 +125,31 @@ export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv,
|
|
|
124
125
|
`reach it; the default of 127.0.0.1 is the safe posture for a host-native install, widen only ` +
|
|
125
126
|
`when the MCP server itself runs in a container that must reach the host emulator\n`);
|
|
126
127
|
}
|
|
127
|
-
|
|
128
|
+
// Audit item I-2 (§4.2, FINDING-C1): a broker-launched stock x64sc used
|
|
129
|
+
// to boot with Drive8Type=0 (NONE) -- nothing answers unit 8, so
|
|
130
|
+
// LOAD"*",8,1 fails ?DEVICE NOT PRESENT ERROR and the entry-point
|
|
131
|
+
// checkpoint never hits. No stock MCP tool can correct this after boot
|
|
132
|
+
// (the 38-tool stock manifest has zero resource-set names by design),
|
|
133
|
+
// so the fix must be a launch-time flag. `-default` MUST be the very
|
|
134
|
+
// first element: it is VICE's reset-to-compiled-in-defaults instruction,
|
|
135
|
+
// not an inert "these are the baselines" no-op, so any flag emitted
|
|
136
|
+
// before it (including -drive8type) is silently clobbered back to its
|
|
137
|
+
// compiled-in value. `-drive8type 1541` therefore has to come
|
|
138
|
+
// immediately after `-default`, and -- per CLAUDE.md's documented
|
|
139
|
+
// constraint -- `-default` also has to come before `-binarymonitor` or
|
|
140
|
+
// the monitor never binds and the subsequent connect hangs in the
|
|
141
|
+
// backlog looking exactly like a wedge. Confirmed sufficient live in
|
|
142
|
+
// Phase 8.1's standalone probe (08.1-WALKTHROUGH-EVIDENCE.md §4):
|
|
143
|
+
// `resourceget "Drive8Type"` moved 0 -> 1541 and a `load` over the text
|
|
144
|
+
// monitor succeeded immediately. Deliberately NOT setting
|
|
145
|
+
// -drive8truedrive / Drive8TrueEmulation here: this build's own default
|
|
146
|
+
// already reads Drive8TrueEmulation=1 (same probe), so 08.2-RESEARCH.md's
|
|
147
|
+
// primary recommendation is that only -drive8type needs adding.
|
|
148
|
+
// Assumption A3 in that doc's Assumptions Log (some other stock build
|
|
149
|
+
// might default Drive8TrueEmulation to 0) is read and deliberately not
|
|
150
|
+
// pre-emptively defended against here; plan 03's live test is what would
|
|
151
|
+
// surface it if that assumption is ever wrong on a different build.
|
|
152
|
+
const args = ["-default", "-drive8type", "1541", "-binarymonitor", "-binarymonitoraddress", `ip4://${host}:${port}`];
|
|
128
153
|
if (typeof remoteMonitorPort === "number") {
|
|
129
154
|
if (host !== "127.0.0.1" && !warnedRemoteMonitorBindWidened) {
|
|
130
155
|
warnedRemoteMonitorBindWidened = true;
|
|
@@ -154,7 +179,7 @@ export function buildViceArgs(port, { backend, mcpHost, binmonHost, viceArgsEnv,
|
|
|
154
179
|
* spawning, so a bad configuration value is visible rather than silently
|
|
155
180
|
* mis-parsed, exactly like the bash launcher's own logging discipline. */
|
|
156
181
|
function spawnAndRecordInstance(reason, port, deps) {
|
|
157
|
-
const spawnFn = deps.spawn ?? ((cmd, args) => nodeSpawn(cmd, args));
|
|
182
|
+
const spawnFn = deps.spawn ?? ((cmd, args, opts) => nodeSpawn(cmd, args, opts));
|
|
158
183
|
const now = deps.now ?? (() => Date.now());
|
|
159
184
|
const viceBin = deps.viceBin ?? process.env.VICE_BIN ?? "x64sc";
|
|
160
185
|
const backend = deps.backend ?? "fork";
|
|
@@ -165,8 +190,52 @@ function spawnAndRecordInstance(reason, port, deps) {
|
|
|
165
190
|
remoteMonitorPort: deps.remoteMonitorPort,
|
|
166
191
|
});
|
|
167
192
|
const log = deps.log ?? defaultLog;
|
|
168
|
-
|
|
169
|
-
|
|
193
|
+
// I-1 rider (audit §4.4, 08.2-02-PLAN.md Task 2): production stock
|
|
194
|
+
// launches used to set no scratch XDG_CONFIG_HOME and would read whatever
|
|
195
|
+
// vicerc the operator's own $HOME already carried -- shared with the
|
|
196
|
+
// operator's own VICE usage and with the fork build. For backend ===
|
|
197
|
+
// "stock" only, compute a fresh, isolated config dir with mkdtempSync
|
|
198
|
+
// (atomic creation, random suffix, 0700 permissions -- the primitive that
|
|
199
|
+
// makes a collision or a symlink-swap into the operator's real config
|
|
200
|
+
// unreachable) and pass it as a third options argument carrying `env`
|
|
201
|
+
// only. Never `shell: true`: the existing array-form spawn(viceBin,
|
|
202
|
+
// viceArgs) call avoids shell interpretation entirely and that property
|
|
203
|
+
// must survive this widening. For backend === "fork", spawnFn is called
|
|
204
|
+
// with NO third argument at all, so the fork path's observable behaviour
|
|
205
|
+
// stays bit-for-bit what it was (BACK-02 is a standing gate and the fork
|
|
206
|
+
// backend has been the sole production backend across all of v0.1.x).
|
|
207
|
+
//
|
|
208
|
+
// Scope boundary (do not remove this note): the production broker daemon
|
|
209
|
+
// always supplies its own deps.spawn / deps.spawnFactory, so the widened
|
|
210
|
+
// default wrapper above is dead code on the real launch paths. This
|
|
211
|
+
// function's job is only to COMPUTE the value at the one seam that should
|
|
212
|
+
// own it; the forwarding to nodeSpawn() happens at four further hops --
|
|
213
|
+
// makeLoggingSpawn() and maintainWarmFloorForRealBroker's inner
|
|
214
|
+
// stashingSpawn in vice-broker.mts, and withCrashSupervision()'s wrapper
|
|
215
|
+
// body and launchSupervised()'s defaultRealSpawn in this file. All four
|
|
216
|
+
// now forward the options argument (plan 08.2-06 closed them in this same
|
|
217
|
+
// phase, with a handleAcquire() composition test that omits
|
|
218
|
+
// buildColdSpawnFactory so an injected stub cannot fake the proof). If you
|
|
219
|
+
// add a fifth spawn hop, it must forward options too, or production stock
|
|
220
|
+
// launches silently lose their config isolation again.
|
|
221
|
+
//
|
|
222
|
+
// Scratch-dir lifetime: this function deliberately does NOT clean the
|
|
223
|
+
// directory up -- the spawned emulator process outlives this function's
|
|
224
|
+
// return and needs the directory for its whole lifetime. Per-launch
|
|
225
|
+
// scratch dirs therefore accumulate under the OS temp dir for the life of
|
|
226
|
+
// the host; this is a recorded trade-off, not an oversight. If reaping
|
|
227
|
+
// them is ever worth doing, the broker's own kill/recycle path is the
|
|
228
|
+
// component that would own it (it already knows when an instance's
|
|
229
|
+
// process has actually exited).
|
|
230
|
+
let spawnOptions;
|
|
231
|
+
let logLine = `vice-broker: launching ${viceBin} ${viceArgs.join(" ")}`;
|
|
232
|
+
if (backend === "stock") {
|
|
233
|
+
const scratchConfigDir = mkdtempSync(join(tmpdir(), "vice-broker-vicerc-"));
|
|
234
|
+
spawnOptions = { env: { ...process.env, XDG_CONFIG_HOME: scratchConfigDir } };
|
|
235
|
+
logLine += ` (XDG_CONFIG_HOME=${scratchConfigDir})`;
|
|
236
|
+
}
|
|
237
|
+
log(logLine);
|
|
238
|
+
const child = spawnOptions === undefined ? spawnFn(viceBin, viceArgs) : spawnFn(viceBin, viceArgs, spawnOptions);
|
|
170
239
|
const record = {
|
|
171
240
|
port,
|
|
172
241
|
url: `http://127.0.0.1:${port}/mcp`,
|
|
@@ -422,6 +491,14 @@ function binmonRequest(commandType, requestId) {
|
|
|
422
491
|
header[10] = commandType;
|
|
423
492
|
return header;
|
|
424
493
|
}
|
|
494
|
+
/** The wire's own "this is not a reply to any request I sent" sentinel
|
|
495
|
+
* (CLAUDE.md's Protocol constraint) -- REGISTER_INFO (0x31) arrives
|
|
496
|
+
* unsolicited at THIS id on every monitor open, and CHECKPOINT_INFO/STOPPED/
|
|
497
|
+
* RESUMED/JAM can too. A response-type byte alone is not enough to
|
|
498
|
+
* distinguish "an event that happens to share a type with a real reply" from
|
|
499
|
+
* an actual reply -- request-id is the only field the wire promises never
|
|
500
|
+
* collides between the two, which is exactly why demux must key on it. */
|
|
501
|
+
const BINMON_UNSOLICITED_REQUEST_ID = 0xffffffff;
|
|
425
502
|
/**
|
|
426
503
|
* WR-01: one PING (0x81) over the binary monitor, requiring a WELL-FORMED 0x81
|
|
427
504
|
* reply -- STX, the expected api_version, response type 0x81, error code 0x00,
|
|
@@ -429,6 +506,23 @@ function binmonRequest(commandType, requestId) {
|
|
|
429
506
|
* here for exactly the reason probeReady()'s own comment gives for the HTTP
|
|
430
507
|
* route: a C64 can accept a connection before it has finished booting.
|
|
431
508
|
*
|
|
509
|
+
* quick task 260818-obc (live-discovered): a NEW binmon connection ALWAYS
|
|
510
|
+
* emits an unsolicited REGISTER_INFO (0x31) frame at request-id 0xffffffff
|
|
511
|
+
* the instant it opens (CLAUDE.md's own Protocol constraint) -- BEFORE this
|
|
512
|
+
* probe's own PING reply ever arrives. The naive "the first 12 bytes ARE the
|
|
513
|
+
* reply" read this code used to do treated that event frame's OWN response-
|
|
514
|
+
* type byte (0x31) as a malformed PING reply and answered `false` forever,
|
|
515
|
+
* live-reproduced against a real crash-respawned stock x64sc: the respawn
|
|
516
|
+
* never left "launching" because THIS probe could never see it as ready,
|
|
517
|
+
* even though the emulator was genuinely up and answering fine underneath.
|
|
518
|
+
* The fix walks frame boundaries using each frame's own body-length field and
|
|
519
|
+
* discards every frame whose request-id is the unsolicited sentinel (or
|
|
520
|
+
* simply is not this probe's own id) rather than assuming the first frame
|
|
521
|
+
* on the wire is the reply -- the same "demux by request-id, never by
|
|
522
|
+
* arrival order" discipline CLAUDE.md's Protocol constraint already requires
|
|
523
|
+
* of every OTHER binmon consumer in this tree (stock-protocol.ts's
|
|
524
|
+
* ViceMonitorClient chief among them).
|
|
525
|
+
*
|
|
432
526
|
* Then EXIT (0xaa), unconditionally, before closing -- because the PING ITSELF
|
|
433
527
|
* HALTS THE MACHINE. Any inbound byte does (docs/phase0-binmon-findings.md §4,
|
|
434
528
|
* and CR-02, which fixed the same omission in the connect handshake). A
|
|
@@ -470,31 +564,52 @@ async function defaultBinmonProbe(port, timeoutMs) {
|
|
|
470
564
|
});
|
|
471
565
|
socket.on("data", (chunk) => {
|
|
472
566
|
buffer = Buffer.concat([buffer, chunk]);
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
buffer
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
567
|
+
// Walk complete frames off the front of the buffer -- never assume the
|
|
568
|
+
// first BINMON_RESPONSE_HEADER_LEN bytes on the wire are this probe's
|
|
569
|
+
// own reply (see this function's own header comment on why an
|
|
570
|
+
// unsolicited event frame can and does arrive first in practice).
|
|
571
|
+
for (;;) {
|
|
572
|
+
if (buffer.length < BINMON_RESPONSE_HEADER_LEN)
|
|
573
|
+
return; // wait for more data
|
|
574
|
+
const bodyLen = buffer.readUInt32LE(2);
|
|
575
|
+
const frameLen = BINMON_RESPONSE_HEADER_LEN + bodyLen;
|
|
576
|
+
if (buffer.length < frameLen)
|
|
577
|
+
return; // header seen, body still incoming
|
|
578
|
+
const responseType = buffer[6];
|
|
579
|
+
const errorCode = buffer[7];
|
|
580
|
+
const requestId = buffer.readUInt32LE(8);
|
|
581
|
+
const stxOk = buffer[0] === BINMON_STX && buffer[1] === BINMON_API_VERSION;
|
|
582
|
+
if (!stxOk) {
|
|
583
|
+
finish(false);
|
|
584
|
+
return;
|
|
585
|
+
}
|
|
586
|
+
if (requestId === BINMON_UNSOLICITED_REQUEST_ID || requestId !== BINMON_PROBE_REQUEST_ID) {
|
|
587
|
+
// Not a reply to anything this probe sent (an unsolicited event, or
|
|
588
|
+
// a stale reply to a previous probe's own request id) -- discard
|
|
589
|
+
// this one frame only and keep walking the rest of the buffer.
|
|
590
|
+
buffer = buffer.subarray(frameLen);
|
|
591
|
+
continue;
|
|
592
|
+
}
|
|
593
|
+
if (responseType !== BINMON_CMD_PING || errorCode !== 0x00) {
|
|
594
|
+
finish(false);
|
|
595
|
+
return;
|
|
596
|
+
}
|
|
597
|
+
// Resume the machine this probe's own PING halted, then close GRACEFULLY:
|
|
598
|
+
// socket.end(data, cb) writes the EXIT and then sends FIN, so the bytes
|
|
599
|
+
// are delivered before the connection goes away. A bare write() followed
|
|
600
|
+
// by destroy() can discard them (destroy may RST), which would leave the
|
|
601
|
+
// instance "ready" and frozen -- the exact outcome the EXIT exists to
|
|
602
|
+
// prevent. The resume is best-effort in its OUTCOME, though: a failed
|
|
603
|
+
// resume must not turn a READY instance into a not-ready one, since the
|
|
604
|
+
// emulator demonstrably answered, which is what this function reports on.
|
|
605
|
+
try {
|
|
606
|
+
socket.end(binmonRequest(BINMON_CMD_EXIT, BINMON_PROBE_REQUEST_ID + 1), () => finish(true));
|
|
607
|
+
}
|
|
608
|
+
catch {
|
|
609
|
+
finish(true);
|
|
610
|
+
}
|
|
482
611
|
return;
|
|
483
612
|
}
|
|
484
|
-
// Resume the machine this probe's own PING halted, then close GRACEFULLY:
|
|
485
|
-
// socket.end(data, cb) writes the EXIT and then sends FIN, so the bytes
|
|
486
|
-
// are delivered before the connection goes away. A bare write() followed
|
|
487
|
-
// by destroy() can discard them (destroy may RST), which would leave the
|
|
488
|
-
// instance "ready" and frozen -- the exact outcome the EXIT exists to
|
|
489
|
-
// prevent. The resume is best-effort in its OUTCOME, though: a failed
|
|
490
|
-
// resume must not turn a READY instance into a not-ready one, since the
|
|
491
|
-
// emulator demonstrably answered, which is what this function reports on.
|
|
492
|
-
try {
|
|
493
|
-
socket.end(binmonRequest(BINMON_CMD_EXIT, BINMON_PROBE_REQUEST_ID + 1), () => finish(true));
|
|
494
|
-
}
|
|
495
|
-
catch {
|
|
496
|
-
finish(true);
|
|
497
|
-
}
|
|
498
613
|
});
|
|
499
614
|
});
|
|
500
615
|
}
|
|
@@ -849,8 +964,11 @@ async function handleExit(reason, port, deps) {
|
|
|
849
964
|
}
|
|
850
965
|
/** The single exit-listener installation point in the whole module tree.
|
|
851
966
|
* Wraps `baseSpawn` (a plain spawn function of the same shape
|
|
852
|
-
* `(command, args) => ChildProcess` every launch path already
|
|
853
|
-
* through
|
|
967
|
+
* `(command, args, options?) => ChildProcess` every launch path already
|
|
968
|
+
* threads through -- the third `options` argument is load-bearing: it
|
|
969
|
+
* carries the scratch XDG_CONFIG_HOME that isolates a stock launch from the
|
|
970
|
+
* operator's real vicerc, and this wrapper MUST forward it) so the returned
|
|
971
|
+
* spawn function, when called, attaches a
|
|
854
972
|
* one-shot "exit" listener that drives handleExit() above -- the SAME
|
|
855
973
|
* respawn/give-up/deliberate-teardown resolution launchSupervised()'s own
|
|
856
974
|
* relaunch path already uses. Returns the child object baseSpawn produced,
|
|
@@ -866,8 +984,13 @@ async function handleExit(reason, port, deps) {
|
|
|
866
984
|
* a second inline listener, is what keeps the "exactly one installation
|
|
867
985
|
* point" invariant a structural gate (broker-launch.test.ts) can hold. */
|
|
868
986
|
export function withCrashSupervision(reason, port, baseSpawn, deps) {
|
|
869
|
-
|
|
870
|
-
|
|
987
|
+
// I-1 rider (08.2-06-PLAN.md, Task 1): forwards a third options argument
|
|
988
|
+
// in the BODY, not just the type -- this is the hop that matters most,
|
|
989
|
+
// because it wraps every real launch path (cold acquire, warm floor, and
|
|
990
|
+
// every respawn). A type-only widening would still silently drop a
|
|
991
|
+
// caller's options at this call site.
|
|
992
|
+
return (cmd, args, options) => {
|
|
993
|
+
const child = baseSpawn(cmd, args, options);
|
|
871
994
|
child.once("exit", () => {
|
|
872
995
|
void handleExit(reason, port, deps);
|
|
873
996
|
});
|
|
@@ -914,9 +1037,18 @@ function launchSupervised(reason, port, deps, crashTimes, backoffMs, remoteMonit
|
|
|
914
1037
|
const logFileName = `${basename(viceBin)}-${Date.now()}-e${epoch}.log`;
|
|
915
1038
|
const logPath = join(logDir, logFileName);
|
|
916
1039
|
const logRelPath = `logs/${logFileName}`;
|
|
917
|
-
|
|
1040
|
+
// I-1 rider (08.2-06-PLAN.md, Task 1): forwards a third options argument
|
|
1041
|
+
// and MERGES it with the per-instance log stdio -- caller options
|
|
1042
|
+
// spread FIRST, `stdio` set LAST, so the per-instance log fd always
|
|
1043
|
+
// wins. Never the other order: a caller-supplied `stdio` would silently
|
|
1044
|
+
// redirect a crash-respawn's output away from the log file the epoch
|
|
1045
|
+
// record names, and the forensic per-instance log (D-23) would point at
|
|
1046
|
+
// a file that received nothing. Without this fix, a stock instance that
|
|
1047
|
+
// crashes and respawns comes back reading the operator's real `vicerc`
|
|
1048
|
+
// even though its original launch was isolated.
|
|
1049
|
+
const defaultRealSpawn = (cmd, args, options) => {
|
|
918
1050
|
const fd = openSync(logPath, "a");
|
|
919
|
-
return nodeSpawn(cmd, args, { stdio: ["ignore", fd, fd] });
|
|
1051
|
+
return nodeSpawn(cmd, args, { ...options, stdio: ["ignore", fd, fd] });
|
|
920
1052
|
};
|
|
921
1053
|
const baseSpawn = deps.spawnFactory ? deps.spawnFactory(port) : (deps.spawn ?? defaultRealSpawn);
|
|
922
1054
|
const wrappedSpawn = withCrashSupervision(reason, port, baseSpawn, deps);
|
|
@@ -184,14 +184,22 @@ function writeBrokerRecordFile(stateDir, record) {
|
|
|
184
184
|
* closure and the log's path relative to supervisorDir (the epoch
|
|
185
185
|
* record's own `log` field). Shared by both launch paths -- a cold
|
|
186
186
|
* acquire and warm-floor maintenance -- so there is exactly one place that
|
|
187
|
-
* opens a launch log fd.
|
|
187
|
+
* opens a launch log fd.
|
|
188
|
+
*
|
|
189
|
+
* I-1 rider (08.2-06-PLAN.md, Task 2): the returned `spawn` now also
|
|
190
|
+
* forwards a caller options object (audit item I-1), MERGING it into the
|
|
191
|
+
* object handed to nodeSpawn() -- caller options spread FIRST, `stdio` set
|
|
192
|
+
* LAST, so the launch log fd always wins over any caller-supplied `stdio`.
|
|
193
|
+
* Merging in the other order would silently redirect a launch's output
|
|
194
|
+
* away from the per-instance log file the epoch record names, breaking
|
|
195
|
+
* D-23's forensic logs while appearing to work. */
|
|
188
196
|
function makeLoggingSpawn(logDir) {
|
|
189
197
|
mkdirSync(logDir, { recursive: true });
|
|
190
198
|
const viceBinForLog = basename(process.env.VICE_BIN ?? "x64sc");
|
|
191
199
|
const logName = `${viceBinForLog}-${Date.now()}.log`;
|
|
192
200
|
const logFd = openSync(join(logDir, logName), "a");
|
|
193
201
|
return {
|
|
194
|
-
spawn: (cmd, cmdArgs) => nodeSpawn(cmd, cmdArgs, { stdio: ["ignore", logFd, logFd] }),
|
|
202
|
+
spawn: (cmd, cmdArgs, options) => nodeSpawn(cmd, cmdArgs, { ...options, stdio: ["ignore", logFd, logFd] }),
|
|
195
203
|
logRelPath: `logs/${logName}`,
|
|
196
204
|
};
|
|
197
205
|
}
|
|
@@ -684,8 +692,12 @@ function maintainWarmFloorForRealBroker(stateDir, state, backend) {
|
|
|
684
692
|
spawnFactory: (port) => {
|
|
685
693
|
const supervisorDir = join(stateDir, String(port));
|
|
686
694
|
const { spawn, logRelPath } = makeLoggingSpawn(join(supervisorDir, "logs"));
|
|
687
|
-
|
|
688
|
-
|
|
695
|
+
// I-1 rider (08.2-06-PLAN.md, Task 2): forwards a third options
|
|
696
|
+
// argument -- this is a SECOND, independent dropper on the
|
|
697
|
+
// warm-floor arm; fixing only makeLoggingSpawn above would leave
|
|
698
|
+
// this arm's own scratch XDG_CONFIG_HOME dropped right here.
|
|
699
|
+
const stashingSpawn = (cmd, args, options) => {
|
|
700
|
+
const child = spawn(cmd, args, options);
|
|
689
701
|
// Stash the log path where onLaunched (fired synchronously right
|
|
690
702
|
// after this returns, still within the SAME maintainWarmFloor()
|
|
691
703
|
// call -- at most one launch per call, per the serialised-warming
|
|
@@ -720,6 +732,15 @@ function maintainWarmFloorForRealBroker(stateDir, state, backend) {
|
|
|
720
732
|
log: (line) => process.stderr.write(`${line}\n`),
|
|
721
733
|
});
|
|
722
734
|
}
|
|
735
|
+
/** Exported ONLY so a test can drive the warm-floor arm's REAL spawn
|
|
736
|
+
* composition (this function's own makeLoggingSpawn()+stashingSpawn+
|
|
737
|
+
* withCrashSupervision() closure above) through the built artifact, the
|
|
738
|
+
* same escape-hatch pattern `_superviseDepsFor` already establishes for the
|
|
739
|
+
* respawn composition -- see vice-broker-acquire.test.ts's I-1 composition
|
|
740
|
+
* tests (08.2-06-PLAN.md, Task 3), which call this directly with no spawn
|
|
741
|
+
* override so the warm floor's own independent `stashingSpawn` dropper
|
|
742
|
+
* cannot hide behind an injected stub. */
|
|
743
|
+
export const _maintainWarmFloorForRealBroker = maintainWarmFloorForRealBroker;
|
|
723
744
|
/** Releases a grant and identity-verified-kills its instance -- but ONLY
|
|
724
745
|
* when the port's CURRENT occupant is proven to be the SAME process this
|
|
725
746
|
* grant was actually issued for (its own recorded `pid`, set at grant time
|