@cotal-ai/cli 0.72.1 → 0.76.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/dist/commands/ext.d.ts.map +1 -1
- package/dist/commands/ext.js +32 -18
- package/dist/commands/ext.js.map +1 -1
- package/dist/commands/service.d.ts.map +1 -1
- package/dist/commands/service.js +36 -31
- package/dist/commands/service.js.map +1 -1
- package/dist/commands/setup.d.ts.map +1 -1
- package/dist/commands/setup.js +23 -19
- package/dist/commands/setup.js.map +1 -1
- package/dist/commands/spawn.d.ts.map +1 -1
- package/dist/commands/spawn.js +74 -28
- package/dist/commands/spawn.js.map +1 -1
- package/dist/commands/status.js +34 -27
- package/dist/commands/status.js.map +1 -1
- package/dist/commands/sync.d.ts.map +1 -1
- package/dist/commands/sync.js +6 -6
- package/dist/commands/sync.js.map +1 -1
- package/dist/commands/up.d.ts.map +1 -1
- package/dist/commands/up.js +89 -120
- package/dist/commands/up.js.map +1 -1
- package/dist/console/mesh.js +1 -1
- package/dist/console/mesh.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -1
- package/dist/index.js.map +1 -1
- package/dist/lib/control.d.ts.map +1 -1
- package/dist/lib/control.js +1 -0
- package/dist/lib/control.js.map +1 -1
- package/dist/lib/manifest/resolve.js +8 -4
- package/dist/lib/manifest/resolve.js.map +1 -1
- package/dist/lib/self-exec.d.ts.map +1 -1
- package/dist/lib/self-exec.js +12 -25
- package/dist/lib/self-exec.js.map +1 -1
- package/dist/lib/status.d.ts +8 -2
- package/dist/lib/status.d.ts.map +1 -1
- package/dist/lib/status.js +24 -5
- package/dist/lib/status.js.map +1 -1
- package/package.json +4 -4
package/dist/commands/up.js
CHANGED
|
@@ -2,7 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { createHash, randomUUID } from "node:crypto";
|
|
3
3
|
import { createConnection, createServer } from "node:net";
|
|
4
4
|
import { hostname } from "node:os";
|
|
5
|
-
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync, readSync, closeSync, lstatSync,
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync, readSync, closeSync, lstatSync, realpathSync, readdirSync, } from "node:fs";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
7
|
import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, ttlBuckets, standaloneConnectOpts, requireBrokerFloor, parseServerVersion, presenceBucket, seedChannelRegistry, ensureDefaultDeliveryClass, LEASE_TTL_MS, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
|
|
8
8
|
import { connect } from "@nats-io/transport-node";
|
|
@@ -903,8 +903,19 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
903
903
|
process.exit(1);
|
|
904
904
|
}
|
|
905
905
|
}
|
|
906
|
+
const restored = resumeAttempt ? pendingRestores.get(resumeAttempt) : undefined;
|
|
907
|
+
const boundListener = restored ? {
|
|
908
|
+
serverName: restored.serverName,
|
|
909
|
+
serverNonce: restored.serverNonce,
|
|
910
|
+
onSpawn: (pid, startedAt) => bindSpawnedRestoreListener(restored, pid, startedAt, startupLock),
|
|
911
|
+
verify: async () => { await provePreparedRestoreListener(restored); },
|
|
912
|
+
} : ordinaryAttempt ? {
|
|
913
|
+
serverName: ordinaryAttempt.serverName,
|
|
914
|
+
serverNonce: ordinaryAttempt.serverNonce,
|
|
915
|
+
onSpawn: (pid, startedAt) => bindSpawnedOrdinaryResumeListener(ordinaryAttempt, pid, startedAt, startupLock),
|
|
916
|
+
verify: async () => { await verifySpawnedOrdinaryListener(ordinaryAttempt); },
|
|
917
|
+
} : undefined;
|
|
906
918
|
if (values.detach) {
|
|
907
|
-
const restored = resumeAttempt ? pendingRestores.get(resumeAttempt) : undefined;
|
|
908
919
|
const { pid, source, authService, controlPlane, delivery, manager } = await startMeshDetached({
|
|
909
920
|
transport,
|
|
910
921
|
server,
|
|
@@ -925,21 +936,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
925
936
|
noManager,
|
|
926
937
|
resumeAttempt,
|
|
927
938
|
resumeCommitToken: restored?.managerCommit?.durableCommitToken ?? ordinaryAttempt?.managerCommit?.durableCommitToken,
|
|
928
|
-
|
|
929
|
-
boundListener: {
|
|
930
|
-
serverName: restored.serverName,
|
|
931
|
-
serverNonce: restored.serverNonce,
|
|
932
|
-
onSpawn: (pid, startedAt) => bindSpawnedRestoreListener(restored, pid, startedAt, startupLock),
|
|
933
|
-
verify: async () => { await provePreparedRestoreListener(restored); },
|
|
934
|
-
},
|
|
935
|
-
} : ordinaryAttempt ? {
|
|
936
|
-
boundListener: {
|
|
937
|
-
serverName: ordinaryAttempt.serverName,
|
|
938
|
-
serverNonce: ordinaryAttempt.serverNonce,
|
|
939
|
-
onSpawn: (pid, startedAt) => bindSpawnedOrdinaryResumeListener(ordinaryAttempt, pid, startedAt, startupLock),
|
|
940
|
-
verify: async () => { await verifySpawnedOrdinaryListener(ordinaryAttempt); },
|
|
941
|
-
},
|
|
942
|
-
} : {}),
|
|
939
|
+
boundListener,
|
|
943
940
|
});
|
|
944
941
|
// Transport policy is committed inside startMeshDetached before delivery launch (S5+S9).
|
|
945
942
|
console.log(c.dim(`Started nats-server (${source}).`));
|
|
@@ -979,7 +976,6 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
979
976
|
assertServesDialHost(transport, new URL(server).hostname);
|
|
980
977
|
const setup = useAuth ? await authSetup(storeDir, server, space, host, wantUser ? { idpUrl: values.idp } : undefined, transport, values["rotate-sys"], maxFileStore) : undefined;
|
|
981
978
|
const port = Number(new URL(server).port) || 4222;
|
|
982
|
-
const restored = resumeAttempt ? pendingRestores.get(resumeAttempt) : undefined;
|
|
983
979
|
// Both modes go through a RENDERER, never bare CLI flags. Open mode used to start from
|
|
984
980
|
// `-js -sd … -p … -a …`, which never called a renderer at all — so the required transport union
|
|
985
981
|
// protected the auth path and was silent on the open one, and a cert/key pair passed to an
|
|
@@ -987,9 +983,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
987
983
|
const confPath = setup ? setup.confPath : writeOpenBrokerConf(storeDir, { port, host, transport, maxFileStore });
|
|
988
984
|
const natsArgs = [
|
|
989
985
|
"-c", confPath,
|
|
990
|
-
...(
|
|
991
|
-
: ordinaryAttempt ? ["--name", ordinaryAttempt.serverName]
|
|
992
|
-
: []),
|
|
986
|
+
...(boundListener ? ["--name", boundListener.serverName] : []),
|
|
993
987
|
];
|
|
994
988
|
const { bin, source } = await resolveNatsServer();
|
|
995
989
|
console.log(c.dim(`Starting nats-server (JetStream, ${useAuth ? "JWT auth" : "OPEN/no-auth"}, ${source}) - store: ${storeDir}, bind: ${host}`));
|
|
@@ -997,29 +991,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
997
991
|
const listenerStartedAt = new Date().toISOString();
|
|
998
992
|
const child = spawn(bin, natsArgs, { stdio: "inherit" });
|
|
999
993
|
let activationFinished = !resumeAttempt;
|
|
1000
|
-
|
|
1001
|
-
throw new Error("nats-server spawned with no pid");
|
|
1002
|
-
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
1003
|
-
if (restored && process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
|
|
1004
|
-
process.exit(87);
|
|
1005
|
-
if (restored)
|
|
1006
|
-
try {
|
|
1007
|
-
bindSpawnedRestoreListener(restored, child.pid ?? 0, listenerStartedAt, startupLock);
|
|
1008
|
-
}
|
|
1009
|
-
catch (error) {
|
|
1010
|
-
await stopUnboundRestoreListener(child);
|
|
1011
|
-
removeMatchingNatsPid(child.pid ?? 0);
|
|
1012
|
-
throw error;
|
|
1013
|
-
}
|
|
1014
|
-
if (ordinaryAttempt)
|
|
1015
|
-
try {
|
|
1016
|
-
bindSpawnedOrdinaryResumeListener(ordinaryAttempt, child.pid ?? 0, listenerStartedAt, startupLock);
|
|
1017
|
-
}
|
|
1018
|
-
catch (error) {
|
|
1019
|
-
await stopUnboundRestoreListener(child);
|
|
1020
|
-
removeMatchingNatsPid(child.pid ?? 0);
|
|
1021
|
-
throw error;
|
|
1022
|
-
}
|
|
994
|
+
await recordSpawnedListener(child, listenerStartedAt, boundListener);
|
|
1023
995
|
releaseStartupLock();
|
|
1024
996
|
child.on("error", (err) => {
|
|
1025
997
|
console.error(c.red(`Failed to start nats-server: ${err.message}`));
|
|
@@ -1072,6 +1044,8 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1072
1044
|
// The broker is gone — drop it from the registry (and the `current` pointer if it was the default)
|
|
1073
1045
|
// so a later `cotal spawn` doesn't try to join a dead mesh.
|
|
1074
1046
|
child.on("exit", async (code, signal) => {
|
|
1047
|
+
if (failedListeners.has(child))
|
|
1048
|
+
return;
|
|
1075
1049
|
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
1076
1050
|
// One teardown at a time: a stop started while another is in flight is refused the reservation
|
|
1077
1051
|
// that one holds, so wait for a Ctrl-C teardown already running instead of racing it.
|
|
@@ -1114,20 +1088,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1114
1088
|
process.exit(code ?? 1);
|
|
1115
1089
|
}
|
|
1116
1090
|
});
|
|
1117
|
-
|
|
1118
|
-
if (!ready) {
|
|
1119
|
-
child.kill("SIGTERM");
|
|
1120
|
-
const reason = `nats-server did not become ready at ${server}`;
|
|
1121
|
-
// `startupLock` is already released by here on this path (and the helper then takes its own);
|
|
1122
|
-
// passed anyway so the call stays correct if the release above ever moves.
|
|
1123
|
-
markPendingResumeDegraded(resumeAttempt ?? "", reason, startupLock);
|
|
1124
|
-
throw new Error(reason);
|
|
1125
|
-
}
|
|
1126
|
-
const v = await brokerVersion(server, space, setup?.creds);
|
|
1127
|
-
if (belowPresenceSafeBroker(v.version) && v.presenceFileBacked)
|
|
1128
|
-
console.error(c.yellow(`! nats-server ${v.version} is below 2.14.5 and the presence bucket of "${space}" is file-backed: it can latch after a stall and close the mesh to new joins (#1356); put a nats-server 2.14.5 or newer on PATH (it wins over the bundled one)`));
|
|
1129
|
-
if (restored)
|
|
1130
|
-
await provePreparedRestoreListener(restored);
|
|
1091
|
+
await awaitSpawnedListener({ child, server, space, creds: setup?.creds, log: "the terminal output above", boundListener });
|
|
1131
1092
|
{
|
|
1132
1093
|
// A DAEMON THAT DIES UNDER THIS RUNNING BROKER IS RESTARTED HERE (#2469). It exits on its own once
|
|
1133
1094
|
// it cannot reach the broker, and a starved host makes a running broker look unreachable, so it can
|
|
@@ -1294,15 +1255,6 @@ function restoreListenerOwner(pid, nonce, startedAt) {
|
|
|
1294
1255
|
throw new Error("restore listener spawn returned no pid");
|
|
1295
1256
|
return { pid, host: hostname(), startedAt, id: `restore-listener-${nonce}` };
|
|
1296
1257
|
}
|
|
1297
|
-
function removeMatchingNatsPid(pid) {
|
|
1298
|
-
const path = cotalPath("nats.pid");
|
|
1299
|
-
try {
|
|
1300
|
-
const stat = lstatSync(path);
|
|
1301
|
-
if (stat.isFile() && !stat.isSymbolicLink() && readFileSync(path, "utf8") === String(pid))
|
|
1302
|
-
rmSync(path);
|
|
1303
|
-
}
|
|
1304
|
-
catch { /* absent, changed, or not owned by this spawn */ }
|
|
1305
|
-
}
|
|
1306
1258
|
function waitForChildExit(child, timeoutMs) {
|
|
1307
1259
|
if (child.exitCode !== null || child.signalCode !== null)
|
|
1308
1260
|
return Promise.resolve(true);
|
|
@@ -1317,15 +1269,23 @@ function waitForChildExit(child, timeoutMs) {
|
|
|
1317
1269
|
child.once("exit", onExit);
|
|
1318
1270
|
});
|
|
1319
1271
|
}
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
|
|
1272
|
+
/** Listeners {@link stopFailedListener} is stopping. Their launch reports its own error, so the
|
|
1273
|
+
* foreground's broker-exit handler must not report that exit as a crash and exit first. */
|
|
1274
|
+
const failedListeners = new WeakSet();
|
|
1275
|
+
/**
|
|
1276
|
+
* Stop a listener whose launch failed before it served. A signal is not proof of exit, so SIGTERM
|
|
1277
|
+
* escalates to SIGKILL and `nats.pid` is removed only once the exit is observed. A listener that
|
|
1278
|
+
* outlives both keeps its record and fails loud with its pid.
|
|
1279
|
+
*/
|
|
1280
|
+
async function stopFailedListener(child) {
|
|
1281
|
+
failedListeners.add(child);
|
|
1323
1282
|
child.kill("SIGTERM");
|
|
1324
|
-
if (await waitForChildExit(child, 5_000))
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1283
|
+
if (!await waitForChildExit(child, 5_000)) {
|
|
1284
|
+
child.kill("SIGKILL");
|
|
1285
|
+
if (!await waitForChildExit(child, 5_000))
|
|
1286
|
+
throw new Error(`nats-server process ${child.pid ?? "unknown"} did not exit after SIGKILL`);
|
|
1287
|
+
}
|
|
1288
|
+
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
1329
1289
|
}
|
|
1330
1290
|
function bindSpawnedRestoreListener(prepared, pid, startedAt, heldLock) {
|
|
1331
1291
|
bindRestoreListenerOwner(prepared, restoreListenerOwner(pid, prepared.serverNonce, startedAt), heldLock);
|
|
@@ -2093,6 +2053,55 @@ tlsRequired, mgr) {
|
|
|
2093
2053
|
return undefined;
|
|
2094
2054
|
}
|
|
2095
2055
|
}
|
|
2056
|
+
/**
|
|
2057
|
+
* Publish a spawned listener's pid, and bind a maintenance-bound one to its attempt, before anything
|
|
2058
|
+
* waits on it. A launch that dies before its listener serves then leaves `nats.pid` for `cotal down`
|
|
2059
|
+
* and a journal naming the exact process for recovery to adopt or retire. A bind that throws stops
|
|
2060
|
+
* the listener. Both launch modes call it right after the spawn.
|
|
2061
|
+
*/
|
|
2062
|
+
async function recordSpawnedListener(child, startedAt, bound) {
|
|
2063
|
+
if (!child.pid)
|
|
2064
|
+
throw new Error("nats-server spawned with no pid");
|
|
2065
|
+
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
2066
|
+
if (!bound)
|
|
2067
|
+
return;
|
|
2068
|
+
if (process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
|
|
2069
|
+
process.exit(87);
|
|
2070
|
+
try {
|
|
2071
|
+
bound.onSpawn(child.pid, startedAt);
|
|
2072
|
+
}
|
|
2073
|
+
catch (error) {
|
|
2074
|
+
await stopFailedListener(child);
|
|
2075
|
+
throw error;
|
|
2076
|
+
}
|
|
2077
|
+
}
|
|
2078
|
+
/**
|
|
2079
|
+
* Everything between spawning a listener and serving it: readiness, the broker version read and a
|
|
2080
|
+
* bound listener's identity check. Foreground `up` and {@link startMeshDetached} both call it, so a
|
|
2081
|
+
* guard added here reaches both launch modes (#2924).
|
|
2082
|
+
*
|
|
2083
|
+
* A listener that never answers or whose version read throws is stopped and its `nats.pid` removed
|
|
2084
|
+
* before `up` fails. Nothing has recorded the mesh yet, so a listener left running would hold the
|
|
2085
|
+
* port with no registry entry. A bound listener that fails its identity check is kept: its
|
|
2086
|
+
* maintenance journal is already bound to it.
|
|
2087
|
+
*/
|
|
2088
|
+
async function awaitSpawnedListener(l) {
|
|
2089
|
+
const { child, server, space } = l;
|
|
2090
|
+
let facts;
|
|
2091
|
+
try {
|
|
2092
|
+
if (!(await waitReady(server, l.creds)))
|
|
2093
|
+
throw new Error(`nats-server did not become reachable at ${server} - see ${l.log}`);
|
|
2094
|
+
facts = await brokerVersion(server, space, l.creds);
|
|
2095
|
+
}
|
|
2096
|
+
catch (e) {
|
|
2097
|
+
await stopFailedListener(child);
|
|
2098
|
+
throw e;
|
|
2099
|
+
}
|
|
2100
|
+
if (belowPresenceSafeBroker(facts.version) && facts.presenceFileBacked)
|
|
2101
|
+
console.error(c.yellow(`! nats-server ${facts.version} is below 2.14.5 and the presence bucket of "${space}" is file-backed: it can latch after a stall and close the mesh to new joins (#1356); put a nats-server 2.14.5 or newer on PATH (it wins over the bundled one)`));
|
|
2102
|
+
if (l.boundListener)
|
|
2103
|
+
await l.boundListener.verify();
|
|
2104
|
+
}
|
|
2096
2105
|
/**
|
|
2097
2106
|
* Everything a launch runs once its listener answers: the space setup, the user-auth service, the
|
|
2098
2107
|
* mesh record, the transport commit and the control plane. Foreground `up` and
|
|
@@ -2112,14 +2121,7 @@ async function serveReadyListener(l) {
|
|
|
2112
2121
|
await postStart(server, space, setup, l.seedFile);
|
|
2113
2122
|
}
|
|
2114
2123
|
catch (e) {
|
|
2115
|
-
|
|
2116
|
-
child.kill("SIGTERM");
|
|
2117
|
-
}
|
|
2118
|
-
catch { /* already gone */ }
|
|
2119
|
-
try {
|
|
2120
|
-
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
2121
|
-
}
|
|
2122
|
-
catch { /* best effort */ }
|
|
2124
|
+
await stopFailedListener(child);
|
|
2123
2125
|
throw e;
|
|
2124
2126
|
}
|
|
2125
2127
|
}
|
|
@@ -2224,50 +2226,17 @@ opts) {
|
|
|
2224
2226
|
const listenerStartedAt = new Date().toISOString();
|
|
2225
2227
|
const child = spawn(bin, args, { detached: true, stdio: ["ignore", fd, fd] });
|
|
2226
2228
|
closeSync(fd);
|
|
2227
|
-
|
|
2228
|
-
if (!child.pid)
|
|
2229
|
-
throw new Error("nats-server spawned with no pid");
|
|
2230
|
-
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
2231
|
-
if (process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
|
|
2232
|
-
process.exit(87);
|
|
2233
|
-
try {
|
|
2234
|
-
opts.boundListener.onSpawn(child.pid ?? 0, listenerStartedAt);
|
|
2235
|
-
}
|
|
2236
|
-
catch (error) {
|
|
2237
|
-
await stopUnboundRestoreListener(child);
|
|
2238
|
-
removeMatchingNatsPid(child.pid ?? 0);
|
|
2239
|
-
throw error;
|
|
2240
|
-
}
|
|
2241
|
-
}
|
|
2229
|
+
await recordSpawnedListener(child, listenerStartedAt, opts.boundListener);
|
|
2242
2230
|
child.unref();
|
|
2243
2231
|
let tailing = Boolean(opts.onLine);
|
|
2244
2232
|
if (opts.onLine)
|
|
2245
2233
|
tailLines(logPath, startOffset, opts.onLine, () => !tailing);
|
|
2246
|
-
const ready = await waitReady(server, setup?.creds);
|
|
2247
|
-
tailing = false;
|
|
2248
|
-
if (!ready) {
|
|
2249
|
-
child.kill("SIGTERM");
|
|
2250
|
-
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
2251
|
-
throw new Error(`nats-server did not become reachable at ${server} - see ${logPath}`);
|
|
2252
|
-
}
|
|
2253
|
-
let brokerVer;
|
|
2254
2234
|
try {
|
|
2255
|
-
|
|
2256
|
-
}
|
|
2257
|
-
catch (e) {
|
|
2258
|
-
child.kill("SIGTERM");
|
|
2259
|
-
removePidPair(cotalPath("nats.pid"), String(child.pid));
|
|
2260
|
-
throw e;
|
|
2235
|
+
await awaitSpawnedListener({ child, server, space, creds: setup?.creds, log: logPath, boundListener: opts.boundListener });
|
|
2261
2236
|
}
|
|
2262
|
-
|
|
2263
|
-
|
|
2264
|
-
if (!opts.boundListener) {
|
|
2265
|
-
if (!child.pid)
|
|
2266
|
-
throw new Error("nats-server spawned with no pid");
|
|
2267
|
-
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
2237
|
+
finally {
|
|
2238
|
+
tailing = false;
|
|
2268
2239
|
}
|
|
2269
|
-
if (opts.boundListener)
|
|
2270
|
-
await opts.boundListener.verify();
|
|
2271
2240
|
const { authService, controlPlane } = await serveReadyListener({
|
|
2272
2241
|
child, server, space, storeDir, setup, seedFile, transport,
|
|
2273
2242
|
host: opts.host,
|