@volter/world-runtime 3.0.163 → 3.0.164
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/dist/src/ancestry.js +2 -2
- package/dist/src/app-url.d.ts +5 -1
- package/dist/src/app-url.js +14 -1
- package/dist/src/branch.js +28 -8
- package/dist/src/browser-files.d.ts +5 -0
- package/dist/src/browser-files.js +14 -0
- package/dist/src/browsers.d.ts +2 -3
- package/dist/src/browsers.js +4 -4
- package/dist/src/catalog.d.ts +2 -1
- package/dist/src/catalog.js +3 -2
- package/dist/src/changeset.js +1 -1
- package/dist/src/cli-commands.js +71 -18
- package/dist/src/cli-load-clock.d.ts +1 -0
- package/dist/src/cli-load-clock.js +2 -0
- package/dist/src/cli.js +5 -1
- package/dist/src/clickhouse-backing.d.ts +2 -0
- package/dist/src/clickhouse-backing.js +145 -0
- package/dist/src/command-lifetime.d.mts +1 -1
- package/dist/src/command-lifetime.mjs +5 -2
- package/dist/src/configs.js +1 -1
- package/dist/src/consumers.js +1 -1
- package/dist/src/covers.d.ts +4 -2
- package/dist/src/covers.js +8 -4
- package/dist/src/go-mysql-binding.d.ts +19 -0
- package/dist/src/go-mysql-binding.js +51 -0
- package/dist/src/guest-processes.d.mts +1 -1
- package/dist/src/guest-processes.mjs +22 -14
- package/dist/src/host-cli.js +8 -1
- package/dist/src/host-ready.d.ts +16 -2
- package/dist/src/host-ready.js +66 -39
- package/dist/src/host-worker.js +7 -1
- package/dist/src/host.d.ts +26 -0
- package/dist/src/host.js +93 -3
- package/dist/src/infra-definition.d.ts +35 -0
- package/dist/src/infra-definition.js +146 -0
- package/dist/src/infra.d.ts +3 -0
- package/dist/src/infra.js +375 -105
- package/dist/src/init.d.ts +46 -0
- package/dist/src/init.js +445 -18
- package/dist/src/lifecycle-record.d.ts +5 -1
- package/dist/src/lifecycle-record.js +27 -4
- package/dist/src/local-branches.js +1 -1
- package/dist/src/mariadb-bootstrap.d.ts +32 -0
- package/dist/src/mariadb-bootstrap.js +90 -0
- package/dist/src/mariadb-supplier.d.ts +23 -0
- package/dist/src/mariadb-supplier.js +215 -0
- package/dist/src/mongodb-backing.d.ts +4 -0
- package/dist/src/mongodb-backing.js +71 -1
- package/dist/src/native-mariadb-backing.d.ts +13 -0
- package/dist/src/native-mariadb-backing.js +512 -0
- package/dist/src/native-postgres-backing.d.ts +4 -10
- package/dist/src/native-postgres-backing.js +428 -43
- package/dist/src/native-server-inputs.d.ts +16 -0
- package/dist/src/native-server-inputs.js +204 -0
- package/dist/src/origin.js +3 -3
- package/dist/src/original-inputs.d.ts +14 -0
- package/dist/src/original-inputs.js +84 -0
- package/dist/src/pack-facts.d.ts +9 -1
- package/dist/src/pglite-backing.d.ts +41 -17
- package/dist/src/pglite-backing.js +213 -79
- package/dist/src/pglite-host.mjs +482 -10
- package/dist/src/process-groups.d.mts +6 -1
- package/dist/src/process-groups.d.ts +2 -1
- package/dist/src/process-groups.js +1 -1
- package/dist/src/process-groups.mjs +82 -15
- package/dist/src/process-signal.d.mts +2 -2
- package/dist/src/process-signal.mjs +29 -9
- package/dist/src/project-inspect.d.ts +12 -0
- package/dist/src/project-inspect.js +41 -4
- package/dist/src/proxy-env.d.ts +34 -0
- package/dist/src/proxy-env.js +114 -0
- package/dist/src/redirect-proxy.d.ts +1 -31
- package/dist/src/redirect-proxy.js +3 -112
- package/dist/src/redis-backing.d.ts +6 -4
- package/dist/src/redis-backing.js +20 -168
- package/dist/src/runtime.d.ts +1 -0
- package/dist/src/runtime.js +503 -218
- package/dist/src/schema.d.ts +1 -0
- package/dist/src/schema.js +5 -3
- package/dist/src/served-world.js +3 -3
- package/dist/src/twin-backing.d.ts +47 -0
- package/dist/src/twin-backing.js +343 -0
- package/dist/src/up-cli.js +56 -3
- package/package.json +3 -3
- package/src/ancestry.ts +2 -2
- package/src/app-url.ts +9 -2
- package/src/branch.ts +26 -8
- package/src/browser-files.ts +15 -0
- package/src/browsers.ts +4 -4
- package/src/catalog.ts +4 -3
- package/src/changeset.ts +1 -1
- package/src/cli-commands.ts +85 -18
- package/src/cli-load-clock.ts +2 -0
- package/src/cli.ts +5 -1
- package/src/clickhouse-backing.ts +70 -0
- package/src/command-lifetime.d.mts +1 -1
- package/src/command-lifetime.mjs +5 -2
- package/src/configs.ts +1 -1
- package/src/consumers.ts +1 -1
- package/src/covers.ts +9 -4
- package/src/go-mysql-binding.ts +49 -0
- package/src/guest-processes.d.mts +1 -1
- package/src/guest-processes.mjs +22 -14
- package/src/host-cli.ts +7 -1
- package/src/host-ready.ts +62 -31
- package/src/host-worker.ts +6 -2
- package/src/host.ts +88 -6
- package/src/infra-definition.ts +135 -0
- package/src/infra.ts +250 -45
- package/src/init.ts +416 -17
- package/src/lifecycle-record.ts +18 -4
- package/src/local-branches.ts +1 -1
- package/src/mariadb-bootstrap.ts +107 -0
- package/src/mariadb-supplier.ts +199 -0
- package/src/mongodb-backing.ts +59 -1
- package/src/native-mariadb-backing.ts +418 -0
- package/src/native-postgres-backing.ts +353 -41
- package/src/native-server-inputs.ts +169 -0
- package/src/origin.ts +3 -3
- package/src/original-inputs.ts +72 -0
- package/src/pack-facts.ts +1 -1
- package/src/pglite-backing.ts +192 -80
- package/src/pglite-host.mjs +482 -10
- package/src/process-groups.d.mts +6 -1
- package/src/process-groups.mjs +82 -15
- package/src/process-groups.ts +2 -1
- package/src/process-signal.d.mts +2 -2
- package/src/process-signal.mjs +29 -9
- package/src/project-inspect.ts +37 -4
- package/src/proxy-env.ts +128 -0
- package/src/redirect-proxy.ts +3 -125
- package/src/redis-backing.ts +19 -136
- package/src/runtime.ts +342 -107
- package/src/schema.ts +5 -4
- package/src/served-world.ts +3 -3
- package/src/twin-backing.ts +300 -0
- package/src/up-cli.ts +52 -3
package/dist/src/runtime.js
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
import { errorMessage } from "./error-message.js";
|
|
2
|
-
import {
|
|
2
|
+
import { materializeWorldBrowserFiles } from "./browser-files.js";
|
|
3
3
|
import { claimEnvOutput, publishEnvOutput } from "./env-output.js";
|
|
4
4
|
import { offSignal } from "./signals.js";
|
|
5
5
|
import { parallelStartup } from "./startup.js";
|
|
6
6
|
import { waitForHostReady } from "./host-ready.js";
|
|
7
7
|
import { assertWorldStateRemovable, removeWorldState } from "./ancestry.js";
|
|
8
8
|
import { packCli, packEntry } from "./catalog.js";
|
|
9
|
-
import { vendorOfPackModule } from '@volter/world-core/runtime';
|
|
9
|
+
import { vendorOfPackModule } from '@volter/world-core/runtime-state';
|
|
10
10
|
import { spawn, spawnSync } from 'node:child_process';
|
|
11
11
|
import { get as httpGet } from 'node:http';
|
|
12
12
|
import { siblingScript } from "./sibling.js";
|
|
13
13
|
import { commandLifetime, commandProcessGroup, CommandRetirementError } from "./command-lifetime.js";
|
|
14
|
-
import {
|
|
15
|
-
import { processStarts, survivingOwnedGroups } from "./process-groups.js";
|
|
14
|
+
import { processStarts, survivingOwnedGroups, setWorldEventsFile, worldEvent } from "./process-groups.js";
|
|
16
15
|
import { spawnIdentity, signalOwnedProcess, signalChild, signalHeldChild, processBirth, childIdentity } from './process-signal.mjs';
|
|
17
16
|
import { readGuestProcesses, releaseGuestProcesses } from './guest-processes.mjs';
|
|
18
17
|
import { randomUUID, createHash } from 'node:crypto';
|
|
@@ -25,10 +24,10 @@ import { basename, delimiter, dirname, join, relative, resolve, sep } from 'node
|
|
|
25
24
|
import { packFacts } from "./pack-facts.js";
|
|
26
25
|
import { assertOwnedRealPath, ownedPath } from "./owned-path.js";
|
|
27
26
|
import { isEnvironmentName } from "./schema.js";
|
|
28
|
-
import { infraConnections, parseInfraDefinition } from "./
|
|
27
|
+
import { infraConnections, parseInfraDefinition } from "./infra-definition.js";
|
|
29
28
|
import { describeServiceEnd, readServiceEnd } from "./service-exit.js";
|
|
30
29
|
import { Socket } from 'node:net';
|
|
31
|
-
import { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, getActiveWorldStore, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core/runtime';
|
|
30
|
+
import { beginPlaceholderPull, endPlaceholderPull, placeholderPullActive, getActiveWorldStore, removeCarriedLocks, withFileLock, stateDirName, PROTOCOL_VERSION, PROTOCOL_MAJOR, protocolStanding, WORLD_BOOT_PATH, WORLD_DATA_ENV, WORLD_ENV_NAMES_ENV } from '@volter/world-core/runtime-state';
|
|
32
31
|
import { loadWorldConfig } from "./configs.js";
|
|
33
32
|
import { createWorldNetworkPolicy, parseWorldNetworkPolicy, normalizeWorldNetwork, intersectWorldNetwork, WORLD_NETWORK_POLICY_ENV } from '@volter/world-core/network-policy';
|
|
34
33
|
import { overlayPackFactsFile, resolveServicePackage } from "./catalog.js";
|
|
@@ -36,10 +35,10 @@ import { fakeEnvValue } from "./fixture-env.js";
|
|
|
36
35
|
import { APP_URL_ENV, ISSUE_ENV_PREFIX, MINT_ENV } from "./schema.js";
|
|
37
36
|
import { readAppUrl } from "./app-url.js";
|
|
38
37
|
import { inertInjectEnvWarnings } from "./inject-map.js";
|
|
39
|
-
import { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, proxyEnvFor,
|
|
38
|
+
import { activeVendorMap, ensureCa, hostHasRawEgress, opensslAvailable, proxyEnvFor, tearDownCa } from "./proxy-env.js";
|
|
40
39
|
import { CaTrustError, sessionTrustEnv } from "./ca-trust.js";
|
|
41
40
|
import { checkedOwner, CONSUMER_HEARTBEAT_MS, createConsumer, refreshConsumer, releaseConsumer, removeConsumerRecord, worldConsumers, consumerProcessState } from "./consumers.js";
|
|
42
|
-
import { assertLifecycleStartable, beginWorldLifecycle, handoffWorldLifecycle, recordWorldLifecycleEvent, releaseWorldLifecycle, retainWorldLifecycleForCleanup, hasWorldLifecycle, isWorldLifecycleRunning, lifecycleLogPath, releaseAbandonedWorldLifecycle, exitedWorldLifecyclePids } from "./lifecycle-record.js";
|
|
41
|
+
import { assertLifecycleStartable, beginWorldLifecycle, handoffWorldLifecycle, recordWorldLifecycleEvent, releaseWorldLifecycle, retainWorldLifecycleForCleanup, hasWorldLifecycle, isWorldLifecycleRunning, describeWorldLifecycle, lifecycleLogPath, releaseAbandonedWorldLifecycle, exitedWorldLifecyclePids } from "./lifecycle-record.js";
|
|
43
42
|
// Births captured at actual spawn; retries never remeasure a retained PID.
|
|
44
43
|
const spawnedIdentities = new Map();
|
|
45
44
|
const spawnedChildren = new Map();
|
|
@@ -130,13 +129,17 @@ async function confirmOwnPort(port, boot, ended, twin, what, log) {
|
|
|
130
129
|
if (answered && typeof answered.boot === 'string') {
|
|
131
130
|
if (answered.boot === boot.bootId)
|
|
132
131
|
return;
|
|
133
|
-
throw
|
|
132
|
+
throw foreignBoot(what, answered.boot, log);
|
|
134
133
|
}
|
|
135
134
|
// Whether the service has already ended, read now: one turn of the event loop, not a wait for it to fail.
|
|
136
135
|
const endedNow = ended ? await Promise.race([ended.then(() => true), new Promise((resolveNow) => setImmediate(() => resolveNow(false)))]) : false;
|
|
137
136
|
if (endedNow)
|
|
138
137
|
throw new Error(`${what} ended right after its port first answered, so another process probably holds the port: the service ${endedAccount(log)}\nLog: ${log}`);
|
|
139
138
|
}
|
|
139
|
+
/** A port answered by a twin of another World boot: a leftover World holds it. */
|
|
140
|
+
function foreignBoot(what, answered, log) {
|
|
141
|
+
return new Error(`${what}: its port is answered by a twin of another World boot (${answered}), not this one; a leftover World holds the port. Stop it (\`volter-world list\`, then \`down\`) and retry.\nLog: ${log}`);
|
|
142
|
+
}
|
|
140
143
|
/** What the server on a loopback port says its World boot is (WORLD_BOOT_PATH), or null. A direct loopback request:
|
|
141
144
|
* never through a proxy an activated shell's environment names. */
|
|
142
145
|
function worldBootOf(port, abort) {
|
|
@@ -806,17 +809,17 @@ async function waitForTcp(port, timeoutMs = 15_000, abort) {
|
|
|
806
809
|
}
|
|
807
810
|
throw new Error(`Service on port ${port} did not become reachable`);
|
|
808
811
|
}
|
|
809
|
-
function readPids(path) {
|
|
812
|
+
function readPids(path, receipts = readGuestProcesses(dirname(path))) {
|
|
810
813
|
const held = getActiveWorldStore().read(path);
|
|
811
|
-
const guests =
|
|
814
|
+
const guests = receipts.map(receipt => receipt.record.pid);
|
|
812
815
|
return [...new Set([...(held ?? '')
|
|
813
816
|
.split(/\r?\n/)
|
|
814
817
|
.map((line) => Number(line.trim()))
|
|
815
818
|
.filter((pid) => Number.isInteger(pid) && pid > 0), ...guests])];
|
|
816
819
|
}
|
|
817
|
-
function livePids(pids) {
|
|
820
|
+
function livePids(pids, listing) {
|
|
818
821
|
// A recorded leader may have exited while its detached descendants still run.
|
|
819
|
-
return survivingOwnedGroups(pids);
|
|
822
|
+
return survivingOwnedGroups(pids, listing);
|
|
820
823
|
}
|
|
821
824
|
function tunnelPids(instance) {
|
|
822
825
|
return Object.values(instance.services)
|
|
@@ -853,7 +856,7 @@ function writeOwnedPids(root, name, pids, fresh = [], births = heldStarts()) {
|
|
|
853
856
|
function identityPath(root, name) {
|
|
854
857
|
return join(instanceDir(root, name), 'pids.identity');
|
|
855
858
|
}
|
|
856
|
-
function readIdentities(root, name) {
|
|
859
|
+
function readIdentities(root, name, receipts = readGuestProcesses(instanceDir(root, name)), onConflict) {
|
|
857
860
|
let known = {};
|
|
858
861
|
try {
|
|
859
862
|
const value = JSON.parse(getActiveWorldStore().read(identityPath(root, name)) ?? '{}');
|
|
@@ -862,13 +865,18 @@ function readIdentities(root, name) {
|
|
|
862
865
|
}
|
|
863
866
|
catch { /* Guest receipts below remain independently authoritative. */ }
|
|
864
867
|
const births = new Map();
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
+
const conflicts = new Set();
|
|
869
|
+
for (const { record } of receipts) {
|
|
870
|
+
if (births.has(record.pid) && births.get(record.pid) !== record.start) {
|
|
871
|
+
if (!onConflict)
|
|
872
|
+
throw new Error('Conflicting guest spawn identities; ownership retained');
|
|
873
|
+
conflicts.add(record.pid);
|
|
874
|
+
onConflict(record.pid);
|
|
875
|
+
}
|
|
868
876
|
births.set(record.pid, record.start);
|
|
869
877
|
}
|
|
870
878
|
for (const [pid, start] of births) {
|
|
871
|
-
if (start)
|
|
879
|
+
if (start && !conflicts.has(pid))
|
|
872
880
|
known[pid] = start;
|
|
873
881
|
else
|
|
874
882
|
delete known[pid];
|
|
@@ -1084,9 +1092,9 @@ async function runExternalCommand(command, cwd, serviceId, phase, env = process.
|
|
|
1084
1092
|
/** Run an external service's `up` command, STREAMING its stdout+stderr to the service log as it runs
|
|
1085
1093
|
* (instead of buffering through spawnSync's 1MB cap, which a large DB restore would overflow). Returns
|
|
1086
1094
|
* the combined output so `discover` can read it. Throws with a bounded tail on nonzero exit. */
|
|
1087
|
-
async function runExternalUp(command, cwd, serviceId, env, logPath, abort, progress) {
|
|
1095
|
+
async function runExternalUp(command, cwd, serviceId, env, logPath, abort, progress, verified) {
|
|
1088
1096
|
if (ownInfrastructureUp(command))
|
|
1089
|
-
return runOwnInfrastructureUp(command, serviceId, env, logPath, abort, progress);
|
|
1097
|
+
return runOwnInfrastructureUp(command, serviceId, env, logPath, abort, progress, verified);
|
|
1090
1098
|
const [bin, ...args] = ownCommand(command);
|
|
1091
1099
|
if (!bin)
|
|
1092
1100
|
throw new Error(`External service "${serviceId}": up command is empty`);
|
|
@@ -1176,13 +1184,14 @@ async function runOwnInfrastructureStatus(serviceId, phase, env, abort, timeoutM
|
|
|
1176
1184
|
abort?.removeEventListener('abort', onAbort);
|
|
1177
1185
|
}
|
|
1178
1186
|
}
|
|
1179
|
-
async function runOwnInfrastructureUp(command, serviceId, env, logPath, abort, progress) {
|
|
1187
|
+
async function runOwnInfrastructureUp(command, serviceId, env, logPath, abort, progress, verified) {
|
|
1180
1188
|
writeFileSync(logPath, `$ ${command.join(' ')}\n`, { mode: 0o600 });
|
|
1181
1189
|
chmodSync(logPath, 0o600);
|
|
1182
1190
|
const out = openPrivateLog(logPath, 'a');
|
|
1183
1191
|
const chunks = [];
|
|
1184
1192
|
try {
|
|
1185
1193
|
abort?.throwIfAborted();
|
|
1194
|
+
const importing = Date.now();
|
|
1186
1195
|
const { runInfrastructure } = await import("./infra.js");
|
|
1187
1196
|
// Its phase lines (backing chosen, host spawned, serving) are the boot's progress too.
|
|
1188
1197
|
const write = (text) => {
|
|
@@ -1192,7 +1201,9 @@ async function runOwnInfrastructureUp(command, serviceId, env, logPath, abort, p
|
|
|
1192
1201
|
if (line.startsWith('managed infrastructure'))
|
|
1193
1202
|
progress?.(`${serviceId}: ${line}`);
|
|
1194
1203
|
};
|
|
1195
|
-
|
|
1204
|
+
// Its module's load, said with the line's own time (S2, Twenty's World up): what of the start before the backing's line was this.
|
|
1205
|
+
write(`managed infrastructure: its module loaded in ${Date.now() - importing} ms\n`);
|
|
1206
|
+
const code = await runInfrastructure(command[1], env, { out: write, err: write, ...(verified ? { verified: (status) => { verified.status = status; } } : {}) }, abort);
|
|
1196
1207
|
abort?.throwIfAborted();
|
|
1197
1208
|
if (code !== 0) {
|
|
1198
1209
|
const detail = chunks.join('').trim().split('\n').slice(-40).join('\n');
|
|
@@ -1216,7 +1227,7 @@ function resolveReadyProbe(ready, svc) {
|
|
|
1216
1227
|
.split('${port}').join(String(svc.port));
|
|
1217
1228
|
return { ...ready, httpUrl };
|
|
1218
1229
|
}
|
|
1219
|
-
async function awaitReadiness(ready, cwd, serviceId, env = process.env, abort) {
|
|
1230
|
+
async function awaitReadiness(ready, cwd, serviceId, env = process.env, abort, passing) {
|
|
1220
1231
|
if (!ready)
|
|
1221
1232
|
return 'no readiness declared';
|
|
1222
1233
|
const timeoutMs = ready.timeoutMs ?? 60_000;
|
|
@@ -1232,7 +1243,9 @@ async function awaitReadiness(ready, cwd, serviceId, env = process.env, abort) {
|
|
|
1232
1243
|
throw new Error(`Service "${serviceId}": readiness command \`${ready.command}\` not found on PATH`);
|
|
1233
1244
|
}
|
|
1234
1245
|
try {
|
|
1235
|
-
await runExternalCommand([ready.command, ...(ready.args ?? [])], cwd, serviceId, 'readiness', env, abort, Math.max(1, timeoutMs - (Date.now() - started)));
|
|
1246
|
+
const output = await runExternalCommand([ready.command, ...(ready.args ?? [])], cwd, serviceId, 'readiness', env, abort, Math.max(1, timeoutMs - (Date.now() - started)));
|
|
1247
|
+
if (passing)
|
|
1248
|
+
Object.assign(passing, output);
|
|
1236
1249
|
return passed(`\`${[ready.command, ...(ready.args ?? [])].join(' ')}\` exited 0`);
|
|
1237
1250
|
}
|
|
1238
1251
|
catch (error) {
|
|
@@ -1289,15 +1302,39 @@ async function startExternalService(service, paths, worldEnv, stripEnv, boot) {
|
|
|
1289
1302
|
// Publish the declared teardown before an external command can create state.
|
|
1290
1303
|
boot?.external({ id: service.id, type: 'external', command: [...external.up], pid: 0, log, env: {},
|
|
1291
1304
|
external: { down: [...external.down], cwd, discoveredEnv: { ...(service.env ?? {}) } } });
|
|
1292
|
-
const
|
|
1305
|
+
const verified = {};
|
|
1306
|
+
const upOutput = await runExternalUp(external.up, cwd, service.id, env, log, boot?.signal, boot?.progress, verified);
|
|
1293
1307
|
boot?.progress(`${service.id}: up returned`);
|
|
1294
1308
|
// 2. readiness — Docker-backed externals take seconds to boot.
|
|
1295
|
-
const
|
|
1309
|
+
const passing = {};
|
|
1310
|
+
// The runtime's own infrastructure, every service twin-served or its PostgreSQL native (no MongoDB native), proves in
|
|
1311
|
+
// its `up` what its `status` would, status's proofs asked again at hand-over though not its choice of backing (infra.ts
|
|
1312
|
+
// handOver: twin-backing.ts twinBackingHeld, each port
|
|
1313
|
+
// served by the child it started; native-postgres-backing.ts nativePostgresStatus, the postmaster's ownership, live
|
|
1314
|
+
// group, identity and answering port; Cal's status run took 70 ms, c670b965 run 2, on the mini, in a browser tab; S2
|
|
1315
|
+
// R46). Its up hands over the status it would print, so the probe,
|
|
1316
|
+
// when it is that same `status`, is not run again (LibreChat: the status run took 206 ms before it connected, e946aa4c
|
|
1317
|
+
// lap 060250 run 2, on the mini, in a browser tab; S2 R45). Any other probe, or a backing up does not prove, is
|
|
1318
|
+
// probed as before.
|
|
1319
|
+
const probed = external.readyWhen?.command === undefined ? undefined : [external.readyWhen.command, ...(external.readyWhen.args ?? [])];
|
|
1320
|
+
const upProved = verified.status !== undefined && probed !== undefined && ownInfrastructureStatus(probed);
|
|
1321
|
+
if (upProved) {
|
|
1322
|
+
passing.stdout = verified.status;
|
|
1323
|
+
passing.stderr = '';
|
|
1324
|
+
}
|
|
1325
|
+
const readiness = upProved ? 'proved by its own up, as its status would' : await awaitReadiness(external.readyWhen, cwd, service.id, env, boot?.signal, passing);
|
|
1296
1326
|
boot?.progress(`${service.id}: ready (${readiness})`);
|
|
1297
1327
|
// 3. status — discover connection info (optional; may reuse up output instead).
|
|
1298
1328
|
let statusOutput = '';
|
|
1299
1329
|
if (external.status) {
|
|
1300
|
-
|
|
1330
|
+
// The runtime's own infrastructure probes its readiness with the very status command it then reads (init declares
|
|
1331
|
+
// both as `volter-world-infra status`): the probe that just passed is that read, taken a moment ago in this
|
|
1332
|
+
// process, so it is not run again (S2 R17: LibreChat's second run of it, 190 ms of up after its infrastructure
|
|
1333
|
+
// was ready). Any other status command, or a probe that is not the same command, is run as before.
|
|
1334
|
+
const probe = external.readyWhen?.command === undefined ? undefined : [external.readyWhen.command, ...(external.readyWhen.args ?? [])];
|
|
1335
|
+
const same = probe !== undefined && passing.stdout !== undefined && ownInfrastructureStatus(external.status)
|
|
1336
|
+
&& probe.length === external.status.length && probe.every((part, index) => part === external.status[index]);
|
|
1337
|
+
const statusResult = same ? { stdout: passing.stdout, stderr: passing.stderr ?? '' } : await runExternalCommand(external.status, cwd, service.id, 'status', env, boot?.signal);
|
|
1301
1338
|
statusOutput = statusResult.stdout || statusResult.stderr;
|
|
1302
1339
|
appendFileSync(log, `\n$ ${external.status.join(' ')}\n${statusOutput}`);
|
|
1303
1340
|
boot?.progress(`${service.id}: status read`);
|
|
@@ -1640,9 +1677,12 @@ const ISSUE_TIMEOUT_MS = 15_000;
|
|
|
1640
1677
|
/** A twin's issued credential (its `issues`, copied from its pack's credentialDoor by init): once it is up, the door is
|
|
1641
1678
|
* asked for one and each pending name of that service is set to it. A twin that answers no credential, or has no
|
|
1642
1679
|
* door, leaves the names unset and says so, rather than handing the application a fixture the twin would refuse. */
|
|
1643
|
-
async function issueCredentials(service, started, env, pending, unissued, controlRoot) {
|
|
1680
|
+
async function issueCredentials(service, started, env, pending, unissued, controlRoot, times) {
|
|
1644
1681
|
const direct = new Set([...pending].filter(([, id]) => id === `${service.id}@direct`).map(([name]) => name));
|
|
1645
1682
|
const wanted = [...pending].filter(([name, id]) => id === service.id || direct.has(name)).map(([name]) => name);
|
|
1683
|
+
if (times) {
|
|
1684
|
+
times.names = wanted.length;
|
|
1685
|
+
}
|
|
1646
1686
|
if (wanted.length === 0)
|
|
1647
1687
|
return;
|
|
1648
1688
|
for (const name of wanted)
|
|
@@ -1653,6 +1693,8 @@ async function issueCredentials(service, started, env, pending, unissued, contro
|
|
|
1653
1693
|
door = packFacts()[service.id]?.credentialDoor ?? door;
|
|
1654
1694
|
}
|
|
1655
1695
|
catch { /* no artifact: the saved copy */ }
|
|
1696
|
+
if (times)
|
|
1697
|
+
times.door = performance.now();
|
|
1656
1698
|
try {
|
|
1657
1699
|
if (!door)
|
|
1658
1700
|
throw new Error('its service names no credential door');
|
|
@@ -1664,130 +1706,148 @@ async function issueCredentials(service, started, env, pending, unissued, contro
|
|
|
1664
1706
|
beginPlaceholderPull(service.id, controlRoot);
|
|
1665
1707
|
let answer;
|
|
1666
1708
|
try {
|
|
1709
|
+
if (times)
|
|
1710
|
+
times.sent = performance.now();
|
|
1667
1711
|
const res = await fetch(new URL(door.path, started.url), { method: 'POST', headers: { 'content-type': 'application/json', 'x-volter-world-activity': 'provisioning' }, body: JSON.stringify(door.body), signal: AbortSignal.timeout(ISSUE_TIMEOUT_MS) });
|
|
1712
|
+
if (times)
|
|
1713
|
+
times.headers = performance.now();
|
|
1714
|
+
// What the door answered is said with its status: a twin's own error is the only reading of why it issued
|
|
1715
|
+
// nothing, and it is otherwise in a log of the twin's host that the caller of `up` may never see.
|
|
1668
1716
|
if (!res.ok)
|
|
1669
|
-
throw new Error(`answered ${res.status}`);
|
|
1717
|
+
throw new Error(`answered ${res.status}: ${(await res.text().catch(() => '')).replace(/\s+/g, ' ').slice(0, 400) || 'no body'}`);
|
|
1670
1718
|
answer = (await res.json());
|
|
1719
|
+
if (times)
|
|
1720
|
+
times.answered = performance.now();
|
|
1671
1721
|
}
|
|
1672
1722
|
finally {
|
|
1673
1723
|
if (!enclosingSeed)
|
|
1674
1724
|
endPlaceholderPull(service.id, controlRoot);
|
|
1675
1725
|
}
|
|
1726
|
+
// EACH NAME IS FILLED ON ITS OWN. A door answers several things (a key, a sign-in's files); a part it could not
|
|
1727
|
+
// make, or a name whose part is not in the answer, leaves THAT name unset and said, and the others are given.
|
|
1728
|
+
// One throw here used to unset every name of the service, the key among them.
|
|
1676
1729
|
for (const name of wanted) {
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
if (
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
1690
|
-
|
|
1691
|
-
|
|
1692
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
|
|
1700
|
-
|
|
1701
|
-
|
|
1702
|
-
|
|
1703
|
-
|
|
1704
|
-
|
|
1705
|
-
|
|
1730
|
+
try {
|
|
1731
|
+
const spec = door.fill[name] ?? '';
|
|
1732
|
+
if (spec.startsWith('directory:') || spec.startsWith('file:')) {
|
|
1733
|
+
if (direct.has(name))
|
|
1734
|
+
throw new Error(`cannot give ${name}'s filesystem credential a direct URL`);
|
|
1735
|
+
if (!isEnvironmentName(name) || !/^[a-z0-9][a-z0-9._-]*$/.test(service.id))
|
|
1736
|
+
throw new Error('Invalid credential destination name');
|
|
1737
|
+
if (!env.VOLTER_WORLD_INSTANCE)
|
|
1738
|
+
throw new Error('World credential custody is unknown');
|
|
1739
|
+
const instance = realpathSync(dirname(env.VOLTER_WORLD_INSTANCE));
|
|
1740
|
+
const custody = ownedPath(instance, 'credentials');
|
|
1741
|
+
const artifact = `${service.id}-${name.toLowerCase()}`;
|
|
1742
|
+
const isDirectory = spec.startsWith('directory:');
|
|
1743
|
+
const field = spec.slice(spec.indexOf(':') + 1), value = answer[field];
|
|
1744
|
+
if (value === undefined || value === null)
|
|
1745
|
+
throw new Error(`answered no ${field}`);
|
|
1746
|
+
if (isDirectory && (typeof value !== 'object' || Array.isArray(value) || Object.keys(value).length === 0))
|
|
1747
|
+
throw new Error('Invalid credential directory');
|
|
1748
|
+
const entries = isDirectory ? Object.entries(value) : [[`${artifact}.json`, value]];
|
|
1749
|
+
const files = entries.map(([file, contents]) => {
|
|
1750
|
+
if (isDirectory && (!file || file.includes('\\') || file.split('/').some(part => !/^[A-Za-z0-9._-]+$/.test(part) || part === '.' || part === '..')))
|
|
1751
|
+
throw new Error('Invalid World-owned path');
|
|
1752
|
+
return { path: ownedPath(custody, isDirectory ? `${artifact}/${file}` : file), contents };
|
|
1753
|
+
});
|
|
1754
|
+
const directories = new Set([custody]);
|
|
1755
|
+
for (const { path } of files) {
|
|
1756
|
+
let parent = dirname(path);
|
|
1757
|
+
while (parent !== custody) {
|
|
1758
|
+
directories.add(parent);
|
|
1759
|
+
parent = dirname(parent);
|
|
1760
|
+
}
|
|
1761
|
+
const stat = lstatSync(path, { throwIfNoEntry: false });
|
|
1762
|
+
if (stat && (!stat.isFile() || stat.nlink !== 1))
|
|
1763
|
+
throw new Error('Invalid World-owned path');
|
|
1706
1764
|
}
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
for (const dir of directories) {
|
|
1713
|
-
ownedPath(instance, relative(instance, dir));
|
|
1714
|
-
const stat = lstatSync(dir, { throwIfNoEntry: false });
|
|
1715
|
-
if (stat && !stat.isDirectory())
|
|
1716
|
-
throw new Error('Invalid World-owned path');
|
|
1717
|
-
}
|
|
1718
|
-
for (const dir of [...directories].sort((a, b) => a.length - b.length)) {
|
|
1719
|
-
if (!existsSync(dir))
|
|
1720
|
-
mkdirSync(dir, { mode: 0o700 });
|
|
1721
|
-
assertOwnedRealPath(custody, dir);
|
|
1722
|
-
chmodSync(dir, 0o700);
|
|
1723
|
-
}
|
|
1724
|
-
for (const { path, contents } of files) {
|
|
1725
|
-
ownedPath(custody, relative(custody, path));
|
|
1726
|
-
assertOwnedRealPath(custody, dirname(path));
|
|
1727
|
-
const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_NOFOLLOW, 0o600);
|
|
1728
|
-
let opened;
|
|
1729
|
-
try {
|
|
1730
|
-
const stat = fstatSync(fd);
|
|
1731
|
-
if (!stat.isFile() || stat.nlink !== 1)
|
|
1765
|
+
// Validate every destination and parent before creating or changing any custody artifact.
|
|
1766
|
+
for (const dir of directories) {
|
|
1767
|
+
ownedPath(instance, relative(instance, dir));
|
|
1768
|
+
const stat = lstatSync(dir, { throwIfNoEntry: false });
|
|
1769
|
+
if (stat && !stat.isDirectory())
|
|
1732
1770
|
throw new Error('Invalid World-owned path');
|
|
1733
|
-
opened = stat;
|
|
1734
|
-
fchmodSync(fd, 0o600);
|
|
1735
|
-
assertOwnedRealPath(custody, dirname(path));
|
|
1736
|
-
ftruncateSync(fd, 0);
|
|
1737
|
-
writeFileSync(fd, typeof contents === 'string' ? contents : `${JSON.stringify(contents, null, 2)}\n`);
|
|
1738
1771
|
}
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
1772
|
+
for (const dir of [...directories].sort((a, b) => a.length - b.length)) {
|
|
1773
|
+
if (!existsSync(dir))
|
|
1774
|
+
mkdirSync(dir, { mode: 0o700 });
|
|
1775
|
+
assertOwnedRealPath(custody, dir);
|
|
1776
|
+
chmodSync(dir, 0o700);
|
|
1777
|
+
}
|
|
1778
|
+
for (const { path, contents } of files) {
|
|
1779
|
+
ownedPath(custody, relative(custody, path));
|
|
1780
|
+
assertOwnedRealPath(custody, dirname(path));
|
|
1781
|
+
const fd = openSync(path, fsConstants.O_WRONLY | fsConstants.O_CREAT | fsConstants.O_NOFOLLOW, 0o600);
|
|
1782
|
+
let opened;
|
|
1742
1783
|
try {
|
|
1743
|
-
|
|
1744
|
-
|
|
1745
|
-
if (!opened || current.dev !== opened.dev || current.ino !== opened.ino)
|
|
1784
|
+
const stat = fstatSync(fd);
|
|
1785
|
+
if (!stat.isFile() || stat.nlink !== 1)
|
|
1746
1786
|
throw new Error('Invalid World-owned path');
|
|
1747
|
-
|
|
1787
|
+
opened = stat;
|
|
1788
|
+
fchmodSync(fd, 0o600);
|
|
1789
|
+
assertOwnedRealPath(custody, dirname(path));
|
|
1790
|
+
ftruncateSync(fd, 0);
|
|
1791
|
+
writeFileSync(fd, typeof contents === 'string' ? contents : `${JSON.stringify(contents, null, 2)}\n`);
|
|
1748
1792
|
}
|
|
1749
|
-
|
|
1750
|
-
|
|
1751
|
-
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1793
|
+
finally {
|
|
1794
|
+
closeSync(fd);
|
|
1795
|
+
let realFile;
|
|
1796
|
+
try {
|
|
1797
|
+
realFile = realpathSync(path);
|
|
1798
|
+
const current = statSync(realFile);
|
|
1799
|
+
if (!opened || current.dev !== opened.dev || current.ino !== opened.ino)
|
|
1800
|
+
throw new Error('Invalid World-owned path');
|
|
1801
|
+
assertOwnedRealPath(custody, realFile);
|
|
1802
|
+
}
|
|
1803
|
+
catch (error) {
|
|
1804
|
+
// Remove only the file whose descriptor we opened; a raced replacement is not ours to delete.
|
|
1805
|
+
const current = realFile ? lstatSync(realFile, { throwIfNoEntry: false }) : undefined;
|
|
1806
|
+
if (opened && current?.dev === opened.dev && current.ino === opened.ino)
|
|
1807
|
+
rmSync(realFile, { force: true });
|
|
1808
|
+
throw error;
|
|
1809
|
+
}
|
|
1755
1810
|
}
|
|
1756
1811
|
}
|
|
1812
|
+
env[name] = isDirectory ? ownedPath(custody, artifact) : files[0].path;
|
|
1813
|
+
continue;
|
|
1757
1814
|
}
|
|
1758
|
-
|
|
1759
|
-
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1815
|
+
// `side:<side>:<field>`: a URL the twin issues for one of its side ports (a native wire beside its HTTP API, the
|
|
1816
|
+
// PlanetScale twin's MySQL), its authority the side's listener as this boot placed it
|
|
1817
|
+
if (spec.startsWith('side:')) {
|
|
1818
|
+
const [, side, field] = spec.split(':');
|
|
1819
|
+
const value = answer[field ?? ''];
|
|
1820
|
+
if (typeof value !== 'string')
|
|
1821
|
+
throw new Error(`answered no ${field}`);
|
|
1822
|
+
const at = side ? started.sides?.[side]?.url : undefined;
|
|
1823
|
+
if (!at)
|
|
1824
|
+
throw new Error(`it serves no ${side} side port`);
|
|
1825
|
+
const issued = new URL(value);
|
|
1826
|
+
issued.host = new URL(at).host;
|
|
1827
|
+
env[name] = issued.toString();
|
|
1828
|
+
continue;
|
|
1829
|
+
}
|
|
1830
|
+
const field = spec;
|
|
1831
|
+
const value = answer[field];
|
|
1766
1832
|
if (typeof value !== 'string')
|
|
1767
1833
|
throw new Error(`answered no ${field}`);
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1834
|
+
if (direct.has(name)) {
|
|
1835
|
+
const issued = new URL(value);
|
|
1836
|
+
if (!['http:', 'https:'].includes(issued.protocol))
|
|
1837
|
+
throw new Error(`cannot route ${name}'s non-HTTP URL directly`);
|
|
1838
|
+
const endpoint = new URL(started.url);
|
|
1839
|
+
issued.protocol = endpoint.protocol;
|
|
1840
|
+
issued.host = endpoint.host;
|
|
1841
|
+
env[name] = issued.toString();
|
|
1842
|
+
}
|
|
1843
|
+
else
|
|
1844
|
+
env[name] = value;
|
|
1775
1845
|
}
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
if (direct.has(name)) {
|
|
1781
|
-
const issued = new URL(value);
|
|
1782
|
-
if (!['http:', 'https:'].includes(issued.protocol))
|
|
1783
|
-
throw new Error(`cannot route ${name}'s non-HTTP URL directly`);
|
|
1784
|
-
const endpoint = new URL(started.url);
|
|
1785
|
-
issued.protocol = endpoint.protocol;
|
|
1786
|
-
issued.host = endpoint.host;
|
|
1787
|
-
env[name] = issued.toString();
|
|
1846
|
+
catch (error) {
|
|
1847
|
+
process.stderr.write(`volter-world: ${service.id} did not issue ${name} (${errorMessage(error)}${typeof answer.codex_unavailable === 'string' ? `; the twin says: ${answer.codex_unavailable.slice(0, 300)}` : ''}); left unset\n`);
|
|
1848
|
+
delete env[name];
|
|
1849
|
+
unissued.add(name);
|
|
1788
1850
|
}
|
|
1789
|
-
else
|
|
1790
|
-
env[name] = value;
|
|
1791
1851
|
}
|
|
1792
1852
|
if (started.url) {
|
|
1793
1853
|
const endpoint = new URL(started.url);
|
|
@@ -1803,6 +1863,8 @@ async function issueCredentials(service, started, env, pending, unissued, contro
|
|
|
1803
1863
|
unissued.add(name);
|
|
1804
1864
|
}
|
|
1805
1865
|
}
|
|
1866
|
+
if (times)
|
|
1867
|
+
times.filled = performance.now();
|
|
1806
1868
|
}
|
|
1807
1869
|
function materializeConfig(config, root) {
|
|
1808
1870
|
const services = config.services.map((service) => {
|
|
@@ -1837,6 +1899,23 @@ function versionSatisfies(actual, pin) {
|
|
|
1837
1899
|
return am === pm && (an ?? 0) >= (pn ?? 0);
|
|
1838
1900
|
return actual === pin;
|
|
1839
1901
|
}
|
|
1902
|
+
/** Where the host's start went, from its receipt, for the receipt's progress line (S2 R38): when its own modules were
|
|
1903
|
+
* loaded, when its twins' factories returned (the slowest named) and when it published, each after its spawn, and how
|
|
1904
|
+
* long after publication the receipt was read. A host that says none of it adds nothing. */
|
|
1905
|
+
function hostStartAccount(times, spawnedAt, readAt) {
|
|
1906
|
+
const after = (time) => `+${Math.max(0, time - spawnedAt)} ms`;
|
|
1907
|
+
const parts = [];
|
|
1908
|
+
if (times.at !== undefined)
|
|
1909
|
+
parts.push(`${Math.max(0, readAt - times.at)} ms after the host published it`);
|
|
1910
|
+
if (times.began !== undefined)
|
|
1911
|
+
parts.push(`the host's own modules loaded ${after(times.began)} after its spawn`);
|
|
1912
|
+
const started = Object.entries(times.started ?? {}).sort((a, b) => a[1] - b[1]);
|
|
1913
|
+
if (started.length)
|
|
1914
|
+
parts.push(`its twins returned ${after(started[0][1])} to ${after(started.at(-1)[1])} (slowest ${started.at(-1)[0]})`);
|
|
1915
|
+
if (times.at !== undefined && (times.began !== undefined || started.length))
|
|
1916
|
+
parts.push(`published ${after(times.at)}`);
|
|
1917
|
+
return parts.length ? `, ${parts.join('; ')}` : '';
|
|
1918
|
+
}
|
|
1840
1919
|
/**
|
|
1841
1920
|
* Boot every `colocate`-declaring service inside ONE `volter-world-host` child (see src/host.ts —
|
|
1842
1921
|
* 'colocated' maps to the host's 'shared' mode, 'worker' to one Worker thread per twin). The
|
|
@@ -1856,21 +1935,33 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1856
1935
|
const readyFile = join(paths.instance, `host-ready-${boot.bootId}.json`);
|
|
1857
1936
|
const args = [hostCli, '--isolation', isolation === 'colocated' ? 'shared' : 'worker', '--ready-file', readyFile];
|
|
1858
1937
|
const allocated = [];
|
|
1859
|
-
|
|
1860
|
-
|
|
1938
|
+
// Every twin's port and side ports are probed together, not one after another: a probe is a listen and a close
|
|
1939
|
+
// on IPv4 and IPv6, ~25 ms each in a tab, and Dub's fourteen twins and their sides held sixteen of them in turn
|
|
1940
|
+
// (S2 R16: 406 ms of its 1,871 ms up before the host was spawned). Concurrent probes hold distinct ports while
|
|
1941
|
+
// open, and `allocatedPorts` is checked and added to synchronously after each probe, so no port is given twice;
|
|
1942
|
+
// a declared port is in it before any automatic one is chosen, as before. Which twin gets which automatic port
|
|
1943
|
+
// is not part of any contract.
|
|
1944
|
+
// Relative module paths resolve from the world root (import() inside the host would
|
|
1945
|
+
// otherwise resolve them against the runtime's own source directory).
|
|
1946
|
+
const moduleOf = (service) => {
|
|
1947
|
+
const declared = service.colocate.module;
|
|
1948
|
+
return declared.startsWith('.') ? resolve(paths.root, declared) : declared;
|
|
1949
|
+
};
|
|
1950
|
+
const probed = await Promise.all(colocated.map(async (service) => ({
|
|
1951
|
+
port: service.port === undefined || service.port === 'auto' ? await allocatePort(allocatedPorts) : service.port,
|
|
1952
|
+
sides: await sidePortsFor(vendorOfModule(moduleOf(service), service.id), [], allocatedPorts),
|
|
1953
|
+
})));
|
|
1954
|
+
for (const [index, service] of colocated.entries()) {
|
|
1955
|
+
const { port, sides } = probed[index];
|
|
1861
1956
|
const serviceDataDir = ownedPath(paths.data, service.id);
|
|
1862
1957
|
mkdirSync(serviceDataDir, { recursive: true });
|
|
1863
|
-
|
|
1864
|
-
// otherwise resolve them against the runtime's own source directory).
|
|
1865
|
-
const declared = service.colocate.module;
|
|
1866
|
-
const module = declared.startsWith('.') ? resolve(paths.root, declared) : declared;
|
|
1958
|
+
const module = moduleOf(service);
|
|
1867
1959
|
// R17 — the lockfile check, BEFORE the host boots: what this module's own package.json says
|
|
1868
1960
|
// it is, against the pin the world config carries; and R16 — the protocol standing the
|
|
1869
1961
|
// catalog recorded for the package (pack-facts), refused two majors back, warned one back.
|
|
1870
1962
|
const mounted = lockfileCheck(service, module);
|
|
1871
1963
|
const scenario = service.colocate.scenarioPath;
|
|
1872
1964
|
const binding = managedDatabaseFor(vendorOfModule(module, service.id), worldEnv.VOLTER_WORLD_CONFIG, config.env ?? {});
|
|
1873
|
-
const sides = await sidePortsFor(vendorOfModule(module, service.id), [], allocatedPorts);
|
|
1874
1965
|
args.push('--spec', [service.id, module, service.colocate.export, String(port), serviceDataDir, ...(scenario ? [`scenario=${scenario.startsWith('/') ? scenario : resolve(paths.root, scenario)}`] : []), ...(binding !== undefined ? [`database=${binding.url}`] : []), ...sides.map((side) => `side.${side.option}=${side.port}`)].join('|'));
|
|
1875
1966
|
allocated.push({ service, port, mounted, sides });
|
|
1876
1967
|
}
|
|
@@ -1886,6 +1977,7 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1886
1977
|
delete env[key];
|
|
1887
1978
|
dropUnissued(env);
|
|
1888
1979
|
env.VOLTER_WORLD_BOOT_ID = boot.bootId;
|
|
1980
|
+
const spawnedAt = Date.now();
|
|
1889
1981
|
// Under the same exit recorder as a process service, so the host's end (and every twin it holds) is in host.log.
|
|
1890
1982
|
const { child, ended, identity } = spawnRecorded('host', process.execPath, args, paths.root, env, log, boot);
|
|
1891
1983
|
child.once('error', error => boot.fail(error));
|
|
@@ -1922,7 +2014,7 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1922
2014
|
});
|
|
1923
2015
|
const ready = (async () => {
|
|
1924
2016
|
const hostTimeoutMs = allocated.reduce((timeout, { service }) => Math.max(timeout, typeof service.ready === 'object' ? service.ready.timeoutMs ?? 15_000 : 15_000), 15_000);
|
|
1925
|
-
await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort, () => { try {
|
|
2017
|
+
const hostTimes = await untilReadyOrEnded((abort) => waitForHostReady(readyFile, boot.bootId, new Map(allocated.map(({ service, port }) => [service.id, port])), hostTimeoutMs, abort, () => { try {
|
|
1926
2018
|
return statSync(log).size;
|
|
1927
2019
|
}
|
|
1928
2020
|
catch {
|
|
@@ -1936,8 +2028,35 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1936
2028
|
catch { /* best effort cleanup */ }
|
|
1937
2029
|
throw new Error(`Co-located host failed to complete startup (isolation=${isolation}): ${errorMessage(error)}\nLog: ${log}`);
|
|
1938
2030
|
});
|
|
1939
|
-
boot.progress(
|
|
2031
|
+
boot.progress(`co-located host: its all-started receipt is read${hostStartAccount(hostTimes, spawnedAt, Date.now())}`);
|
|
2032
|
+
// The host's receipt (this boot, every requested port) is published only
|
|
2033
|
+
// after every factory bound its own port; a port another process held fails
|
|
2034
|
+
// its bind and leaves no receipt. The receipt is the host's ownership proof
|
|
2035
|
+
// for each port (ADR 0014). A port that answers with another boot's identity
|
|
2036
|
+
// is still refused.
|
|
2037
|
+
// So each port is asked its boot at once: this boot's answer is the port's reachability and its ownership in one
|
|
2038
|
+
// connection. Another boot's answer is refused at once, as confirmation refuses it; a port that answers no
|
|
2039
|
+
// boot is waited for and confirmed as before.
|
|
2040
|
+
// A twin the host's receipt names a byte stream (host.ts servesByteStream) is not asked: it would answer the request
|
|
2041
|
+
// in its own protocol, after its own greeting work (Rallly's SMTP relay answered 57 ms after the request, holding
|
|
2042
|
+
// host-ready: e946aa4c run 3, on the mini, World 3.0.2197; S2 R38), and then be asked again by confirmation. Its
|
|
2043
|
+
// admission is its TCP answer and its host still running, as a port with no boot identity's is: another holder of
|
|
2044
|
+
// its port fails its bind, so no receipt.
|
|
2045
|
+
const streams = new Set(hostTimes.streams ?? []);
|
|
1940
2046
|
await parallelStartup(allocated, async ({ service, port }) => {
|
|
2047
|
+
const byteStream = streams.has(service.id);
|
|
2048
|
+
const answered = byteStream ? null : await worldBootOf(port, boot.signal);
|
|
2049
|
+
if (answered && typeof answered.boot === 'string') {
|
|
2050
|
+
if (answered.boot === boot.bootId)
|
|
2051
|
+
return;
|
|
2052
|
+
if (undeadPids([child.pid]).length)
|
|
2053
|
+
recordWorldStop([log], 'world.service.stop', 'up (port not its own)', 'SIGTERM');
|
|
2054
|
+
try {
|
|
2055
|
+
await signalHeldChild(child, 'SIGTERM', true, identity);
|
|
2056
|
+
}
|
|
2057
|
+
catch { /* best effort cleanup */ }
|
|
2058
|
+
throw foreignBoot(`Co-located twin "${service.id}"`, answered.boot, log);
|
|
2059
|
+
}
|
|
1941
2060
|
const bindTimeoutMs = typeof service.ready === 'object' && service.ready.timeoutMs !== undefined ? Math.max(service.ready.timeoutMs, 15_000) : 15_000;
|
|
1942
2061
|
await untilReadyOrEnded((abort) => waitForTcp(port, bindTimeoutMs, abort), ended, boot, 'the co-located host', log).catch(async (error) => {
|
|
1943
2062
|
if (undeadPids([child.pid]).length)
|
|
@@ -1948,14 +2067,7 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1948
2067
|
catch { /* best effort cleanup */ }
|
|
1949
2068
|
throw new Error(`Co-located twin "${service.id}" failed to start (host isolation=${isolation}): ${errorMessage(error)}\nLog: ${log}`);
|
|
1950
2069
|
});
|
|
1951
|
-
|
|
1952
|
-
// The host's receipt (this boot, every requested port) is published only
|
|
1953
|
-
// after every factory bound its own port; a port another process held fails
|
|
1954
|
-
// its bind and leaves no receipt. The receipt is the host's ownership proof
|
|
1955
|
-
// for each port (ADR 0014). A port that answers with another boot's identity
|
|
1956
|
-
// is still refused.
|
|
1957
|
-
await parallelStartup(allocated, async ({ service, port }) => {
|
|
1958
|
-
await confirmOwnPort(port, boot, ended, true, `Co-located twin "${service.id}"`, log).catch(async (error) => {
|
|
2070
|
+
await confirmOwnPort(port, boot, ended, !byteStream, `Co-located twin "${service.id}"`, log).catch(async (error) => {
|
|
1959
2071
|
if (undeadPids([child.pid]).length)
|
|
1960
2072
|
recordWorldStop([log], 'world.service.stop', 'up (port not its own)', 'SIGTERM');
|
|
1961
2073
|
try {
|
|
@@ -1975,9 +2087,51 @@ async function startColocatedServices(config, colocated, isolation, worldEnv, pa
|
|
|
1975
2087
|
ready.catch(() => undefined);
|
|
1976
2088
|
return { services, ready };
|
|
1977
2089
|
}
|
|
2090
|
+
/** How late this process's event loop ran while `up` worked (S2 R45): a 10 ms tick, unref'd, on the monotonic clock.
|
|
2091
|
+
* A tick 2 ms or more late adds its lateness to the total (less is timer jitter, not counted); the longest is kept; a
|
|
2092
|
+
* stall is a tick 10 ms or more late, and the three longest are kept with when, after up began, each began (to within
|
|
2093
|
+
* the 10 ms tick). LibreChat's up showed three windows of 206-263 ms with no work of the runtime's in its trace
|
|
2094
|
+
* (e946aa4c lap 060250 run 2, on the mini, in a browser tab): this says whether its loop was stalled in them or working. */
|
|
2095
|
+
function eventLoopLag() {
|
|
2096
|
+
const TICK_MS = 10;
|
|
2097
|
+
const JITTER_MS = 2;
|
|
2098
|
+
const started = performance.now();
|
|
2099
|
+
let expected = started + TICK_MS;
|
|
2100
|
+
let total = 0;
|
|
2101
|
+
let longest = 0;
|
|
2102
|
+
const stalls = [];
|
|
2103
|
+
const timer = setInterval(() => {
|
|
2104
|
+
const now = performance.now();
|
|
2105
|
+
const late = now - expected;
|
|
2106
|
+
expected = now + TICK_MS;
|
|
2107
|
+
if (late < JITTER_MS)
|
|
2108
|
+
return;
|
|
2109
|
+
total += late;
|
|
2110
|
+
longest = Math.max(longest, late);
|
|
2111
|
+
if (late < TICK_MS)
|
|
2112
|
+
return;
|
|
2113
|
+
stalls.push({ at: now - late - started, ms: late });
|
|
2114
|
+
stalls.sort((a, b) => b.ms - a.ms);
|
|
2115
|
+
stalls.length = Math.min(stalls.length, 3);
|
|
2116
|
+
}, TICK_MS);
|
|
2117
|
+
timer.unref?.();
|
|
2118
|
+
return {
|
|
2119
|
+
account: () => `runtime event loop ${Math.round(total)} ms late in all (ticks 2 ms or more late), longest ${Math.round(longest)} ms${stalls.length ? `; stalls ${stalls.map((stall) => `${Math.round(stall.ms)} ms at +${(stall.at / 1000).toFixed(2)} s`).join(', ')} (starts ±10 ms)` : ''}`,
|
|
2120
|
+
stop: () => clearInterval(timer),
|
|
2121
|
+
};
|
|
2122
|
+
}
|
|
1978
2123
|
export async function upWorld(configId, options = {}, abort) {
|
|
1979
|
-
abort?.throwIfAborted();
|
|
1980
2124
|
const upStarted = Date.now();
|
|
2125
|
+
const lag = eventLoopLag();
|
|
2126
|
+
try {
|
|
2127
|
+
return await upWorldRecorded(configId, options, abort, upStarted, lag);
|
|
2128
|
+
}
|
|
2129
|
+
finally {
|
|
2130
|
+
lag.stop();
|
|
2131
|
+
}
|
|
2132
|
+
}
|
|
2133
|
+
async function upWorldRecorded(configId, options, abort, upStarted, lag) {
|
|
2134
|
+
abort?.throwIfAborted();
|
|
1981
2135
|
const root = resolve(options.root ?? process.cwd());
|
|
1982
2136
|
const found = loadWorldConfig(configId, root);
|
|
1983
2137
|
// the facts of the World's installed packs, over the kernel's, for this process and (through the instance file) its own
|
|
@@ -1985,7 +2139,10 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
1985
2139
|
for (const file of packFactsPaths)
|
|
1986
2140
|
overlayPackFactsFile(file);
|
|
1987
2141
|
// and this process's injector host rules, which `up`'s inert-injectEnv warning reads, know them too
|
|
1988
|
-
|
|
2142
|
+
// An empty overlay changes no facts. Leave built-in host rules at their first use
|
|
2143
|
+
// instead of loading and rebuilding a second facts/host table on every empty up.
|
|
2144
|
+
if (packFactsPaths.length)
|
|
2145
|
+
createRequire(import.meta.url)('@volter/world-core/pack-facts').extendPackFacts(packFactsPaths);
|
|
1989
2146
|
const loaded = { path: found.path, config: materializeConfig(found.config, root) };
|
|
1990
2147
|
const name = options.name ?? loaded.config.id;
|
|
1991
2148
|
const mode = options.mode ?? 'local';
|
|
@@ -2071,6 +2228,8 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2071
2228
|
// the lockfile adds the cross-process exclusion, and an in-process lock holder always releases
|
|
2072
2229
|
// before its first await, so withFileLock's synchronous contender wait can never deadlock the
|
|
2073
2230
|
// event loop against a holder in the same process.
|
|
2231
|
+
// The claim's start: a kept state's lock file older than it is its last life's (removeCarriedLocks, S2 K4).
|
|
2232
|
+
const claimStartedAt = Date.now();
|
|
2074
2233
|
try {
|
|
2075
2234
|
withFileLock(instanceLockFile(root, name), () => {
|
|
2076
2235
|
assertProxyOwnershipResolved(root, name);
|
|
@@ -2160,6 +2319,9 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2160
2319
|
for (const entry of existsSync(instance) ? readdirSync(instance) : [])
|
|
2161
2320
|
if (!KEEP.includes(entry))
|
|
2162
2321
|
rmSync(join(instance, entry), { recursive: true, force: true });
|
|
2322
|
+
// The kept state's lock files with no readable holder record are its last life's, and no one's: removed now,
|
|
2323
|
+
// under this boot's claim, before any process of it waits on one (S2 K4; world-store.ts removeCarriedLocks).
|
|
2324
|
+
removeCarriedLocks(KEEP.map(entry => join(instance, entry)).filter(path => existsSync(path)), { stateDir: stateDirName(), before: claimStartedAt });
|
|
2163
2325
|
}
|
|
2164
2326
|
else
|
|
2165
2327
|
removeWorldState(root, instance, () => rmSync(instance, { recursive: true, force: true }));
|
|
@@ -2315,11 +2477,52 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2315
2477
|
await host.ready;
|
|
2316
2478
|
if (colocated.length)
|
|
2317
2479
|
boot.progress('co-located host ready: receipt read, every port admitted as its own');
|
|
2318
|
-
for
|
|
2480
|
+
// Every twin is asked for its credentials at once, and each ask's time is said: thirteen twins took 4.7 s
|
|
2481
|
+
// of one World's up and fourteen took 0.2 s of another's, and the one line after them said only that all had.
|
|
2482
|
+
// Asked in turn they were the tail of up (S2 R17: LibreChat's three, 426 ms, after its infrastructure was
|
|
2483
|
+
// ready). Each ask fills a copy of the environment as it stood before any ask; the copies' changes are then
|
|
2484
|
+
// applied in declaration order, so the environment, its file and up's return are what asking in turn made:
|
|
2485
|
+
// a twin's names are its own (`pending` is split by service before the first await), and no ask reads
|
|
2486
|
+
// another's answer. One difference: where one ask fails, the others still run to their end and write their
|
|
2487
|
+
// custody files, where asking in turn stopped at the first failure; up fails either way.
|
|
2488
|
+
const before = { ...env };
|
|
2489
|
+
// Each ask's own steps, from one origin (S2, the twins' credentials: Twenty's thirteen took 388-500 ms each and
|
|
2490
|
+
// Dub's fourteen 72-90 ms): queued (its turn of the map), its door read, sent, the status, the body, every name
|
|
2491
|
+
// filled. Asks sent together and answered together late say the host or the tab was busy; one long between its
|
|
2492
|
+
// own sent and status says that twin's door.
|
|
2493
|
+
const origin = performance.now();
|
|
2494
|
+
const steps = [];
|
|
2495
|
+
const asks = await Promise.all(host.services.map(async (started) => {
|
|
2319
2496
|
const declared = colocated.find((s) => s.id === started.id);
|
|
2320
|
-
if (declared)
|
|
2321
|
-
|
|
2497
|
+
if (!declared)
|
|
2498
|
+
return undefined;
|
|
2499
|
+
const began = performance.now();
|
|
2500
|
+
const times = { ...(declared.package ? { twin: declared.package } : {}) };
|
|
2501
|
+
steps.push({ id: started.id, queued: began, times });
|
|
2502
|
+
const own = { ...before };
|
|
2503
|
+
await issueCredentials(declared, started, own, issuing, unissued, ownedPath(data, started.id), times);
|
|
2504
|
+
return { id: started.id, ms: Math.round(performance.now() - began), own };
|
|
2505
|
+
}));
|
|
2506
|
+
const at = (time) => time === undefined ? '-' : `+${Math.round(time - origin)}`;
|
|
2507
|
+
const named = steps.filter((step) => (step.times.names ?? 0) > 0);
|
|
2508
|
+
if (named.length)
|
|
2509
|
+
boot.progress(`co-located twins: each credential ask's steps, ms from the asks' start (queued/door/sent/status/body/filled): ${named
|
|
2510
|
+
.sort((left, right) => (right.times.filled ?? 0) - (left.times.filled ?? 0))
|
|
2511
|
+
.map(({ id, queued, times }) => `${id}${times.twin ? ` (${times.twin})` : ''} ${at(queued)}/${at(times.door)}/${at(times.sent)}/${at(times.headers)}/${at(times.answered)}/${at(times.filled)}`).join('; ')}`);
|
|
2512
|
+
const asked = [];
|
|
2513
|
+
for (const ask of asks) {
|
|
2514
|
+
if (!ask)
|
|
2515
|
+
continue;
|
|
2516
|
+
for (const [key, value] of Object.entries(ask.own))
|
|
2517
|
+
if (before[key] !== value)
|
|
2518
|
+
env[key] = value;
|
|
2519
|
+
for (const key of Object.keys(before))
|
|
2520
|
+
if (!Object.hasOwn(ask.own, key))
|
|
2521
|
+
delete env[key];
|
|
2522
|
+
asked.push({ id: ask.id, ms: ask.ms });
|
|
2322
2523
|
}
|
|
2524
|
+
if (asked.length)
|
|
2525
|
+
boot.progress(`co-located twins: each asked for its credentials at once, slowest first: ${[...asked].sort((left, right) => right.ms - left.ms).slice(0, 6).map((each) => `${each.id} ${each.ms} ms`).join(', ')}; ${asked.length} asked, ${Math.max(...asked.map((each) => each.ms))} ms the longest`);
|
|
2323
2526
|
writeProxyEnv(root, name, env);
|
|
2324
2527
|
if (colocated.length)
|
|
2325
2528
|
boot.progress('co-located twins: credentials issued, proxy environment written');
|
|
@@ -2345,11 +2548,13 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2345
2548
|
// possible) or the proxy daemon fails/times out, refuse rather than let unmodified CLIs bypass
|
|
2346
2549
|
// the twins undetected.
|
|
2347
2550
|
const attemptProxy = () => {
|
|
2348
|
-
if (proxyForServices
|
|
2551
|
+
if (proxyForServices)
|
|
2349
2552
|
return;
|
|
2350
2553
|
// a host with no raw sockets (a browser tab) has nothing for the proxy to intercept; the injector holds the policy
|
|
2351
2554
|
if (!hostHasRawEgress())
|
|
2352
2555
|
return;
|
|
2556
|
+
if (Object.keys(activeVendorMap(env)).length === 0 && env[WORLD_NETWORK_POLICY_ENV] === undefined)
|
|
2557
|
+
return;
|
|
2353
2558
|
if (!opensslAvailable()) {
|
|
2354
2559
|
if (mode === 'sealed' || loaded.config.network !== undefined) {
|
|
2355
2560
|
throw new Error(`world "${name}" requires its network policy proxy, but openssl is unavailable; refusing to start. Install openssl.`);
|
|
@@ -2426,12 +2631,12 @@ export async function upWorld(configId, options = {}, abort) {
|
|
|
2426
2631
|
delete worldInstance.previousEnvOutput;
|
|
2427
2632
|
saveWorldInstance(worldInstance);
|
|
2428
2633
|
boot.progress(`environment written: ${envFile}`);
|
|
2429
|
-
|
|
2634
|
+
await materializeWorldBrowserFiles(data, root, name, loaded.config.id);
|
|
2430
2635
|
writePidsFromInstance(worldInstance, Object.values(worldInstance.services).map((service) => service.pid).filter((pid) => pid > 0));
|
|
2431
2636
|
saveWorldInstance(worldInstance);
|
|
2432
2637
|
const servicePids = [...new Set(Object.values(services).map((service) => service.pid).filter((pid) => pid > 0))];
|
|
2433
2638
|
handoffWorldLifecycle(root, name, servicePids);
|
|
2434
|
-
boot.progress(
|
|
2639
|
+
boot.progress(`returning: the World is up; ${lag.account()}`);
|
|
2435
2640
|
if (mode === 'share') {
|
|
2436
2641
|
try {
|
|
2437
2642
|
return await shareWorldServices(name, { root, ...(options.share ?? {}) });
|
|
@@ -2710,6 +2915,7 @@ export async function downWorld(name, root = process.cwd(), options = {}) {
|
|
|
2710
2915
|
assertSafeWorldName(name);
|
|
2711
2916
|
const resolvedRoot = resolve(root);
|
|
2712
2917
|
const token = randomUUID();
|
|
2918
|
+
const ownershipErrors = [];
|
|
2713
2919
|
// Like boot's claim: a short synchronous lock protects an async lifecycle transition.
|
|
2714
2920
|
// Do not hold a filesystem lock while external shutdown or the grace period waits.
|
|
2715
2921
|
withFileLock(instanceLockFile(resolvedRoot, name), () => withFileLock(`${sharingClaimFile(resolvedRoot, name)}.lock`, () => {
|
|
@@ -2726,8 +2932,12 @@ export async function downWorld(name, root = process.cwd(), options = {}) {
|
|
|
2726
2932
|
throw new Error(`World ownership mismatch: ${name}`);
|
|
2727
2933
|
}
|
|
2728
2934
|
}
|
|
2729
|
-
|
|
2730
|
-
|
|
2935
|
+
// Unresolved startup evidence prevents finalization, not retirement of known children.
|
|
2936
|
+
let prior;
|
|
2937
|
+
try {
|
|
2938
|
+
prior = readWorldInstance(name, resolvedRoot, true);
|
|
2939
|
+
}
|
|
2940
|
+
catch { /* diagnosed after owned children retire */ }
|
|
2731
2941
|
if (prior?.resources && !prior.lifecycle)
|
|
2732
2942
|
throw new Error(`World "${name}" belongs to a legacy runtime; use its pinned runtime for teardown before migration`);
|
|
2733
2943
|
if (options.purge)
|
|
@@ -2746,11 +2956,20 @@ export async function downWorld(name, root = process.cwd(), options = {}) {
|
|
|
2746
2956
|
throw new Error(`World "${name}" is being shared or has an unreadable sharing claim; retry after sharing finishes`);
|
|
2747
2957
|
rmSync(sharePath); // explicit down may reclaim a provably stale sharing claim
|
|
2748
2958
|
}
|
|
2749
|
-
retainWorldLifecycleForCleanup(resolvedRoot, name);
|
|
2959
|
+
retainWorldLifecycleForCleanup(resolvedRoot, name, error => { ownershipErrors.push(`Lifecycle ownership unreadable; retained: ${errorMessage(error)}`); });
|
|
2750
2960
|
writeTextAtomic(stoppingFile(resolvedRoot, name), JSON.stringify({ pid: process.pid, at: new Date().toISOString(), token }));
|
|
2751
2961
|
}));
|
|
2752
2962
|
try {
|
|
2753
|
-
|
|
2963
|
+
// The down's own signals and its whole time are said in the World's log (process-groups.mjs, worldEvent).
|
|
2964
|
+
const downAsked = Date.now();
|
|
2965
|
+
setWorldEventsFile(worldEventsPath(join(instanceDir(resolvedRoot, name), 'logs')));
|
|
2966
|
+
try {
|
|
2967
|
+
return await downWorldClaimed(name, resolvedRoot, options, ownershipErrors);
|
|
2968
|
+
}
|
|
2969
|
+
finally {
|
|
2970
|
+
worldEvent({ event: 'world.down', world: name, ms: Date.now() - downAsked });
|
|
2971
|
+
setWorldEventsFile(undefined);
|
|
2972
|
+
}
|
|
2754
2973
|
}
|
|
2755
2974
|
finally {
|
|
2756
2975
|
withFileLock(instanceLockFile(resolvedRoot, name), () => {
|
|
@@ -2760,16 +2979,26 @@ export async function downWorld(name, root = process.cwd(), options = {}) {
|
|
|
2760
2979
|
});
|
|
2761
2980
|
}
|
|
2762
2981
|
}
|
|
2763
|
-
async function downWorldClaimed(name, root, options) {
|
|
2982
|
+
async function downWorldClaimed(name, root, options, externalErrors = []) {
|
|
2983
|
+
const clearBrowserFiles = options.purge ? (await import("./browsers.js")).clearBrowserFiles : undefined;
|
|
2764
2984
|
assertSafeWorldName(name);
|
|
2765
2985
|
const resolvedRoot = resolve(root);
|
|
2766
2986
|
const dir = instanceDir(resolvedRoot, name);
|
|
2767
2987
|
const pidsFile = join(dir, 'pids');
|
|
2768
|
-
|
|
2988
|
+
let before;
|
|
2989
|
+
try {
|
|
2990
|
+
before = getActiveWorldStore().read(instanceFile(resolvedRoot, name)) ?? undefined;
|
|
2991
|
+
}
|
|
2992
|
+
catch (error) {
|
|
2993
|
+
externalErrors.push(`World instance unreadable; retained: ${errorMessage(error)}`);
|
|
2994
|
+
}
|
|
2769
2995
|
// A retry must not retain an earlier success receipt while teardown is in progress.
|
|
2770
2996
|
rmSync(join(dir, 'stopped.json'), { force: true });
|
|
2771
|
-
|
|
2772
|
-
|
|
2997
|
+
const guestError = (error) => {
|
|
2998
|
+
const message = errorMessage(error);
|
|
2999
|
+
if (!externalErrors.includes(message))
|
|
3000
|
+
externalErrors.push(message);
|
|
3001
|
+
};
|
|
2773
3002
|
let instance;
|
|
2774
3003
|
try {
|
|
2775
3004
|
instance = readWorldInstance(name, resolvedRoot);
|
|
@@ -2779,51 +3008,76 @@ async function downWorldClaimed(name, root, options) {
|
|
|
2779
3008
|
catch {
|
|
2780
3009
|
instance = undefined;
|
|
2781
3010
|
}
|
|
2782
|
-
|
|
2783
|
-
|
|
2784
|
-
|
|
2785
|
-
|
|
3011
|
+
// Read independent process records even when a guest receipt or instance is unreadable.
|
|
3012
|
+
const readGuests = () => {
|
|
3013
|
+
try {
|
|
3014
|
+
return readGuestProcesses(dir, instance?.instance, guestError);
|
|
3015
|
+
}
|
|
3016
|
+
catch (error) {
|
|
3017
|
+
guestError(error);
|
|
3018
|
+
return [];
|
|
3019
|
+
}
|
|
3020
|
+
};
|
|
3021
|
+
const guests = readGuests();
|
|
3022
|
+
let proxyBefore = null;
|
|
3023
|
+
try {
|
|
3024
|
+
proxyBefore = getActiveWorldStore().read(proxyStateFile(resolvedRoot, name));
|
|
2786
3025
|
}
|
|
2787
|
-
|
|
2788
|
-
|
|
2789
|
-
externalErrors: ['World instance or external teardown metadata missing or unreadable; cleanup refused; lifecycle evidence retained'] };
|
|
3026
|
+
catch (error) {
|
|
3027
|
+
externalErrors.push(`Proxy process record unreadable; retained: ${errorMessage(error)}`);
|
|
2790
3028
|
}
|
|
2791
3029
|
const proxyState = readProxyState(resolvedRoot, name);
|
|
2792
|
-
const proxyPid = proxyState
|
|
2793
|
-
|
|
3030
|
+
const proxyPid = proxyState && Number.isInteger(proxyState.pid) && proxyState.pid > 0 ? proxyState.pid : undefined;
|
|
3031
|
+
if (proxyBefore !== null && (!proxyPid || (proxyState?.birth !== undefined && (typeof proxyState.birth !== 'string' || !proxyState.birth)))) {
|
|
3032
|
+
externalErrors.push('Proxy process record unreadable; ownership retained');
|
|
3033
|
+
}
|
|
2794
3034
|
const foregroundPid = instance?.lastRun?.state !== 'completed' ? instance?.lastRun?.consumerPid : undefined;
|
|
2795
3035
|
const holderPid = instance?.lifecycle?.holderPid ?? instance?.resources?.holderPid;
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
3036
|
+
let filePids = [];
|
|
3037
|
+
try {
|
|
3038
|
+
filePids = readPids(pidsFile, guests);
|
|
3039
|
+
}
|
|
3040
|
+
catch (error) {
|
|
3041
|
+
externalErrors.push(`World PID record unreadable; retained: ${errorMessage(error)}`);
|
|
3042
|
+
}
|
|
3043
|
+
const recordedPids = [...new Set([...(proxyPid ? [proxyPid] : []), ...filePids, ...(foregroundPid ? [foregroundPid] : []), ...(holderPid ? [holderPid] : [])])].filter(pid => Number.isInteger(pid) && pid > 0);
|
|
3044
|
+
const conflicts = new Set();
|
|
3045
|
+
const conflictingIdentity = (pid) => {
|
|
3046
|
+
if (conflicts.has(pid))
|
|
3047
|
+
return;
|
|
3048
|
+
conflicts.add(pid);
|
|
3049
|
+
externalErrors.push(`Conflicting spawn identities for pid ${pid}; not signalled; ownership retained`);
|
|
3050
|
+
};
|
|
3051
|
+
const identities = readIdentities(resolvedRoot, name, guests, conflictingIdentity);
|
|
3052
|
+
if (proxyPid && typeof proxyState?.birth === 'string' && proxyState.birth) {
|
|
3053
|
+
if (identities[proxyPid] && identities[proxyPid] !== proxyState.birth)
|
|
3054
|
+
conflictingIdentity(proxyPid);
|
|
3055
|
+
else
|
|
3056
|
+
identities[proxyPid] = proxyState.birth;
|
|
3057
|
+
}
|
|
2800
3058
|
const foreign = (await Promise.all(recordedPids.map(async (pid) => {
|
|
2801
3059
|
const now = identities[pid] ? await processBirth(pid) : undefined;
|
|
2802
3060
|
return now && now !== identities[pid] ? { pid, recorded: identities[pid], now } : undefined;
|
|
2803
3061
|
}))).filter((entry) => !!entry);
|
|
2804
|
-
const pids = recordedPids.filter((pid) => !foreign.some((entry) => entry.pid === pid));
|
|
3062
|
+
const pids = recordedPids.filter((pid) => pid !== holderPid && !conflicts.has(pid) && !foreign.some((entry) => entry.pid === pid));
|
|
2805
3063
|
const processServices = Object.values(instance?.services ?? {}).filter((service) => service.type !== 'external' && service.log);
|
|
2806
3064
|
// Only a service still alive is stopped by this `down`; a dead one's end was not the World's.
|
|
2807
3065
|
const stillAlive = undeadPids(processServices.map((service) => service.pid).filter((pid) => pids.includes(pid)));
|
|
2808
3066
|
recordWorldStop(processServices.filter((service) => stillAlive.includes(service.pid)).map((service) => service.log), 'world.service.stop', 'down', 'SIGTERM');
|
|
2809
3067
|
const stopped = await signalPids(pids, 'SIGTERM', identities, spawnedChildren);
|
|
2810
|
-
//
|
|
2811
|
-
// safe only without retained lifecycle ownership, checked before any signals above.
|
|
3068
|
+
// Missing external metadata remains a finalization error after known children retire.
|
|
2812
3069
|
let externalStopped = [];
|
|
2813
|
-
let externalErrors = [];
|
|
2814
3070
|
let unavailable = [];
|
|
2815
|
-
|
|
2816
|
-
|
|
2817
|
-
|
|
2818
|
-
|
|
2819
|
-
|
|
2820
|
-
|
|
2821
|
-
|
|
2822
|
-
|
|
2823
|
-
|
|
2824
|
-
|
|
2825
|
-
if (before !== undefined)
|
|
2826
|
-
externalErrors.push('World instance or external teardown metadata unreadable; cleanup refused');
|
|
3071
|
+
if (instance) {
|
|
3072
|
+
try {
|
|
3073
|
+
const result = await teardownExternalServices(instance);
|
|
3074
|
+
externalStopped = result.stopped;
|
|
3075
|
+
externalErrors.push(...result.errors);
|
|
3076
|
+
unavailable = result.unavailable;
|
|
3077
|
+
}
|
|
3078
|
+
catch (error) {
|
|
3079
|
+
externalErrors.push(errorMessage(error));
|
|
3080
|
+
}
|
|
2827
3081
|
}
|
|
2828
3082
|
// Escalate AFTER the external teardown so externals never wait on the grace, and
|
|
2829
3083
|
// poll the full pids list rather than `stopped`: a pid whose SIGTERM errored is almost always
|
|
@@ -2832,7 +3086,10 @@ async function downWorldClaimed(name, root, options) {
|
|
|
2832
3086
|
recordWorldStop(processServices.filter((service) => escalated.includes(service.pid)).map((service) => service.log), 'world.service.killed', 'down', 'SIGKILL');
|
|
2833
3087
|
// A launch which had already entered its spawn seam may publish while TERM is delivered.
|
|
2834
3088
|
// Once services/agents retire, join every late receipt before certifying this generation stopped.
|
|
2835
|
-
const
|
|
3089
|
+
const settlingGuests = readGuests();
|
|
3090
|
+
readIdentities(resolvedRoot, name, settlingGuests, conflictingIdentity);
|
|
3091
|
+
const lateGuests = settlingGuests.filter(({ record }) => !conflicts.has(record.pid)
|
|
3092
|
+
&& (!recordedPids.includes(record.pid) || identities[record.pid] !== record.start));
|
|
2836
3093
|
try {
|
|
2837
3094
|
await retireOwnedProcessGroups(lateGuests.map(receipt => receipt.record), options.graceMs ?? DOWN_GRACE_MS_DEFAULT);
|
|
2838
3095
|
}
|
|
@@ -2840,7 +3097,7 @@ async function downWorldClaimed(name, root, options) {
|
|
|
2840
3097
|
externalErrors.push(errorMessage(error));
|
|
2841
3098
|
}
|
|
2842
3099
|
pids.push(...lateGuests.map(receipt => receipt.record.pid));
|
|
2843
|
-
if (holderPid &&
|
|
3100
|
+
if (holderPid && recordedPids.includes(holderPid) && !conflicts.has(holderPid) && !foreign.some(entry => entry.pid === holderPid)) {
|
|
2844
3101
|
pids.push(holderPid);
|
|
2845
3102
|
stopped.push(...await signalPids([holderPid], 'SIGTERM', identities, spawnedChildren));
|
|
2846
3103
|
escalated.push(...await killSurvivorsAfterGrace([holderPid], options.graceMs ?? DOWN_GRACE_MS_DEFAULT, identities, spawnedChildren));
|
|
@@ -2851,21 +3108,43 @@ async function downWorldClaimed(name, root, options) {
|
|
|
2851
3108
|
const notSignalled = foreign.map((entry) => `pid ${entry.pid} now names another process (started ${entry.now}; the World's started ${entry.recorded}); not signalled`);
|
|
2852
3109
|
const result = { name, stopped, escalated, externalStopped, externalErrors, ...(notSignalled.length ? { notSignalled } : {}) };
|
|
2853
3110
|
const path = instanceFile(resolvedRoot, name);
|
|
2854
|
-
|
|
3111
|
+
let current;
|
|
3112
|
+
try {
|
|
3113
|
+
current = getActiveWorldStore().read(path) ?? undefined;
|
|
3114
|
+
}
|
|
3115
|
+
catch (error) {
|
|
3116
|
+
externalErrors.push(`World generation unreadable; finalization refused: ${errorMessage(error)}`);
|
|
3117
|
+
return result;
|
|
3118
|
+
}
|
|
2855
3119
|
if (current !== before || existsSync(bootingFile(resolvedRoot, name))) {
|
|
2856
3120
|
externalErrors.push('World generation changed or is booting; finalization refused');
|
|
2857
3121
|
return result;
|
|
2858
3122
|
}
|
|
2859
|
-
|
|
3123
|
+
let currentProxy;
|
|
3124
|
+
try {
|
|
3125
|
+
currentProxy = getActiveWorldStore().read(proxyStateFile(resolvedRoot, name));
|
|
3126
|
+
}
|
|
3127
|
+
catch (error) {
|
|
3128
|
+
externalErrors.push(`Proxy record unreadable; finalization refused: ${errorMessage(error)}`);
|
|
3129
|
+
return result;
|
|
3130
|
+
}
|
|
3131
|
+
if ((currentProxy !== null && currentProxy !== proxyBefore) || existsSync(sharingClaimFile(resolvedRoot, name))) {
|
|
2860
3132
|
externalErrors.push('Proxy or sharing lifecycle changed during teardown; cleanup refused');
|
|
2861
3133
|
return result;
|
|
2862
3134
|
}
|
|
2863
|
-
|
|
3135
|
+
if (existsSync(proxyStartingFile(resolvedRoot, name))) {
|
|
3136
|
+
externalErrors.push(`World "${name}" has unresolved proxy startup ownership; recorded children retired where identity verified; lifecycle and instance retained. Inspect: ${proxyStartingFile(resolvedRoot, name)}`);
|
|
3137
|
+
}
|
|
3138
|
+
const guestReceipts = readGuests();
|
|
2864
3139
|
const surviving = undeadPids([...pids, ...guestReceipts.map(receipt => receipt.record.pid)]);
|
|
2865
3140
|
if (surviving.length)
|
|
2866
3141
|
externalErrors.push(`World processes still alive or uninspectable after teardown: ${surviving.join(', ')}; cleanup refused`);
|
|
2867
3142
|
if (guestReceipts.some(receipt => foreign.some(entry => entry.pid === receipt.record.pid)))
|
|
2868
3143
|
externalErrors.push('Guest spawn identity changed; receipt retained and cleanup refused');
|
|
3144
|
+
if (!instance && (before !== undefined || hasWorldLifecycle(resolvedRoot, name))
|
|
3145
|
+
&& !(before === undefined && !externalErrors.length && releaseAbandonedWorldLifecycle(resolvedRoot, name))) {
|
|
3146
|
+
externalErrors.push('World instance or external teardown metadata missing or unreadable; cleanup refused; lifecycle evidence retained');
|
|
3147
|
+
}
|
|
2869
3148
|
let externalAbandoned;
|
|
2870
3149
|
if (options.purge && instance && externalErrors.length && externalErrors.length === unavailable.length && !foreign.length) {
|
|
2871
3150
|
const inventory = exitedWorldLifecyclePids(resolvedRoot, name, instance.createdAt);
|
|
@@ -2880,6 +3159,8 @@ async function downWorldClaimed(name, root, options) {
|
|
|
2880
3159
|
}
|
|
2881
3160
|
if (externalErrors.length)
|
|
2882
3161
|
return result;
|
|
3162
|
+
// Keep proxy state and session trust until its recorded processes have exited.
|
|
3163
|
+
clearWorldProxyFiles(resolvedRoot, name);
|
|
2883
3164
|
releaseGuestProcesses(guestReceipts);
|
|
2884
3165
|
rmSync(pidsFile, { force: true });
|
|
2885
3166
|
if (before) {
|
|
@@ -2899,7 +3180,9 @@ async function downWorldClaimed(name, root, options) {
|
|
|
2899
3180
|
}
|
|
2900
3181
|
export function statusWorld(name, root = process.cwd(), options = {}) {
|
|
2901
3182
|
const worldInstance = readWorldInstance(name, resolve(root), options.readOnly);
|
|
2902
|
-
|
|
3183
|
+
// One status, one reading of the process table: its questions below are about the same moment.
|
|
3184
|
+
const listing = {};
|
|
3185
|
+
const live = ownedPids(resolve(root), name, livePids(readPids(worldInstance.pidsFile), listing));
|
|
2903
3186
|
const externalOnly = Object.values(worldInstance.services).every(service => service.type === 'external');
|
|
2904
3187
|
const recorded = externalOnly && !existsSync(stoppingFile(root, name)) && !existsSync(join(instanceDir(root, name), 'stopped.json'))
|
|
2905
3188
|
&& isWorldLifecycleRunning(root, name, worldInstance.createdAt);
|
|
@@ -2907,9 +3190,9 @@ export function statusWorld(name, root = process.cwd(), options = {}) {
|
|
|
2907
3190
|
// read; a service's own state is judged by its process group, so a dead service reads as dead beside a live daemon.
|
|
2908
3191
|
const owned = Object.values(worldInstance.services).filter((service) => service.type !== 'external' && service.pid);
|
|
2909
3192
|
const ownPids = ownedPids(resolve(root), name, owned.map((service) => service.pid));
|
|
2910
|
-
const leaders = livePids(ownPids);
|
|
3193
|
+
const leaders = livePids(ownPids, listing);
|
|
2911
3194
|
// `light` (a listing of every World) skips the process-table scan: a lingering group then reads as stopped.
|
|
2912
|
-
const groups = ownPids.length && !options.light ? survivingOwnedGroups(ownPids) : [];
|
|
3195
|
+
const groups = ownPids.length && !options.light ? survivingOwnedGroups(ownPids, listing) : [];
|
|
2913
3196
|
const serviceStates = {};
|
|
2914
3197
|
for (const service of Object.values(worldInstance.services)) {
|
|
2915
3198
|
serviceStates[service.id] = service.type === 'external' ? 'external'
|
|
@@ -3771,7 +4054,7 @@ export async function runWorld(configId, command, options = {}) {
|
|
|
3771
4054
|
if (command.length === 0)
|
|
3772
4055
|
throw new Error('Missing command after --');
|
|
3773
4056
|
if (!options.keep)
|
|
3774
|
-
return superviseWorldRun(configId, command, options);
|
|
4057
|
+
return (await import("./run-task.js")).superviseWorldRun(configId, command, options);
|
|
3775
4058
|
return runWorldOwned(configId, command, options);
|
|
3776
4059
|
}
|
|
3777
4060
|
/** Internal task entry. The lifetime owner exists before admission/startup. Cancellation during
|
|
@@ -3878,13 +4161,13 @@ export function listWorlds(root = process.cwd()) {
|
|
|
3878
4161
|
})
|
|
3879
4162
|
.sort((a, b) => a.name.localeCompare(b.name));
|
|
3880
4163
|
}
|
|
3881
|
-
function worldAttachedCommandEnv(name, root, verbose = false) {
|
|
3882
|
-
const status = statusWorld(name, root);
|
|
4164
|
+
function worldAttachedCommandEnv(name, root, verbose = false, instance) {
|
|
4165
|
+
const status = instance ?? statusWorld(name, root);
|
|
3883
4166
|
const sealed = status.env.VOLTER_WORLD_MODE === 'sealed' || status.env[WORLD_NETWORK_POLICY_ENV] !== undefined;
|
|
3884
4167
|
const baseEnv = { ...admittedEnv(stripEnvOf(status.env)), ...status.env };
|
|
3885
4168
|
let proxyEnv = {};
|
|
3886
4169
|
try {
|
|
3887
|
-
const proxy =
|
|
4170
|
+
const proxy = ensureWorldProxyForCommand(name, root, [admittedEnv(stripEnvOf(status.env)), status.env], instance);
|
|
3888
4171
|
if (proxy?.url) {
|
|
3889
4172
|
proxyEnv = proxy.env;
|
|
3890
4173
|
// a host with no raw sockets (a browser tab) runs no proxy: the injector holds the policy there, as at `up`
|
|
@@ -3996,7 +4279,7 @@ export async function attachWorld(name, command, root = process.cwd(), options =
|
|
|
3996
4279
|
assertSafeWorldName(name);
|
|
3997
4280
|
const resolvedRoot = resolve(root);
|
|
3998
4281
|
checkedOwner(options.owner);
|
|
3999
|
-
const attached = worldAttachedCommandEnv(name, resolvedRoot, options.verbose);
|
|
4282
|
+
const attached = worldAttachedCommandEnv(name, resolvedRoot, options.verbose, options.instance);
|
|
4000
4283
|
const record = withFileLock(instanceLockFile(resolvedRoot, name), () => {
|
|
4001
4284
|
if (existsSync(stoppingFile(resolvedRoot, name)) || existsSync(bootingFile(resolvedRoot, name))) {
|
|
4002
4285
|
throw new Error(`World "${name}" is changing lifecycle; retry after it finishes`);
|
|
@@ -4004,7 +4287,7 @@ export async function attachWorld(name, command, root = process.cwd(), options =
|
|
|
4004
4287
|
const current = statusWorld(name, resolvedRoot);
|
|
4005
4288
|
// Which of the two it was, with what was read: "stopped" and "another instance" are different repairs.
|
|
4006
4289
|
if (!current.running)
|
|
4007
|
-
throw new Error(`World "${name}" is not running while attaching: none of its recorded processes is live and its own (recorded pids ${readPids(current.pidsFile).join(', ') || 'none'}; live and its own: none)`);
|
|
4290
|
+
throw new Error(`World "${name}" is not running while attaching: none of its recorded processes is live and its own (recorded pids ${readPids(current.pidsFile).join(', ') || 'none'}; live and its own: none); ${describeWorldLifecycle(resolvedRoot, name, current.createdAt)})`);
|
|
4008
4291
|
if (current.createdAt !== attached.instance.createdAt)
|
|
4009
4292
|
throw new Error(`World "${name}" changed while attaching: its instance was created ${attached.instance.createdAt} when the command's environment was read and ${current.createdAt} now`);
|
|
4010
4293
|
// Attaching to a World whose services died is allowed (a command may not need them), but never silent.
|
|
@@ -4016,7 +4299,7 @@ export async function attachWorld(name, command, root = process.cwd(), options =
|
|
|
4016
4299
|
process.stderr.write(`World "${name}": service ${id} is not running${end ? `: it ${describeServiceEnd(end)}` : ''}${service?.log ? `\n log: ${service.log}` : ''}\n`);
|
|
4017
4300
|
}
|
|
4018
4301
|
return createConsumer(resolvedRoot, name, current.createdAt, 'attach', options.owner ?? current.lifecycle?.owner ?? current.resources?.owner, { completionRequired: true });
|
|
4019
|
-
});
|
|
4302
|
+
}, { operation: 'instance attach' });
|
|
4020
4303
|
let timer;
|
|
4021
4304
|
let retired = true;
|
|
4022
4305
|
try {
|
|
@@ -4214,6 +4497,7 @@ export async function shellWorld(name, root = process.cwd()) {
|
|
|
4214
4497
|
}
|
|
4215
4498
|
else if (opensslAvailable()) {
|
|
4216
4499
|
try {
|
|
4500
|
+
const { startRedirectProxy } = await import("./redirect-proxy.js");
|
|
4217
4501
|
proxy = await startRedirectProxy({ env, trustEnv: [admittedEnv(stripEnvOf(env)), env], tlsDir: tlsDir(resolvedRoot, name) });
|
|
4218
4502
|
Object.assign(env, proxy.proxyEnv());
|
|
4219
4503
|
process.stderr.write(`world '${name}' active in a subshell — vendor calls hit the twins (ambient proxy ${proxy.url}). type 'exit' to leave.\n`);
|
|
@@ -4257,6 +4541,9 @@ function proxyDaemonDeadlineMs() {
|
|
|
4257
4541
|
* reuses a live daemon. Returns its proxy env, or null if openssl is missing (env-only fallback). The
|
|
4258
4542
|
* daemon is recorded in `proxy.json` and stopped by `down`. */
|
|
4259
4543
|
export function ensureWorldProxy(name, root = process.cwd(), caller = process.env) {
|
|
4544
|
+
return ensureWorldProxyForCommand(name, root, caller);
|
|
4545
|
+
}
|
|
4546
|
+
function ensureWorldProxyForCommand(name, root, caller, instance) {
|
|
4260
4547
|
assertSafeWorldName(name);
|
|
4261
4548
|
const resolvedRoot = resolve(root);
|
|
4262
4549
|
return withFileLock(instanceLockFile(resolvedRoot, name), () => {
|
|
@@ -4265,7 +4552,7 @@ export function ensureWorldProxy(name, root = process.cwd(), caller = process.en
|
|
|
4265
4552
|
throw new Error(`World "${name}" is being stopped`);
|
|
4266
4553
|
if (liveBootingClaim(bootingFile(resolvedRoot, name)))
|
|
4267
4554
|
throw new Error(`World "${name}" is being booted`);
|
|
4268
|
-
if (!statusWorld(name, resolvedRoot, { readOnly: true }).running)
|
|
4555
|
+
if (!instance && !statusWorld(name, resolvedRoot, { readOnly: true }).running)
|
|
4269
4556
|
throw new Error(`World "${name}" is not running`);
|
|
4270
4557
|
return ensureWorldProxyUnlocked(name, resolvedRoot, caller);
|
|
4271
4558
|
});
|
|
@@ -4483,6 +4770,7 @@ export async function runProxyDaemon(name, root = process.cwd(), envFile) {
|
|
|
4483
4770
|
throw new Error(`World "${name}" is not running`);
|
|
4484
4771
|
env = { ...admittedEnv(stripEnvOf(status.env)), ...status.env };
|
|
4485
4772
|
}
|
|
4773
|
+
const { startRedirectProxy } = await import("./redirect-proxy.js");
|
|
4486
4774
|
const proxy = await startRedirectProxy({ env, envLoader, tlsDir: tlsDir(resolvedRoot, name), note: (line) => process.stderr.write(`${line}\n`) });
|
|
4487
4775
|
const identity = await spawnIdentity(process.pid);
|
|
4488
4776
|
if (!identity.start) {
|
|
@@ -4516,8 +4804,8 @@ export async function runProxyDaemon(name, root = process.cwd(), envFile) {
|
|
|
4516
4804
|
process.on('SIGTERM', stop);
|
|
4517
4805
|
process.on('SIGINT', stop);
|
|
4518
4806
|
}
|
|
4519
|
-
/**
|
|
4520
|
-
*
|
|
4807
|
+
/** Failed boot's proxy retirement; normal down retires it with the other recorded children
|
|
4808
|
+
* and clears session trust only after confirming exit. */
|
|
4521
4809
|
async function stopWorldProxy(root, name, handles) {
|
|
4522
4810
|
const state = readProxyState(root, name);
|
|
4523
4811
|
if (state && isAlive(state.pid)) {
|
|
@@ -4532,14 +4820,11 @@ async function stopWorldProxy(root, name, handles) {
|
|
|
4532
4820
|
await signalOwnedProcess(identity, 'SIGTERM');
|
|
4533
4821
|
}
|
|
4534
4822
|
}
|
|
4535
|
-
|
|
4536
|
-
|
|
4537
|
-
|
|
4538
|
-
|
|
4539
|
-
|
|
4540
|
-
rmSync(proxyEnvFile(root, name), { force: true });
|
|
4541
|
-
}
|
|
4542
|
-
catch { /* ignore */ }
|
|
4823
|
+
clearWorldProxyFiles(root, name);
|
|
4824
|
+
}
|
|
4825
|
+
function clearWorldProxyFiles(root, name) {
|
|
4826
|
+
rmSync(proxyStateFile(root, name), { force: true });
|
|
4827
|
+
rmSync(proxyEnvFile(root, name), { force: true });
|
|
4543
4828
|
tearDownCa(tlsDir(root, name));
|
|
4544
4829
|
}
|
|
4545
4830
|
/**
|