@cotal-ai/cli 0.57.0 → 0.58.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/up.d.ts.map +1 -1
- package/dist/commands/up.js +145 -30
- package/dist/commands/up.js.map +1 -1
- package/dist/lib/auth-proc.d.ts.map +1 -1
- package/dist/lib/auth-proc.js +21 -17
- package/dist/lib/auth-proc.js.map +1 -1
- package/dist/lib/delivery-proc.d.ts.map +1 -1
- package/dist/lib/delivery-proc.js +11 -10
- package/dist/lib/delivery-proc.js.map +1 -1
- package/dist/lib/manager-proc.d.ts.map +1 -1
- package/dist/lib/manager-proc.js +15 -13
- package/dist/lib/manager-proc.js.map +1 -1
- package/dist/lib/self-exec.d.ts.map +1 -1
- package/dist/lib/self-exec.js +38 -13
- package/dist/lib/self-exec.js.map +1 -1
- package/dist/seed/reconcile.d.ts.map +1 -1
- package/dist/seed/reconcile.js +8 -3
- package/dist/seed/reconcile.js.map +1 -1
- package/package.json +7 -7
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"up.d.ts","sourceRoot":"","sources":["../../src/commands/up.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"up.d.ts","sourceRoot":"","sources":["../../src/commands/up.ts"],"names":[],"mappings":"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"}
|
package/dist/commands/up.js
CHANGED
|
@@ -2,11 +2,11 @@ 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, rmSync, realpathSync, } from "node:fs";
|
|
5
|
+
import { mkdirSync, writeFileSync, readFileSync, existsSync, openSync, statSync, readSync, closeSync, lstatSync, rmSync, realpathSync, readdirSync, } from "node:fs";
|
|
6
6
|
import { dirname, join, resolve } from "node:path";
|
|
7
|
-
import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, standaloneConnectOpts, seedChannelRegistry, ensureDefaultDeliveryClass, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
|
|
7
|
+
import { isReachable, DEFAULT_SERVER, createSpaceAuth, serverConfig, openServerConfig, validateTlsMaterial, mintCreds, mintLifecycleUid, DEV_OWNER, mintConnectionEvictorCreds, mintMembershipObserverCreds, newIdentity, setupSpaceStreams, reconcileSpaceTtls, standaloneConnectOpts, requireBrokerFloor, parseServerVersion, seedChannelRegistry, ensureDefaultDeliveryClass, mkSecretDir, writeSecretFile, } from "@cotal-ai/core";
|
|
8
8
|
import { connect } from "@nats-io/transport-node";
|
|
9
|
-
import { assertSingleSpaceBroker, assertUserAuthInfo, authDir, getSoleSpaceAuth, getSpaceAuth, hasUserAuthState, listSpaceAccounts, preloadSpaceAccounts, putSpaceAuth, clearCurrent, findMesh, getCurrent, loadMeshes, connectionEvictorCredsKey, membershipConfigPath, membershipObserverCredsKey, membershipRwCredsKey, MEMBERSHIP_CONFIG_KIND, MEMBERSHIP_RW_CREDS_KIND, parsePositiveIntegerFlag, recordMesh, meshesForRoot, removeMesh, rotateSystemCreds, setCurrent, staleSystemCreds, SYSTEM_CREDS_FILES, userAuthStateDir, userAuthSpacesOnDisk, workspaceSecretStore, acquireMaintenanceLock, assertStoreIdentity, assessRestoreClaim, beginOrdinaryResume, bindOrdinaryResumeListener, consumeRetiredMaintenance, markOrdinaryResumeActive, markOrdinaryResumeDegraded, replaceDeadOrdinaryResumeListener, localProcessOwnerStatus, readMaintenanceJournal, readMaintenanceResumeDocument, seatCheckpointDir, readStoreIdentity, recordOrdinaryResumeManagerCommit, releaseMaintenanceLock, retireOrdinaryResume, sameStoreIdentity, readBrokerPolicy, writeBrokerPolicy, removeIdentityPin,
|
|
9
|
+
import { assertSingleSpaceBroker, assertUserAuthInfo, authDir, getSoleSpaceAuth, getSpaceAuth, hasUserAuthState, listSpaceAccounts, preloadSpaceAccounts, putSpaceAuth, clearCurrent, findMesh, getCurrent, loadMeshes, connectionEvictorCredsKey, membershipConfigPath, membershipObserverCredsKey, membershipRwCredsKey, MEMBERSHIP_CONFIG_KIND, MEMBERSHIP_RW_CREDS_KIND, parsePositiveIntegerFlag, recordMesh, meshesForRoot, removeMesh, rotateSystemCreds, setCurrent, staleSystemCreds, SYSTEM_CREDS_FILES, userAuthStateDir, userAuthSpacesOnDisk, workspaceSecretStore, acquireMaintenanceLock, assertStoreIdentity, assessRestoreClaim, beginOrdinaryResume, bindOrdinaryResumeListener, consumeRetiredMaintenance, markOrdinaryResumeActive, markOrdinaryResumeDegraded, replaceDeadOrdinaryResumeListener, localProcessOwnerStatus, readMaintenanceJournal, readMaintenanceResumeDocument, seatCheckpointDir, readStoreIdentity, recordOrdinaryResumeManagerCommit, releaseMaintenanceLock, retireOrdinaryResume, sameStoreIdentity, readBrokerPolicy, writeBrokerPolicy, removeIdentityPin, writePidPair, localProcessPath, MANAGER_PIDFILE, assertManagerCanSpare, verifyIdentityPin, isLoopbackHost, } from "@cotal-ai/workspace";
|
|
10
10
|
import { ensureAuthService, resolveAuthProvider, stopAuthService } from "../lib/auth-proc.js";
|
|
11
11
|
import { resolveSpace } from "../lib/status.js";
|
|
12
12
|
import { c } from "../ui.js";
|
|
@@ -584,8 +584,6 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
584
584
|
// Gated on the ATTEMPT, not on holding the lock. The lock is now always held, and this refuses
|
|
585
585
|
// whenever a journal exists — which is precisely the state a resume re-entry is in, so gating
|
|
586
586
|
// it on the lock would refuse the very resume the lock is here to protect.
|
|
587
|
-
if (!resumeAttempt)
|
|
588
|
-
assertOrdinaryUpAllowed(cotalRoot(), values["store-dir"] ? resolve(values["store-dir"]) : cotalPath("nats"));
|
|
589
587
|
let server = values.server ?? DEFAULT_SERVER;
|
|
590
588
|
const host = values.host ?? "127.0.0.1";
|
|
591
589
|
// `--host` is the BIND address; `server` is the URL the readiness probe, the mesh registry, and
|
|
@@ -596,6 +594,46 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
596
594
|
// pair rather than starting a broker nothing can reach.
|
|
597
595
|
if (values.host)
|
|
598
596
|
server = reconcileHostAndServer(values.host, values.server);
|
|
597
|
+
// A same-root repair reopens the store the mesh record names, ONCE, before the ordinary-up
|
|
598
|
+
// assertion below reads --store-dir (#2218): an ordinary `up` on a space whose broker died must
|
|
599
|
+
// not silently fall back to the root's default store while the record still names a custom one.
|
|
600
|
+
// Never runs during a resume attempt, which keeps its own store checks. The reachability probe
|
|
601
|
+
// here is reused as `listenerReachable` further down instead of probing twice.
|
|
602
|
+
let repairProbe;
|
|
603
|
+
if (!resumeAttempt) {
|
|
604
|
+
const repairSpace = values.space ?? resolveSpace(process.cwd());
|
|
605
|
+
const repairRoot = cotalRoot();
|
|
606
|
+
const heldForRepair = loadMeshes().find((m) => m.origin !== "manual" && m.origin !== "catalog" && m.server === server && m.root === repairRoot && m.space === repairSpace);
|
|
607
|
+
if (heldForRepair) {
|
|
608
|
+
repairProbe = await isReachable(server);
|
|
609
|
+
if (!repairProbe) {
|
|
610
|
+
if (values["store-dir"] !== undefined) {
|
|
611
|
+
const requestedStoreDir = resolve(values["store-dir"]);
|
|
612
|
+
if (heldForRepair.storeDir !== undefined && requestedStoreDir !== heldForRepair.storeDir) {
|
|
613
|
+
console.error(c.red(`✗ mesh "${repairSpace}" is recorded at ${server} with store ${heldForRepair.storeDir} - a stopped broker can't change --store-dir (it is fixed at start); repair it with \`cotal up --store-dir ${heldForRepair.storeDir}\`, or \`cotal down\` it first`));
|
|
614
|
+
process.exit(1);
|
|
615
|
+
}
|
|
616
|
+
}
|
|
617
|
+
else if (heldForRepair.storeDir !== undefined) {
|
|
618
|
+
values["store-dir"] = heldForRepair.storeDir;
|
|
619
|
+
console.log(c.dim(`reopening the recorded store ${heldForRepair.storeDir}`));
|
|
620
|
+
}
|
|
621
|
+
else {
|
|
622
|
+
const defaultStore = cotalPath("nats");
|
|
623
|
+
let hasEntries = false;
|
|
624
|
+
try {
|
|
625
|
+
hasEntries = readdirSync(join(defaultStore, "jetstream")).length > 0;
|
|
626
|
+
}
|
|
627
|
+
catch { /* absent: no jetstream dir yet */ }
|
|
628
|
+
if (!hasEntries) {
|
|
629
|
+
console.error(c.yellow(`! mesh "${repairSpace}" was recorded before the store directory was kept, so this repair opens ${defaultStore}; if the previous broker used --store-dir, stop this one with \`cotal down\` and repair with that flag (its Store Directory: line is in ${repairRoot}/.cotal/nats.log)`));
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
if (!resumeAttempt)
|
|
636
|
+
assertOrdinaryUpAllowed(cotalRoot(), values["store-dir"] ? resolve(values["store-dir"]) : cotalPath("nats"));
|
|
599
637
|
const restoredAttempt = resumeAttempt ? pendingRestores.get(resumeAttempt) : undefined;
|
|
600
638
|
if (restoredAttempt?.reentry) {
|
|
601
639
|
if (!restoredAttempt.listenerProof)
|
|
@@ -622,7 +660,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
622
660
|
}
|
|
623
661
|
if (ordinaryAttempt?.journalState === "resume-committed")
|
|
624
662
|
throw new Error(`resume attempt ${resumeAttempt} is manager-committed but has no adoptable bound listener; preserving the commit token and retained suppression`);
|
|
625
|
-
const listenerReachable = await isReachable(server);
|
|
663
|
+
const listenerReachable = repairProbe !== undefined ? repairProbe : await isReachable(server);
|
|
626
664
|
if (resumeAttempt && listenerReachable)
|
|
627
665
|
throw new Error(`resume attempt ${resumeAttempt} refuses the unproven occupied listener at ${server}`);
|
|
628
666
|
if (listenerReachable) {
|
|
@@ -678,6 +716,13 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
678
716
|
console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change --max-file-store (it is fixed at start); \`cotal down\` it first, then \`cotal up --max-file-store ${maxFileStore}\``));
|
|
679
717
|
process.exit(1);
|
|
680
718
|
}
|
|
719
|
+
// The store directory is fixed the same way: an explicit --store-dir on a live refresh can
|
|
720
|
+
// only be refused when it disagrees with what the running broker actually opened. Absent
|
|
721
|
+
// `held.storeDir` (a pre-field record) has nothing to compare against, so it is not checked.
|
|
722
|
+
if (values["store-dir"] !== undefined && held.storeDir !== undefined && resolve(values["store-dir"]) !== held.storeDir) {
|
|
723
|
+
console.error(c.red(`✗ mesh "${held.space}" is already running at ${server} - a running broker can't change --store-dir (it is fixed at start); \`cotal down\` it first, then \`cotal up --store-dir ${resolve(values["store-dir"])}\``));
|
|
724
|
+
process.exit(1);
|
|
725
|
+
}
|
|
681
726
|
// `--max-sessions` is fixed when the manager starts. A refresh reuses a live manager as-is
|
|
682
727
|
// (`ensureManager` returns immediately), then used to persist the requested ceiling anyway.
|
|
683
728
|
// That printed `✓ already running` over an unchanged plane and left MeshEntry lying about it.
|
|
@@ -746,6 +791,9 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
746
791
|
// a same-root caller holds the space's signing material either way.
|
|
747
792
|
reconcileCreds = await mintCreds(spaceAuth, newIdentity(), "provisioner");
|
|
748
793
|
}
|
|
794
|
+
const v = await brokerVersion(server, reconcileCreds);
|
|
795
|
+
if (belowPresenceSafeBroker(v))
|
|
796
|
+
console.error(c.yellow(`! nats-server ${v} is below 2.14.5: its file-backed presence bucket 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)`));
|
|
749
797
|
for (const r of await reconcileSpaceTtls({ servers: server, space: held.space, creds: reconcileCreds }))
|
|
750
798
|
console.log(c.dim(` reconciled ${r.stream} TTL ${r.fromMs === 0 ? "none" : `${r.fromMs}ms`} -> ${r.toMs}ms`));
|
|
751
799
|
}
|
|
@@ -826,7 +874,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
826
874
|
// no transport decision to record — and `recordOurMesh` writes the entry whole, so omitting
|
|
827
875
|
// the field here would erase the requirement on every bare refresh, exactly the way dropping
|
|
828
876
|
// `attachHost` would silently demote the mesh to loopback.
|
|
829
|
-
recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ...(held.maxFileStore !== undefined ? { maxFileStore: held.maxFileStore } : {}), ts: new Date().toISOString() }, "refresh");
|
|
877
|
+
recordOurMesh({ space: held.space, server, root, mode: held.mode, ...(held.tlsRequired !== undefined ? { tlsRequired: held.tlsRequired } : {}), ...(userAuth ? { userAuth } : {}), ...(heldAttachHost ? { attachHost: heldAttachHost } : {}), ...(heldMaxSessions !== undefined ? { maxSessions: heldMaxSessions } : {}), ...(held.maxFileStore !== undefined ? { maxFileStore: held.maxFileStore } : {}), ...(held.storeDir !== undefined ? { storeDir: held.storeDir } : {}), ts: new Date().toISOString() }, "refresh");
|
|
830
878
|
return;
|
|
831
879
|
}
|
|
832
880
|
const who = held ? `mesh "${held.space}" (${held.root})` : "a broker not started here";
|
|
@@ -947,9 +995,9 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
947
995
|
const listenerStartedAt = new Date().toISOString();
|
|
948
996
|
const child = spawn(bin, natsArgs, { stdio: "inherit" });
|
|
949
997
|
let activationFinished = !resumeAttempt;
|
|
950
|
-
if (child.pid)
|
|
951
|
-
|
|
952
|
-
|
|
998
|
+
if (!child.pid)
|
|
999
|
+
throw new Error("nats-server spawned with no pid");
|
|
1000
|
+
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
953
1001
|
if (restored && process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
|
|
954
1002
|
process.exit(87);
|
|
955
1003
|
if (restored)
|
|
@@ -1041,28 +1089,47 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1041
1089
|
process.on("SIGTERM", stop);
|
|
1042
1090
|
// The broker is gone — drop it from the registry (and the `current` pointer if it was the default)
|
|
1043
1091
|
// so a later `cotal spawn` doesn't try to join a dead mesh.
|
|
1044
|
-
child.on("exit", async (code) => {
|
|
1092
|
+
child.on("exit", async (code, signal) => {
|
|
1045
1093
|
removeIdentityPin(cotalPath("nats.pid"));
|
|
1046
1094
|
rmSync(cotalPath("nats.pid"), { force: true });
|
|
1047
1095
|
// Logged, never silently swallowed; the daemon kill runs in stopDelivery's finally regardless.
|
|
1048
1096
|
await stopDelivery(undefined, undefined, space).catch((e) => console.error(`! delivery teardown: ${e.message}`));
|
|
1049
1097
|
await stopManager(undefined, undefined, undefined, space).catch((e) => console.error(`! manager teardown: ${e.message}`));
|
|
1050
1098
|
await stopAuthService(space).catch((e) => console.error(`! auth teardown: ${e.message}`));
|
|
1051
|
-
//
|
|
1052
|
-
//
|
|
1053
|
-
//
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1058
|
-
|
|
1059
|
-
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1099
|
+
// Unrecording (and the exit code below) both depend on WHY the broker is gone. `stopping` is
|
|
1100
|
+
// true only for the intended shutdown `stop()` drives (Ctrl-C, SIGTERM) — everything else here
|
|
1101
|
+
// (a crash, an OOM kill, a signal from outside `up`) leaves the record and its store in place.
|
|
1102
|
+
if (stopping) {
|
|
1103
|
+
// Only unrecord if the registry still points at THIS broker. A newer broker for the same space
|
|
1104
|
+
// (a concurrent `up`, or a different-port re-up that recorded after us) may have replaced our
|
|
1105
|
+
// record — removing by name would clobber the live winner and hide it from the registry.
|
|
1106
|
+
// …and only if it is still OUR kind of record. A concurrent `cotal meshes add --force` can
|
|
1107
|
+
// replace it with a hand-registered one carrying the same server + root; that record outlives
|
|
1108
|
+
// this broker by design (it is the operator's, and only they remove it), so unrecording it on
|
|
1109
|
+
// our exit would delete a registration this process never owned.
|
|
1110
|
+
const mine = findMesh(space);
|
|
1111
|
+
if (mine && mine.origin !== "manual" && mine.server === server && mine.root === cotalRoot()) {
|
|
1112
|
+
removeMesh(space);
|
|
1113
|
+
if (getCurrent() === space)
|
|
1114
|
+
clearCurrent();
|
|
1115
|
+
}
|
|
1116
|
+
if (activationFinished)
|
|
1117
|
+
process.exit(code ?? 0);
|
|
1118
|
+
}
|
|
1119
|
+
else {
|
|
1120
|
+
// The record is kept ON PURPOSE: it still names the store this broker opened, so a repair
|
|
1121
|
+
// `up` reopens that SAME store instead of falling back to the root's default, and a
|
|
1122
|
+
// supervising unit's restart takes the refresh path against the recorded store rather than
|
|
1123
|
+
// racing a second broker into existence. Foreground stdio is inherited (its own log lines
|
|
1124
|
+
// are already on this terminal, above), so there is no `.cotal/nats.log` to name here.
|
|
1125
|
+
const detail = signal ? `signal ${signal}` : `code ${code}`;
|
|
1126
|
+
console.error(c.red(`✗ nats-server exited unexpectedly (${detail}); the mesh "${space}" stays recorded (\`cotal meshes\` lists it offline); log: see the terminal output above; repair: \`cotal up --server ${server} --space ${space}\``));
|
|
1127
|
+
// A foreground broker under `Restart=on-failure` must exit non-zero on a crash so the unit
|
|
1128
|
+
// restarts it; exiting 0 here (as an unconditional `code ?? 0` used to) looked like a clean
|
|
1129
|
+
// stop and left the unit sitting dead.
|
|
1130
|
+
if (activationFinished)
|
|
1131
|
+
process.exit(code ?? 1);
|
|
1063
1132
|
}
|
|
1064
|
-
if (activationFinished)
|
|
1065
|
-
process.exit(code ?? 0);
|
|
1066
1133
|
});
|
|
1067
1134
|
const ready = await waitReady(server, setup?.creds);
|
|
1068
1135
|
if (!ready) {
|
|
@@ -1073,6 +1140,9 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1073
1140
|
markPendingResumeDegraded(resumeAttempt ?? "", reason, startupLock);
|
|
1074
1141
|
throw new Error(reason);
|
|
1075
1142
|
}
|
|
1143
|
+
const v = await brokerVersion(server, setup?.creds);
|
|
1144
|
+
if (belowPresenceSafeBroker(v))
|
|
1145
|
+
console.error(c.yellow(`! nats-server ${v} is below 2.14.5: its file-backed presence bucket 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)`));
|
|
1076
1146
|
if (restored)
|
|
1077
1147
|
await provePreparedRestoreListener(restored);
|
|
1078
1148
|
// Listener ready — commit the transport decision (S5: not before start).
|
|
@@ -1110,6 +1180,7 @@ async function runUp(args, inheritedLock, onAdopt) {
|
|
|
1110
1180
|
...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
|
|
1111
1181
|
...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
|
|
1112
1182
|
...(maxFileStore !== undefined ? { maxFileStore } : {}),
|
|
1183
|
+
storeDir,
|
|
1113
1184
|
ts: new Date().toISOString(),
|
|
1114
1185
|
}, "started");
|
|
1115
1186
|
// Bring up the delivery daemon WITH the server (auth mode only — it self-gates on `.cotal/auth`).
|
|
@@ -1476,6 +1547,7 @@ async function resumeProvenOrdinaryListener(pending, heldLock) {
|
|
|
1476
1547
|
...(adoptAttachHost ? { attachHost: adoptAttachHost } : {}),
|
|
1477
1548
|
...(adoptMaxSessions !== undefined ? { maxSessions: adoptMaxSessions } : {}),
|
|
1478
1549
|
...(pending.maxFileStore !== undefined ? { maxFileStore: pending.maxFileStore } : {}),
|
|
1550
|
+
storeDir: resolve(pending.storeDir),
|
|
1479
1551
|
ts: new Date().toISOString(),
|
|
1480
1552
|
}, "started");
|
|
1481
1553
|
const controlPlane = await startDeliveryWithBroker(pending.space, pending.server, adoptedTlsRequired(pending.root), {
|
|
@@ -1521,6 +1593,7 @@ async function resumeProvenRestoreListener(prepared, heldLock) {
|
|
|
1521
1593
|
...(svc.userAuth ? { userAuth: svc.userAuth } : {}),
|
|
1522
1594
|
...(restoreAttachHost ? { attachHost: restoreAttachHost } : {}),
|
|
1523
1595
|
...(restoreMaxSessions !== undefined ? { maxSessions: restoreMaxSessions } : {}),
|
|
1596
|
+
storeDir: resolve(prepared.targetPath),
|
|
1524
1597
|
ts: new Date().toISOString(),
|
|
1525
1598
|
}, "started");
|
|
1526
1599
|
const controlPlane = await startDeliveryWithBroker(prepared.space, prepared.server, adoptedTlsRequired(prepared.root), {
|
|
@@ -2041,8 +2114,9 @@ opts) {
|
|
|
2041
2114
|
const child = spawn(bin, args, { detached: true, stdio: ["ignore", fd, fd] });
|
|
2042
2115
|
closeSync(fd);
|
|
2043
2116
|
if (opts.boundListener) {
|
|
2044
|
-
|
|
2045
|
-
|
|
2117
|
+
if (!child.pid)
|
|
2118
|
+
throw new Error("nats-server spawned with no pid");
|
|
2119
|
+
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
2046
2120
|
if (process.env.COTAL_SMOKE_EXIT_AFTER_RESTORE_LISTENER_SPAWN === "1")
|
|
2047
2121
|
process.exit(87);
|
|
2048
2122
|
try {
|
|
@@ -2067,9 +2141,24 @@ opts) {
|
|
|
2067
2141
|
rmSync(cotalPath("nats.pid"), { force: true });
|
|
2068
2142
|
throw new Error(`nats-server did not become reachable at ${server} - see ${logPath}`);
|
|
2069
2143
|
}
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2144
|
+
let brokerVer;
|
|
2145
|
+
try {
|
|
2146
|
+
brokerVer = await brokerVersion(server, setup?.creds);
|
|
2147
|
+
}
|
|
2148
|
+
catch (e) {
|
|
2149
|
+
child.kill("SIGTERM");
|
|
2150
|
+
if (opts.boundListener)
|
|
2151
|
+
removeIdentityPin(cotalPath("nats.pid"));
|
|
2152
|
+
rmSync(cotalPath("nats.pid"), { force: true });
|
|
2153
|
+
throw e;
|
|
2154
|
+
}
|
|
2155
|
+
if (belowPresenceSafeBroker(brokerVer))
|
|
2156
|
+
console.error(c.yellow(`! nats-server ${brokerVer} is below 2.14.5: its file-backed presence bucket 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)`));
|
|
2157
|
+
if (!opts.boundListener) {
|
|
2158
|
+
if (!child.pid)
|
|
2159
|
+
throw new Error("nats-server spawned with no pid");
|
|
2160
|
+
writePidPair(cotalPath("nats.pid"), child.pid); // #969/#1238: publish the pair by rename, never torn
|
|
2161
|
+
}
|
|
2073
2162
|
if (opts.boundListener)
|
|
2074
2163
|
await opts.boundListener.verify();
|
|
2075
2164
|
// POST-START MUST NOT LEAVE AN ORPHAN LISTENER.
|
|
@@ -2125,6 +2214,7 @@ opts) {
|
|
|
2125
2214
|
...(effectiveAttachHost ? { attachHost: effectiveAttachHost } : {}),
|
|
2126
2215
|
...(effectiveMaxSessions !== undefined ? { maxSessions: effectiveMaxSessions } : {}),
|
|
2127
2216
|
...(opts.maxFileStore !== undefined ? { maxFileStore: opts.maxFileStore } : {}),
|
|
2217
|
+
storeDir,
|
|
2128
2218
|
ts: new Date().toISOString(),
|
|
2129
2219
|
}, "started");
|
|
2130
2220
|
// Commit policy BEFORE delivery launch (S9). Listener is proved; refuse paths never reach here.
|
|
@@ -2356,6 +2446,31 @@ async function waitReady(server, creds) {
|
|
|
2356
2446
|
}
|
|
2357
2447
|
return false;
|
|
2358
2448
|
}
|
|
2449
|
+
// SPEC §13.12: reads the connected broker's version, and refuses loud below the 2.12 floor
|
|
2450
|
+
// before any pidfile or provisioning touches it (the same gate `requireBrokerFloor` enforces
|
|
2451
|
+
// on every other control-surface connection).
|
|
2452
|
+
async function brokerVersion(server, creds) {
|
|
2453
|
+
const nc = await connect({ servers: server, ...standaloneConnectOpts({ creds, tls: false }) });
|
|
2454
|
+
try {
|
|
2455
|
+
requireBrokerFloor(nc);
|
|
2456
|
+
return nc.info?.version ?? "";
|
|
2457
|
+
}
|
|
2458
|
+
finally {
|
|
2459
|
+
await nc.drain();
|
|
2460
|
+
}
|
|
2461
|
+
}
|
|
2462
|
+
// #1356: the presence bucket's file-backed latch is fixed in nats-server 2.14.5. Below that,
|
|
2463
|
+
// `cotal up` warns (never refuses: 2.14.0 meets the SPEC §13.12 floor and is the bundled broker).
|
|
2464
|
+
function belowPresenceSafeBroker(version) {
|
|
2465
|
+
const p = parseServerVersion(version);
|
|
2466
|
+
if (!p)
|
|
2467
|
+
return false;
|
|
2468
|
+
if (p.major !== 2)
|
|
2469
|
+
return p.major < 2;
|
|
2470
|
+
if (p.minor !== 14)
|
|
2471
|
+
return p.minor < 14;
|
|
2472
|
+
return p.patch < 5;
|
|
2473
|
+
}
|
|
2359
2474
|
/** Wildcard binds mean "every interface", so they are NOT an address a client can be told to dial:
|
|
2360
2475
|
* the probe/registry URL stays whatever it already is (loopback by default), which the wildcard
|
|
2361
2476
|
* bind necessarily covers. */
|