@empire-builder-kit/nx 1.0.0-rc.43 → 1.0.0-rc.45
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/package.json +3 -3
- package/src/blueprints/external-sandbox.d.ts +1 -1
- package/src/cli/main.d.ts +11 -5
- package/src/cli/main.js +88 -29
- package/src/container-images.d.ts +1 -1
- package/src/container-images.js +1 -1
- package/src/executors/deployment-plan/deployment-plan.js +5 -1
- package/src/executors/deployment-plan/schema.json +1 -1
- package/src/executors/fleet/fleet.js +4 -0
- package/src/executors/fleet/schema.json +1 -1
- package/src/executors/local-deploy/federated-credential-transport.d.ts +4 -3
- package/src/executors/local-deploy/federated-credential-transport.js +15 -5
- package/src/foundation/cost-estimate.d.ts +8 -3
- package/src/foundation/cost-estimate.js +42 -19
- package/src/foundation/index.d.ts +1 -0
- package/src/foundation/index.js +1 -0
- package/src/foundation/local-private-smoke-access.js +6 -0
- package/src/foundation/logging.d.ts +3 -1
- package/src/foundation/logging.js +8 -1
- package/src/foundation/observability.d.ts +42 -0
- package/src/foundation/observability.js +181 -0
- package/src/foundation/preflight.js +1 -1
- package/src/generators/function/function.js +5 -2
- package/src/generators/observability-infrastructure.d.ts +2 -0
- package/src/generators/observability-infrastructure.js +31 -0
- package/src/generators/observability-policy.d.ts +4 -0
- package/src/generators/observability-policy.js +23 -0
- package/src/generators/preset/dependency-overrides.d.ts +34 -0
- package/src/generators/preset/dependency-overrides.js +35 -1
- package/src/generators/preset/tool-versions.d.ts +2 -2
- package/src/generators/preset/tool-versions.js +2 -2
- package/src/generators/preset/workspace-files-adopter-skills.js +28 -17
- package/src/generators/preset/workspace-files-agent-development.d.ts +9 -0
- package/src/generators/preset/workspace-files-agent-development.js +42 -0
- package/src/generators/preset/workspace-files-thin-deploy.js +12 -3
- package/src/generators/preset/workspace-files-thin-docs.js +10 -22
- package/src/generators/preset/workspace-files.d.ts +3 -0
- package/src/generators/preset/workspace-files.js +93 -8
- package/src/generators/slice/built-in-plan-alarm-identity.d.ts +173 -0
- package/src/generators/slice/built-in-plan-alarm-identity.js +202 -0
- package/src/generators/slice/built-in-plan-api-contract-files.js +136 -0
- package/src/generators/slice/built-in-plan-database-lifecycle-files.js +1 -1
- package/src/generators/slice/built-in-plan-event-contract-literal.d.ts +6 -0
- package/src/generators/slice/built-in-plan-event-contract-literal.js +52 -0
- package/src/generators/slice/built-in-plan-infrastructure-files.js +304 -144
- package/src/generators/slice/built-in-plan-key-value-retention-infrastructure.js +2 -2
- package/src/generators/slice/built-in-plan-metadata-files.js +33 -1
- package/src/generators/slice/built-in-plan-operations-files.js +106 -6
- package/src/generators/slice/built-in-plan-project-files.js +1 -0
- package/src/generators/slice/built-in-plan-release-files.js +2 -43
- package/src/generators/slice/built-in-plan-reliability-files.js +222 -236
- package/src/generators/slice/built-in-plan-shared.d.ts +29 -3
- package/src/generators/slice/built-in-plan-shared.js +29 -2
- package/src/generators/slice/built-in-plan-web-files.js +18 -1
- package/src/generators/slice/built-in-plan.js +1 -0
- package/src/generators/slice/slice.js +2 -0
- package/src/generators/upgrade/upgrade.d.ts +4 -3
- package/src/generators/upgrade/upgrade.js +50 -26
- package/src/generators/workload/frozen-container.d.ts +1 -1
- package/src/generators/workload/service-performance-files.js +22 -5
- package/src/generators/workload/workload.js +5 -1
- package/src/local-dev/dev-command-core.d.ts +24 -3
- package/src/local-dev/dev-command-core.js +124 -12
- package/src/local-dev/errors.d.ts +19 -1
- package/src/local-dev/errors.js +18 -1
- package/src/local-dev/fleet-client.d.ts +64 -6
- package/src/local-dev/fleet-client.js +189 -15
- package/src/local-dev/fleet-process.d.ts +47 -2
- package/src/local-dev/fleet-process.js +248 -31
- package/src/local-dev/fleet-runtime.d.ts +37 -0
- package/src/local-dev/fleet-runtime.js +127 -69
- package/src/local-dev/fleet-session.d.ts +1 -0
- package/src/local-dev/fleet-session.js +42 -3
- package/src/local-dev/fleet-state.d.ts +5 -0
- package/src/local-dev/fleet-state.js +11 -2
- package/src/local-dev/fleet-supervisor.d.ts +6 -0
- package/src/local-dev/fleet-supervisor.js +49 -1
- package/src/local-dev/ports.d.ts +11 -1
- package/src/local-dev/ports.js +78 -38
- package/src/local-dev/slice-runner.d.ts +19 -1
- package/src/local-dev/slice-runner.js +290 -91
- package/src/repository-policy/deployment-plan.d.ts +1 -1
- package/src/repository-policy/deployment-plan.js +16 -1
- package/src/secrets/generated-aws-control-plane.d.ts +11 -1
- package/src/secrets/generated-aws-control-plane.js +92 -29
- package/src/secrets/operator.d.ts +3 -2
- package/src/secrets/operator.js +27 -5
- package/src/supply-chain/generated-files.js +9 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.resolveDevelopmentRouter = resolveDevelopmentRouter;
|
|
4
|
+
exports.createFleetSliceRunnerEffects = createFleetSliceRunnerEffects;
|
|
4
5
|
exports.startFleetRuntime = startFleetRuntime;
|
|
5
6
|
const node_fs_1 = require("node:fs");
|
|
6
7
|
const node_crypto_1 = require("node:crypto");
|
|
@@ -71,6 +72,111 @@ async function resolveDevelopmentRouter(request) {
|
|
|
71
72
|
throw new errors_js_1.LocalDevContractError('foundation-unavailable', `Development Foundation discovery failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
72
73
|
}
|
|
73
74
|
}
|
|
75
|
+
/** One Slice's Fleet process effects: a coordinating runner per launch,
|
|
76
|
+
* generation-bound restart and quiescence, and cleanup whose retry output
|
|
77
|
+
* joins the Slice log store. startFleetRuntime and the real-runner
|
|
78
|
+
* quiescence specs share this wiring, so a dropped effect fails those specs. */
|
|
79
|
+
function createFleetSliceRunnerEffects(input, daemon = {}) {
|
|
80
|
+
const { logStore, name } = input;
|
|
81
|
+
let activeRunner;
|
|
82
|
+
let activeAttempt = 0;
|
|
83
|
+
const runnersByChild = new WeakMap();
|
|
84
|
+
const project = input.projects[name];
|
|
85
|
+
const nativeProjectRoot = project?.targets?.[input.adapter.target]?.executor ===
|
|
86
|
+
'@empire-builder-kit/nx:dev'
|
|
87
|
+
? (0, node_path_1.join)(input.workspaceRealpath, project.root)
|
|
88
|
+
: undefined;
|
|
89
|
+
const effects = {
|
|
90
|
+
...(nativeProjectRoot
|
|
91
|
+
? {
|
|
92
|
+
nativeDeployment: async (signal) => activeRunner
|
|
93
|
+
? (0, native_completion_js_1.readNativeDevelopmentCompletion)({
|
|
94
|
+
projectRoot: nativeProjectRoot,
|
|
95
|
+
stage: input.stage,
|
|
96
|
+
startedAt: activeRunner.request.createdAt,
|
|
97
|
+
}, signal)
|
|
98
|
+
: 'pending',
|
|
99
|
+
applicationStarted: () => Boolean(activeRunner &&
|
|
100
|
+
(0, fleet_web_start_js_1.hasFleetWebStarted)(activeRunner.requestPath, activeRunner.request)),
|
|
101
|
+
}
|
|
102
|
+
: {}),
|
|
103
|
+
cleanup: async () => {
|
|
104
|
+
if (!activeRunner) {
|
|
105
|
+
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
|
|
106
|
+
}
|
|
107
|
+
try {
|
|
108
|
+
(0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
|
|
109
|
+
}
|
|
110
|
+
catch {
|
|
111
|
+
// A runner that already exited (or whose own cleanup failed)
|
|
112
|
+
// is retried by a separate runner; keep its output in the
|
|
113
|
+
// same Slice log as the in-runner cleanup.
|
|
114
|
+
const attempt = activeAttempt;
|
|
115
|
+
try {
|
|
116
|
+
await (0, slice_runner_js_1.retryFleetSliceRunnerCleanup)(activeRunner, {
|
|
117
|
+
...daemon,
|
|
118
|
+
output: (stream, chunk) => {
|
|
119
|
+
logStore.writeChunk(name, stream, attempt, chunk);
|
|
120
|
+
},
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
finally {
|
|
124
|
+
logStore.flush(name, undefined, attempt);
|
|
125
|
+
}
|
|
126
|
+
(0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
|
|
127
|
+
}
|
|
128
|
+
await (0, slice_runner_js_1.recoverFleetSliceRunnerCleanupsForSlices)({
|
|
129
|
+
fleetId: activeRunner.request.fleetId,
|
|
130
|
+
sessionGeneration: activeRunner.request.sessionGeneration,
|
|
131
|
+
}, [name], input.runtimePaths, daemon);
|
|
132
|
+
},
|
|
133
|
+
launch: (attempt) => {
|
|
134
|
+
activeAttempt = attempt;
|
|
135
|
+
activeRunner = (0, slice_runner_js_1.launchFleetSliceRunner)({
|
|
136
|
+
origin: input.origin,
|
|
137
|
+
parent: input.parent,
|
|
138
|
+
port: input.port,
|
|
139
|
+
runnerGeneration: `${input.sessionGeneration}:${name}:${attempt}:${(0, node_crypto_1.randomUUID)()}`,
|
|
140
|
+
runtimePaths: input.runtimePaths,
|
|
141
|
+
sessionGeneration: input.sessionGeneration,
|
|
142
|
+
slice: name,
|
|
143
|
+
startupPath: input.startupPath,
|
|
144
|
+
workspaceRealpath: input.workspaceRealpath,
|
|
145
|
+
}, daemon);
|
|
146
|
+
runnersByChild.set(activeRunner.child, activeRunner);
|
|
147
|
+
return { attempt, child: activeRunner.child };
|
|
148
|
+
},
|
|
149
|
+
prepareRestart: async () => {
|
|
150
|
+
if (!activeRunner) {
|
|
151
|
+
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
|
|
152
|
+
}
|
|
153
|
+
(0, slice_runner_js_1.requestFleetSliceRunnerRestart)(activeRunner);
|
|
154
|
+
},
|
|
155
|
+
// The runner stops its writer and stays alive with pending
|
|
156
|
+
// cleanup authority; a later stop runs declared cleanup in that
|
|
157
|
+
// runner, so its output reaches this Slice's log store.
|
|
158
|
+
quiesce: async (child) => {
|
|
159
|
+
if (!activeRunner || activeRunner.child !== child) {
|
|
160
|
+
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before quiescence.');
|
|
161
|
+
}
|
|
162
|
+
await (0, fleet_process_js_1.quiesceCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
|
|
163
|
+
},
|
|
164
|
+
runnerGeneration: () => activeRunner?.request.runnerGeneration,
|
|
165
|
+
startTargetExit: (child) => {
|
|
166
|
+
const runner = runnersByChild.get(child);
|
|
167
|
+
return runner
|
|
168
|
+
? (0, slice_runner_js_1.readFleetSliceRunnerState)(runner.statePath).childExit
|
|
169
|
+
: undefined;
|
|
170
|
+
},
|
|
171
|
+
terminate: async (child) => {
|
|
172
|
+
if (!activeRunner || activeRunner.child !== child) {
|
|
173
|
+
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before shutdown.');
|
|
174
|
+
}
|
|
175
|
+
await (0, fleet_process_js_1.terminateCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
return { activeRunner: () => activeRunner, effects };
|
|
179
|
+
}
|
|
74
180
|
async function startFleetRuntime(startupPath, dependencies = {}) {
|
|
75
181
|
const request = (0, fleet_startup_js_1.readFleetStartupRequest)(startupPath);
|
|
76
182
|
if ((0, node_fs_1.realpathSync)(request.workspaceRealpath) !== request.workspaceRealpath) {
|
|
@@ -220,75 +326,27 @@ async function startFleetRuntime(startupPath, dependencies = {}) {
|
|
|
220
326
|
adapters: plan.adapters,
|
|
221
327
|
allocatePort: async (name) => ports.get(name),
|
|
222
328
|
fleetId,
|
|
223
|
-
processFactory: ({ adapter, name, onStateChange, upstream }) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
(0, fleet_web_start_js_1.hasFleetWebStarted)(activeRunner.requestPath, activeRunner.request)),
|
|
245
|
-
}
|
|
246
|
-
: {}),
|
|
247
|
-
cleanup: async () => {
|
|
248
|
-
if (!activeRunner) {
|
|
249
|
-
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
|
|
250
|
-
}
|
|
251
|
-
try {
|
|
252
|
-
(0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
|
|
253
|
-
}
|
|
254
|
-
catch {
|
|
255
|
-
await (0, slice_runner_js_1.retryFleetSliceRunnerCleanup)(activeRunner);
|
|
256
|
-
(0, slice_runner_js_1.assertFleetSliceRunnerCleanup)(activeRunner.statePath, activeRunner.request);
|
|
257
|
-
}
|
|
258
|
-
await (0, slice_runner_js_1.recoverFleetSliceRunnerCleanupsForSlices)({
|
|
259
|
-
fleetId: activeRunner.request.fleetId,
|
|
260
|
-
sessionGeneration: activeRunner.request.sessionGeneration,
|
|
261
|
-
}, [name], runtimePaths);
|
|
262
|
-
},
|
|
263
|
-
launch: (attempt) => {
|
|
264
|
-
activeRunner = (0, slice_runner_js_1.launchFleetSliceRunner)({
|
|
265
|
-
origin,
|
|
266
|
-
parent: processOwner,
|
|
267
|
-
port: ports.get(name),
|
|
268
|
-
runnerGeneration: `${request.sessionGeneration}:${name}:${attempt}:${(0, node_crypto_1.randomUUID)()}`,
|
|
269
|
-
runtimePaths,
|
|
270
|
-
sessionGeneration: request.sessionGeneration,
|
|
271
|
-
slice: name,
|
|
272
|
-
startupPath,
|
|
273
|
-
workspaceRealpath: request.workspaceRealpath,
|
|
274
|
-
});
|
|
275
|
-
return { attempt, child: activeRunner.child };
|
|
276
|
-
},
|
|
277
|
-
prepareRestart: async () => {
|
|
278
|
-
if (!activeRunner) {
|
|
279
|
-
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', `Fleet Slice runner state for ${name} is missing.`);
|
|
280
|
-
}
|
|
281
|
-
(0, slice_runner_js_1.requestFleetSliceRunnerRestart)(activeRunner);
|
|
282
|
-
},
|
|
283
|
-
runnerGeneration: () => activeRunner?.request.runnerGeneration,
|
|
284
|
-
terminate: async (child) => {
|
|
285
|
-
if (!activeRunner || activeRunner.child !== child) {
|
|
286
|
-
throw new errors_js_1.LocalDevContractError('cleanup-incomplete', 'Fleet runner ownership changed before shutdown.');
|
|
287
|
-
}
|
|
288
|
-
await (0, fleet_process_js_1.terminateCoordinatingFleetRunner)(child, activeRunner.request.runnerGeneration);
|
|
289
|
-
},
|
|
290
|
-
});
|
|
291
|
-
},
|
|
329
|
+
processFactory: ({ adapter, name, onStateChange, upstream }) => new fleet_process_js_1.FleetProcessController({
|
|
330
|
+
adapter,
|
|
331
|
+
logStore,
|
|
332
|
+
maxRestartAttempts: request.context.maxRestartAttempts,
|
|
333
|
+
name,
|
|
334
|
+
onStateChange,
|
|
335
|
+
upstream,
|
|
336
|
+
}, createFleetSliceRunnerEffects({
|
|
337
|
+
adapter,
|
|
338
|
+
logStore,
|
|
339
|
+
name,
|
|
340
|
+
origin,
|
|
341
|
+
parent: processOwner,
|
|
342
|
+
port: ports.get(name),
|
|
343
|
+
projects: plan.projects,
|
|
344
|
+
runtimePaths,
|
|
345
|
+
sessionGeneration: request.sessionGeneration,
|
|
346
|
+
stage: request.identity.stage,
|
|
347
|
+
startupPath,
|
|
348
|
+
workspaceRealpath: request.workspaceRealpath,
|
|
349
|
+
}).effects),
|
|
292
350
|
proxy: proxyHost,
|
|
293
351
|
proxyPort,
|
|
294
352
|
routes,
|
|
@@ -8,6 +8,12 @@ const slice_claim_js_1 = require("./slice-claim.js");
|
|
|
8
8
|
function sessionError(classification, message) {
|
|
9
9
|
throw new errors_js_1.LocalDevContractError(classification, message);
|
|
10
10
|
}
|
|
11
|
+
function startupFailure(error) {
|
|
12
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
13
|
+
return error instanceof errors_js_1.LocalDevContractError
|
|
14
|
+
? `${error.classification}: ${message}`
|
|
15
|
+
: message;
|
|
16
|
+
}
|
|
11
17
|
class FleetSession {
|
|
12
18
|
routes;
|
|
13
19
|
#allocatePort;
|
|
@@ -79,6 +85,29 @@ class FleetSession {
|
|
|
79
85
|
this.#operation = result.then(() => undefined, () => undefined);
|
|
80
86
|
return result;
|
|
81
87
|
}
|
|
88
|
+
async #quiesce(names) {
|
|
89
|
+
const failures = [];
|
|
90
|
+
// The complete selection was dependency-validated before this phase.
|
|
91
|
+
// Keep every claim selected until all writers stop, even if one fails.
|
|
92
|
+
for (const name of names) {
|
|
93
|
+
const process = this.#processes.get(name);
|
|
94
|
+
if (!process)
|
|
95
|
+
continue;
|
|
96
|
+
const route = this.#route(name);
|
|
97
|
+
try {
|
|
98
|
+
await process.quiesce();
|
|
99
|
+
}
|
|
100
|
+
catch (error) {
|
|
101
|
+
this.#cleanupStates.set(name, 'failed');
|
|
102
|
+
failures.push(`${name}: ${error instanceof Error ? error.message : String(error)}`);
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
route.selected = true;
|
|
106
|
+
route.state = 'unavailable';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return failures;
|
|
110
|
+
}
|
|
82
111
|
up(names) {
|
|
83
112
|
return this.#serialized(async () => {
|
|
84
113
|
const requested = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'up', names, this.selectedSlices());
|
|
@@ -119,11 +148,14 @@ class FleetSession {
|
|
|
119
148
|
}
|
|
120
149
|
}
|
|
121
150
|
catch (error) {
|
|
122
|
-
const
|
|
151
|
+
const quiesceFailures = await this.#quiesce([...changed].reverse());
|
|
152
|
+
const cleanupFailures = [...quiesceFailures];
|
|
123
153
|
const releasable = [];
|
|
124
154
|
for (const name of [...changed].reverse()) {
|
|
125
155
|
const process = this.#processes.get(name);
|
|
126
156
|
if (process) {
|
|
157
|
+
if (quiesceFailures.length > 0)
|
|
158
|
+
continue;
|
|
127
159
|
try {
|
|
128
160
|
(0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', [name], this.selectedSlices());
|
|
129
161
|
await process.stop({ cleanup: true });
|
|
@@ -161,7 +193,9 @@ class FleetSession {
|
|
|
161
193
|
}
|
|
162
194
|
}
|
|
163
195
|
if (cleanupFailures.length > 0) {
|
|
164
|
-
|
|
196
|
+
// The start failure leads: the persisted failure message is
|
|
197
|
+
// bounded, and the cause must survive the rollback report.
|
|
198
|
+
return sessionError('cleanup-incomplete', `Fleet startup failed (${startupFailure(error)}) and generation-owned rollback was incomplete: ${cleanupFailures.join('; ')}`);
|
|
165
199
|
}
|
|
166
200
|
throw error;
|
|
167
201
|
}
|
|
@@ -214,10 +248,15 @@ class FleetSession {
|
|
|
214
248
|
const results = [];
|
|
215
249
|
const cleanupFailures = [];
|
|
216
250
|
const ordered = (0, contracts_js_1.resolveFleetSliceOperation)(this.#adapters, 'down', names, this.selectedSlices());
|
|
251
|
+
const previousStates = new Map(ordered.map((name) => [name, this.#route(name).state]));
|
|
252
|
+
const quiesceFailures = await this.#quiesce(ordered);
|
|
253
|
+
if (quiesceFailures.length > 0) {
|
|
254
|
+
return sessionError('cleanup-incomplete', `Fleet writers did not quiesce; no cloud cleanup ran and all claims were retained: ${quiesceFailures.join('; ')}`);
|
|
255
|
+
}
|
|
217
256
|
for (const name of ordered) {
|
|
218
257
|
const route = this.#route(name);
|
|
219
258
|
const process = this.#processes.get(name);
|
|
220
|
-
const previousState =
|
|
259
|
+
const previousState = previousStates.get(name);
|
|
221
260
|
if (!route.selected || !process) {
|
|
222
261
|
results.push({
|
|
223
262
|
changed: false,
|
|
@@ -5,6 +5,9 @@ import { type PrivateStateSecurity } from './private-state.js';
|
|
|
5
5
|
import type { FleetRouterSource } from './fleet-configuration.js';
|
|
6
6
|
export declare const FLEET_STATE_SCHEMA_VERSION: 1;
|
|
7
7
|
export declare const FLEET_STATE_FILE: "control-state.json";
|
|
8
|
+
/** A persisted operation failure cause stays one bounded line, so the full
|
|
9
|
+
* state (up to 128 operations) remains inside the CLI result bound. */
|
|
10
|
+
export declare const FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = 1024;
|
|
8
11
|
export declare const FLEET_ROUTE_STATES: readonly ["remote", "starting", "ready", "unavailable"];
|
|
9
12
|
export declare const FLEET_CLEANUP_STATES: readonly ["not-declared", "pending", "succeeded", "failed"];
|
|
10
13
|
export declare const FLEET_OPERATION_STATES: readonly ["accepted", "running", "succeeded", "failed"];
|
|
@@ -28,6 +31,8 @@ export interface FleetOperationStateRecord {
|
|
|
28
31
|
command: FleetMutationCommand;
|
|
29
32
|
completedAt?: string;
|
|
30
33
|
failureClassification?: LocalDevFailureClassification;
|
|
34
|
+
/** Bounded, redacted, single-line cause of a failed operation. */
|
|
35
|
+
failureMessage?: string;
|
|
31
36
|
id: string;
|
|
32
37
|
idempotencyKey: string;
|
|
33
38
|
slices: string[];
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FLEET_OPERATION_STATES = exports.FLEET_CLEANUP_STATES = exports.FLEET_ROUTE_STATES = exports.FLEET_STATE_FILE = exports.FLEET_STATE_SCHEMA_VERSION = void 0;
|
|
3
|
+
exports.FLEET_OPERATION_STATES = exports.FLEET_CLEANUP_STATES = exports.FLEET_ROUTE_STATES = exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = exports.FLEET_STATE_FILE = exports.FLEET_STATE_SCHEMA_VERSION = void 0;
|
|
4
4
|
exports.fleetOwnedSliceNames = fleetOwnedSliceNames;
|
|
5
5
|
exports.fleetOwnsStartingSlice = fleetOwnsStartingSlice;
|
|
6
6
|
exports.validateFleetStateRecord = validateFleetStateRecord;
|
|
@@ -18,6 +18,9 @@ const private_state_js_1 = require("./private-state.js");
|
|
|
18
18
|
const personal_stage_js_1 = require("./personal-stage.js");
|
|
19
19
|
exports.FLEET_STATE_SCHEMA_VERSION = 1;
|
|
20
20
|
exports.FLEET_STATE_FILE = 'control-state.json';
|
|
21
|
+
/** A persisted operation failure cause stays one bounded line, so the full
|
|
22
|
+
* state (up to 128 operations) remains inside the CLI result bound. */
|
|
23
|
+
exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH = 1_024;
|
|
21
24
|
exports.FLEET_ROUTE_STATES = [
|
|
22
25
|
'remote',
|
|
23
26
|
'starting',
|
|
@@ -120,7 +123,7 @@ function validateSliceState(value) {
|
|
|
120
123
|
}
|
|
121
124
|
function validateOperation(value) {
|
|
122
125
|
(0, contract_validation_js_1.assertRecord)(value, 'Fleet operation state');
|
|
123
|
-
(0, contract_validation_js_1.assertExactProperties)(value, ['acceptedAt', 'command', 'id', 'idempotencyKey', 'slices', 'state'], ['completedAt', 'failureClassification'], 'Fleet operation state');
|
|
126
|
+
(0, contract_validation_js_1.assertExactProperties)(value, ['acceptedAt', 'command', 'id', 'idempotencyKey', 'slices', 'state'], ['completedAt', 'failureClassification', 'failureMessage'], 'Fleet operation state');
|
|
124
127
|
if (value.command !== 'up' &&
|
|
125
128
|
value.command !== 'down' &&
|
|
126
129
|
value.command !== 'restart') {
|
|
@@ -157,6 +160,12 @@ function validateOperation(value) {
|
|
|
157
160
|
}
|
|
158
161
|
result.failureClassification = classification;
|
|
159
162
|
}
|
|
163
|
+
if (value.failureMessage !== undefined) {
|
|
164
|
+
if (result.state !== 'failed') {
|
|
165
|
+
return stateError('Fleet operation failureMessage is valid only for a failed operation.');
|
|
166
|
+
}
|
|
167
|
+
result.failureMessage = (0, contract_validation_js_1.requireBoundedString)(value.failureMessage, 'Fleet operation failureMessage', { maximumLength: exports.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH });
|
|
168
|
+
}
|
|
160
169
|
const terminal = result.state === 'succeeded' || result.state === 'failed';
|
|
161
170
|
if (terminal !== (result.completedAt !== undefined)) {
|
|
162
171
|
return stateError('Fleet operation terminal state and completedAt must agree.');
|
|
@@ -42,6 +42,12 @@ export interface FleetSupervisorOptions {
|
|
|
42
42
|
security?: PrivateStateSecurity;
|
|
43
43
|
shutdownGraceMs?: number;
|
|
44
44
|
}
|
|
45
|
+
/** Failed operations beyond this many keep their classification only. */
|
|
46
|
+
export declare const FLEET_RETAINED_FAILURE_MESSAGES = 8;
|
|
47
|
+
/** The persisted cause is one redacted, bounded line. Whitespace collapses
|
|
48
|
+
* before redaction because the terminal-safety strip deletes line breaks and
|
|
49
|
+
* the state validator refuses every control character. */
|
|
50
|
+
export declare function fleetOperationFailureMessage(error: unknown): string | undefined;
|
|
45
51
|
export declare class FleetSupervisor {
|
|
46
52
|
#private;
|
|
47
53
|
constructor(options: FleetSupervisorOptions, effects?: FleetSupervisorEffects);
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.FleetSupervisor = void 0;
|
|
3
|
+
exports.FleetSupervisor = exports.FLEET_RETAINED_FAILURE_MESSAGES = void 0;
|
|
4
|
+
exports.fleetOperationFailureMessage = fleetOperationFailureMessage;
|
|
4
5
|
const node_crypto_1 = require("node:crypto");
|
|
5
6
|
const contracts_js_1 = require("./contracts.js");
|
|
6
7
|
const node_fs_1 = require("node:fs");
|
|
@@ -8,6 +9,7 @@ const control_contract_js_1 = require("./control-contract.js");
|
|
|
8
9
|
const errors_js_1 = require("./errors.js");
|
|
9
10
|
const fleet_ipc_js_1 = require("./fleet-ipc.js");
|
|
10
11
|
const fleet_lock_js_1 = require("./fleet-lock.js");
|
|
12
|
+
const fleet_logs_js_1 = require("./fleet-logs.js");
|
|
11
13
|
const fleet_state_js_1 = require("./fleet-state.js");
|
|
12
14
|
const private_state_js_1 = require("./private-state.js");
|
|
13
15
|
const process_identity_js_1 = require("./process-identity.js");
|
|
@@ -33,6 +35,49 @@ function failureClassification(error) {
|
|
|
33
35
|
? error.classification
|
|
34
36
|
: 'internal-error';
|
|
35
37
|
}
|
|
38
|
+
/** Failed operations beyond this many keep their classification only. */
|
|
39
|
+
exports.FLEET_RETAINED_FAILURE_MESSAGES = 8;
|
|
40
|
+
/** The persisted cause is one redacted, bounded line. Whitespace collapses
|
|
41
|
+
* before redaction because the terminal-safety strip deletes line breaks and
|
|
42
|
+
* the state validator refuses every control character. */
|
|
43
|
+
function fleetOperationFailureMessage(error) {
|
|
44
|
+
const raw = error instanceof Error ? error.message : String(error);
|
|
45
|
+
const line = (0, fleet_logs_js_1.redactLocalLogText)(raw.replace(/\s+/gu, ' '))
|
|
46
|
+
// The persisted line must carry no control character of any kind.
|
|
47
|
+
// eslint-disable-next-line no-control-regex
|
|
48
|
+
.replace(/[\u0000-\u001f\u007f-\u009f\u2028\u2029]+/gu, ' ')
|
|
49
|
+
.replace(/ {2,}/gu, ' ')
|
|
50
|
+
.trim();
|
|
51
|
+
if (line.length === 0)
|
|
52
|
+
return undefined;
|
|
53
|
+
if (line.length <= fleet_state_js_1.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH)
|
|
54
|
+
return line;
|
|
55
|
+
let bounded = '';
|
|
56
|
+
for (const character of line) {
|
|
57
|
+
if (bounded.length + character.length >
|
|
58
|
+
fleet_state_js_1.FLEET_OPERATION_FAILURE_MESSAGE_MAX_LENGTH - 1)
|
|
59
|
+
break;
|
|
60
|
+
bounded += character;
|
|
61
|
+
}
|
|
62
|
+
return `${bounded.trimEnd()}\u2026`;
|
|
63
|
+
}
|
|
64
|
+
function retainRecentFailureMessages(operations) {
|
|
65
|
+
let retained = 0;
|
|
66
|
+
return operations
|
|
67
|
+
.slice()
|
|
68
|
+
.reverse()
|
|
69
|
+
.map((operation) => {
|
|
70
|
+
if (operation.failureMessage === undefined)
|
|
71
|
+
return operation;
|
|
72
|
+
retained += 1;
|
|
73
|
+
if (retained <= exports.FLEET_RETAINED_FAILURE_MESSAGES)
|
|
74
|
+
return operation;
|
|
75
|
+
const classificationOnly = { ...operation };
|
|
76
|
+
delete classificationOnly.failureMessage;
|
|
77
|
+
return classificationOnly;
|
|
78
|
+
})
|
|
79
|
+
.reverse();
|
|
80
|
+
}
|
|
36
81
|
class FleetSupervisor {
|
|
37
82
|
#closed = false;
|
|
38
83
|
#adapters;
|
|
@@ -316,12 +361,15 @@ class FleetSupervisor {
|
|
|
316
361
|
}
|
|
317
362
|
catch (error) {
|
|
318
363
|
this.#shutdownAfterOperations.delete(operation.id);
|
|
364
|
+
const failureMessage = fleetOperationFailureMessage(error);
|
|
319
365
|
this.#state.operations[index] = {
|
|
320
366
|
...this.#state.operations[index],
|
|
321
367
|
completedAt: this.#effects.now().toISOString(),
|
|
322
368
|
failureClassification: failureClassification(error),
|
|
369
|
+
...(failureMessage === undefined ? {} : { failureMessage }),
|
|
323
370
|
state: 'failed',
|
|
324
371
|
};
|
|
372
|
+
this.#state.operations = retainRecentFailureMessages(this.#state.operations);
|
|
325
373
|
this.#persistState();
|
|
326
374
|
}
|
|
327
375
|
}
|
package/src/local-dev/ports.d.ts
CHANGED
|
@@ -6,11 +6,21 @@ export interface TcpPortRange {
|
|
|
6
6
|
first: number;
|
|
7
7
|
last: number;
|
|
8
8
|
}
|
|
9
|
-
/**
|
|
9
|
+
/**
|
|
10
|
+
* Read only. Never change sysctl/netsh or assume a custom range is default.
|
|
11
|
+
*
|
|
12
|
+
* A supported platform is read at most three times, pausing 250 ms x attempt
|
|
13
|
+
* between reads (250 ms, then 500 ms). Each native command read is bounded by a
|
|
14
|
+
* 5 s timeout, so the worst-case failure on Windows or macOS takes about
|
|
15
|
+
* 3 x 5 s + 250 ms + 500 ms = 15.75 s before the
|
|
16
|
+
* `prerequisite-unavailable` error. An unsupported platform fails at once,
|
|
17
|
+
* without a read or a pause, and the error names it as unsupported.
|
|
18
|
+
*/
|
|
10
19
|
export declare function readAutomaticTcpPortRange(effects?: {
|
|
11
20
|
platform?: NodeJS.Platform;
|
|
12
21
|
read?: (path: string) => string;
|
|
13
22
|
execute?: (command: string, args: string[]) => string;
|
|
23
|
+
pause?: (milliseconds: number) => void;
|
|
14
24
|
}): TcpPortRange;
|
|
15
25
|
export declare function preferredFleetPort(key: string): number;
|
|
16
26
|
export declare function canBindLoopbackPort(port: number): Promise<boolean>;
|
package/src/local-dev/ports.js
CHANGED
|
@@ -21,7 +21,33 @@ function validRange(range) {
|
|
|
21
21
|
range.first <= range.last &&
|
|
22
22
|
range.last <= 65_535);
|
|
23
23
|
}
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Bounded attempts for the host range read. A loaded Windows host can fail a
|
|
26
|
+
* single netsh read transiently; three attempts with a short pause absorb that
|
|
27
|
+
* without hiding a persistent failure. Module-private: the specs observe the
|
|
28
|
+
* bound through the injected effects, and the public ./local-dev subpath does
|
|
29
|
+
* not export it.
|
|
30
|
+
*/
|
|
31
|
+
const AUTOMATIC_TCP_PORT_RANGE_READ_ATTEMPTS = 3;
|
|
32
|
+
const AUTOMATIC_TCP_PORT_RANGE_RETRY_PAUSE_MS = 250;
|
|
33
|
+
const SUPPORTED_RANGE_PLATFORMS = [
|
|
34
|
+
'linux',
|
|
35
|
+
'darwin',
|
|
36
|
+
'win32',
|
|
37
|
+
];
|
|
38
|
+
function pauseSynchronously(milliseconds) {
|
|
39
|
+
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, milliseconds);
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Read only. Never change sysctl/netsh or assume a custom range is default.
|
|
43
|
+
*
|
|
44
|
+
* A supported platform is read at most three times, pausing 250 ms x attempt
|
|
45
|
+
* between reads (250 ms, then 500 ms). Each native command read is bounded by a
|
|
46
|
+
* 5 s timeout, so the worst-case failure on Windows or macOS takes about
|
|
47
|
+
* 3 x 5 s + 250 ms + 500 ms = 15.75 s before the
|
|
48
|
+
* `prerequisite-unavailable` error. An unsupported platform fails at once,
|
|
49
|
+
* without a read or a pause, and the error names it as unsupported.
|
|
50
|
+
*/
|
|
25
51
|
function readAutomaticTcpPortRange(effects = {}) {
|
|
26
52
|
const platform = effects.platform ?? process.platform;
|
|
27
53
|
const execute = effects.execute ??
|
|
@@ -32,48 +58,62 @@ function readAutomaticTcpPortRange(effects = {}) {
|
|
|
32
58
|
windowsHide: true,
|
|
33
59
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
34
60
|
}));
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'-n',
|
|
46
|
-
'net.inet.ip.portrange.first',
|
|
47
|
-
'net.inet.ip.portrange.last',
|
|
48
|
-
]);
|
|
49
|
-
if (!/^\s*\d+\s+\d+\s*$/.test(text))
|
|
50
|
-
throw new Error('Invalid range');
|
|
51
|
-
values = text.trim().split(/\s+/).map(Number);
|
|
61
|
+
const read = effects.read ?? ((path) => (0, node_fs_1.readFileSync)(path, 'utf8'));
|
|
62
|
+
const pause = effects.pause ?? pauseSynchronously;
|
|
63
|
+
// An unsupported platform is not transient: fail before any read.
|
|
64
|
+
if (!SUPPORTED_RANGE_PLATFORMS.includes(platform)) {
|
|
65
|
+
throw new errors_js_1.LocalDevContractError('prerequisite-unavailable', `The host platform ${platform} is not supported for the automatic TCP client-port range read; ebk dev up supports Linux, macOS and Windows. No network setting was changed.`);
|
|
66
|
+
}
|
|
67
|
+
const attempts = AUTOMATIC_TCP_PORT_RANGE_READ_ATTEMPTS;
|
|
68
|
+
for (let attempt = 1; attempt <= attempts; attempt += 1) {
|
|
69
|
+
try {
|
|
70
|
+
return readAutomaticTcpPortRangeOnce(platform, read, execute);
|
|
52
71
|
}
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
'ipv4',
|
|
57
|
-
'show',
|
|
58
|
-
'dynamicport',
|
|
59
|
-
'tcp',
|
|
60
|
-
]);
|
|
61
|
-
// Labels are localized; the two numeric colon-delimited values are not.
|
|
62
|
-
values = [...text.matchAll(/:\s*(\d+)\s*$/gm)].map((match) => Number(match[1]));
|
|
63
|
-
if (values.length !== 2 || values[1] < 1)
|
|
64
|
-
throw new Error('Invalid range');
|
|
65
|
-
values[1] = values[0] + values[1] - 1;
|
|
72
|
+
catch {
|
|
73
|
+
if (attempt < attempts)
|
|
74
|
+
pause(AUTOMATIC_TCP_PORT_RANGE_RETRY_PAUSE_MS * attempt);
|
|
66
75
|
}
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
76
|
+
}
|
|
77
|
+
throw new errors_js_1.LocalDevContractError('prerequisite-unavailable', `Cannot read the host automatic TCP client-port range after ${attempts} ${attempts === 1 ? 'attempt' : 'attempts'}. Verify the native network configuration is readable before retrying ebk dev up; no network setting was changed.`);
|
|
78
|
+
}
|
|
79
|
+
function readAutomaticTcpPortRangeOnce(platform, read, execute) {
|
|
80
|
+
let values;
|
|
81
|
+
if (platform === 'linux') {
|
|
82
|
+
const text = read('/proc/sys/net/ipv4/ip_local_port_range');
|
|
83
|
+
if (!/^\s*\d+\s+\d+\s*$/.test(text))
|
|
84
|
+
throw new Error('Invalid range');
|
|
85
|
+
values = text.trim().split(/\s+/).map(Number);
|
|
86
|
+
}
|
|
87
|
+
else if (platform === 'darwin') {
|
|
88
|
+
const text = execute('/usr/sbin/sysctl', [
|
|
89
|
+
'-n',
|
|
90
|
+
'net.inet.ip.portrange.first',
|
|
91
|
+
'net.inet.ip.portrange.last',
|
|
92
|
+
]);
|
|
93
|
+
if (!/^\s*\d+\s+\d+\s*$/.test(text))
|
|
71
94
|
throw new Error('Invalid range');
|
|
72
|
-
|
|
95
|
+
values = text.trim().split(/\s+/).map(Number);
|
|
73
96
|
}
|
|
74
|
-
|
|
75
|
-
|
|
97
|
+
else if (platform === 'win32') {
|
|
98
|
+
const text = execute('netsh.exe', [
|
|
99
|
+
'int',
|
|
100
|
+
'ipv4',
|
|
101
|
+
'show',
|
|
102
|
+
'dynamicport',
|
|
103
|
+
'tcp',
|
|
104
|
+
]);
|
|
105
|
+
// Labels are localized; the two numeric colon-delimited values are not.
|
|
106
|
+
values = [...text.matchAll(/:\s*(\d+)\s*$/gm)].map((match) => Number(match[1]));
|
|
107
|
+
if (values.length !== 2 || values[1] < 1)
|
|
108
|
+
throw new Error('Invalid range');
|
|
109
|
+
values[1] = values[0] + values[1] - 1;
|
|
76
110
|
}
|
|
111
|
+
else
|
|
112
|
+
throw new Error('Unsupported platform');
|
|
113
|
+
const range = { first: values[0], last: values[1] };
|
|
114
|
+
if (!validRange(range))
|
|
115
|
+
throw new Error('Invalid range');
|
|
116
|
+
return range;
|
|
77
117
|
}
|
|
78
118
|
function preferredFleetPort(key) {
|
|
79
119
|
const digest = (0, node_crypto_1.createHash)('sha256').update(key).digest();
|