@devrouter/cli 0.0.62 → 0.0.64
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/devrouter-lifecycle-worker.js +1382 -1257
- package/dist/devrouter.js +360 -97
- package/package.json +1 -1
- package/upgrade-prompts/0.0.63.md +36 -0
- package/upgrade-prompts/0.0.64.md +34 -0
|
@@ -240,7 +240,9 @@ function tryReclaimStaleLock(lockPath, staleLinkPath, livenessCache) {
|
|
|
240
240
|
function acquireFileLock(lockPath, options) {
|
|
241
241
|
const processBirth = processBirthIdentity(process.pid);
|
|
242
242
|
if (!processBirth) {
|
|
243
|
-
throw new Error(
|
|
243
|
+
throw new Error(
|
|
244
|
+
`could not determine process identity for ${options.activity} lock: process inspection may be unavailable or sandbox-denied; run the canonical command in a permitted host context`
|
|
245
|
+
);
|
|
244
246
|
}
|
|
245
247
|
const ownerId = (0, import_node_crypto2.randomUUID)();
|
|
246
248
|
const owner = `${process.pid}:${Buffer.from(processBirth).toString("base64url")}:${ownerId}`;
|
|
@@ -978,6 +980,33 @@ function inspectManagedStopWorkspaceIds(endpoint, composeDirectory) {
|
|
|
978
980
|
assertUniqueContainerIds(ids);
|
|
979
981
|
return ids;
|
|
980
982
|
}
|
|
983
|
+
function hasExactManagedStopMissingObjectError(stderr, id2) {
|
|
984
|
+
if (typeof stderr !== "string") return false;
|
|
985
|
+
const line = stderr.endsWith("\r\n") ? stderr.slice(0, -2) : stderr.endsWith("\n") ? stderr.slice(0, -1) : stderr;
|
|
986
|
+
return line.toLowerCase() === `error: no such object: ${id2}`;
|
|
987
|
+
}
|
|
988
|
+
function assertManagedStopContainersAbsent(endpoint, ids) {
|
|
989
|
+
assertManagedStopEndpoint(endpoint);
|
|
990
|
+
if (!Array.isArray(ids) || ids.length === 0 || ids.length > 256) {
|
|
991
|
+
throw new Error("Managed stop absence proof requires between 1 and 256 container ids.");
|
|
992
|
+
}
|
|
993
|
+
ids.forEach(assertFullContainerId);
|
|
994
|
+
assertUniqueContainerIds(ids);
|
|
995
|
+
const env = { ...process.env };
|
|
996
|
+
delete env.DOCKER_CONTEXT;
|
|
997
|
+
delete env.DOCKER_HOST;
|
|
998
|
+
for (const id2 of ids) {
|
|
999
|
+
const result = (0, import_node_child_process3.spawnSync)("docker", ["--host", endpoint, "inspect", "--format", "{{.Id}}", id2], {
|
|
1000
|
+
env,
|
|
1001
|
+
encoding: "utf-8",
|
|
1002
|
+
timeout: MANAGED_STOP_DOCKER_TIMEOUT_MS,
|
|
1003
|
+
maxBuffer: MANAGED_STOP_DOCKER_MAX_BUFFER
|
|
1004
|
+
});
|
|
1005
|
+
if (result.status !== 1 || result.error || result.signal || result.stdout !== "" && result.stdout !== "\n" && result.stdout !== "\r\n" || !hasExactManagedStopMissingObjectError(result.stderr, id2)) {
|
|
1006
|
+
throw new Error("Managed stop Docker inspection did not prove container absence.");
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
}
|
|
981
1010
|
function stopPinnedManagedContainer(endpoint, containerId) {
|
|
982
1011
|
assertFullContainerId(containerId);
|
|
983
1012
|
runManagedStopDocker(["stop", containerId], endpoint, 3e4);
|
|
@@ -988,11 +1017,13 @@ var import_node_child_process8 = require("child_process");
|
|
|
988
1017
|
|
|
989
1018
|
// src/core/devpod-registry.ts
|
|
990
1019
|
var import_node_child_process4 = require("child_process");
|
|
991
|
-
function listDevpodWorkspacesRaw() {
|
|
1020
|
+
function listDevpodWorkspacesRaw(options = {}) {
|
|
992
1021
|
const result = (0, import_node_child_process4.spawnSync)("devpod", ["list", "--output", "json", "--skip-pro"], {
|
|
993
1022
|
encoding: "utf-8"
|
|
994
1023
|
});
|
|
995
|
-
if (result.
|
|
1024
|
+
if (options.allowMissingExecutable && result.error?.code === "ENOENT")
|
|
1025
|
+
return [];
|
|
1026
|
+
if (result.error || result.status !== 0) {
|
|
996
1027
|
const details = [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
997
1028
|
throw new Error(`devpod list failed: ${details || "devpod is not installed or unavailable"}`);
|
|
998
1029
|
}
|
|
@@ -3240,7 +3271,56 @@ function proveRetainedManagedStop(state) {
|
|
|
3240
3271
|
}
|
|
3241
3272
|
return result;
|
|
3242
3273
|
}
|
|
3274
|
+
function absentRegistrationIdentity(state) {
|
|
3275
|
+
resetWorkspaceRuntimeCaches();
|
|
3276
|
+
if (resolveWorkspaceRuntimeOrDefault(state.repoPath) !== "devsy")
|
|
3277
|
+
throw new Error("Stop provider changed.");
|
|
3278
|
+
if (!state.workspace || !isLinkedWorktree(state.repoPath))
|
|
3279
|
+
throw new Error("Absent stop requires a linked workspace.");
|
|
3280
|
+
const record4 = readWorkspaceOwnership(state.repoPath, state.workspace);
|
|
3281
|
+
if (!record4 || record4.workspace !== state.workspace || record4.devpodId !== state.devpodId || !sameWorkspacePath(record4.worktreePath, state.repoPath) || resolveWorktreeWorkspace(state.repoPath) !== state.workspace || inspectWorkspaceOwnership(record4, listGitWorktrees(state.repoPath), void 0).ownerStatus !== "present")
|
|
3282
|
+
throw new Error("Absent stop workspace ownership changed.");
|
|
3283
|
+
for (const entries of [
|
|
3284
|
+
listDevsyWorkspaces(),
|
|
3285
|
+
listDevpodWorkspacesRaw({ allowMissingExecutable: true })
|
|
3286
|
+
]) {
|
|
3287
|
+
if (entries.some(
|
|
3288
|
+
(entry) => entry.id === state.devpodId || sameWorkspacePath(entry.source.localFolder, state.repoPath)
|
|
3289
|
+
))
|
|
3290
|
+
throw new Error("Absent stop requires both provider registrations to remain absent.");
|
|
3291
|
+
}
|
|
3292
|
+
return { record: record4, gitCommonDir: resolveGitCommonDir(state.repoPath) };
|
|
3293
|
+
}
|
|
3294
|
+
function proveManagedStop(state) {
|
|
3295
|
+
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
3296
|
+
const owner = inspectDevsyWorkspaceOwnership(
|
|
3297
|
+
listDevsyWorkspaces(),
|
|
3298
|
+
state.devpodId,
|
|
3299
|
+
state.repoPath
|
|
3300
|
+
);
|
|
3301
|
+
if (owner.status !== "absent")
|
|
3302
|
+
return { status: "retained", containers: proveRetainedManagedStop(state) };
|
|
3303
|
+
if (baseline.sourceContainer) throw new Error("Absent stop cannot prove a source container.");
|
|
3304
|
+
const before = absentRegistrationIdentity(state);
|
|
3305
|
+
if (!(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
3306
|
+
throw new Error("Retained stop generation changed.");
|
|
3307
|
+
if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId)
|
|
3308
|
+
throw new Error("Stop daemon changed.");
|
|
3309
|
+
assertManagedStopContainersAbsent(
|
|
3310
|
+
baseline.endpoint,
|
|
3311
|
+
baseline.containers.map((container) => container.id)
|
|
3312
|
+
);
|
|
3313
|
+
const uidBytes = Buffer.byteLength(baseline.uid);
|
|
3314
|
+
const runnerId = uidBytes === 16 || uidBytes === 40 ? baseline.uid : baseline.providerId;
|
|
3315
|
+
if (inspectManagedStopContainers(baseline.project, baseline.endpoint).length !== 0 || inspectManagedStopWorkspaceIds(baseline.endpoint, baseline.composeDirectory).length !== 0 || inspectProviderRunnerContainers(baseline.endpoint, runnerId).length !== 0)
|
|
3316
|
+
throw new Error("Absent stop observed a remaining workspace population.");
|
|
3317
|
+
if (inspectManagedStopDaemon(baseline.endpoint) !== baseline.daemonId || !(0, import_node_util2.isDeepStrictEqual)(absentRegistrationIdentity(state), before) || !(0, import_node_util2.isDeepStrictEqual)(readManagedRuntimeState(state.repoPath, state.workspace), state))
|
|
3318
|
+
throw new Error("Absent stop authority changed during inspection.");
|
|
3319
|
+
return { status: "proven-absent", containers: [] };
|
|
3320
|
+
}
|
|
3243
3321
|
function stopFromManagedBaseline(state) {
|
|
3322
|
+
const proof = proveManagedStop(state);
|
|
3323
|
+
if (proof.status === "proven-absent") return proof.status;
|
|
3244
3324
|
const baseline = validateManagedStopBaseline(state.stopBaseline, state);
|
|
3245
3325
|
const stopped = /* @__PURE__ */ new Set();
|
|
3246
3326
|
for (const initial of proveRetainedManagedStop(state))
|
|
@@ -3263,6 +3343,12 @@ function stopFromManagedBaseline(state) {
|
|
|
3263
3343
|
}
|
|
3264
3344
|
if (inspect().some((container) => container.state.Running))
|
|
3265
3345
|
throw new Error("Retained workloads remain running.");
|
|
3346
|
+
return "retained";
|
|
3347
|
+
}
|
|
3348
|
+
function managedStopRouteReferences(state) {
|
|
3349
|
+
return state.desired.apps.flatMap(
|
|
3350
|
+
(name) => ["http", "tcp"].map((protocol) => ({ repoPath: state.repoPath, name, protocol }))
|
|
3351
|
+
);
|
|
3266
3352
|
}
|
|
3267
3353
|
|
|
3268
3354
|
// src/core/repo-config.ts
|
|
@@ -4621,7 +4707,7 @@ function loadRepoConfig(repoPath, read = (file) => import_node_fs14.default.read
|
|
|
4621
4707
|
const config = parseConfig(parsed ?? {}, configPath);
|
|
4622
4708
|
const requiredVersion = config.devrouter?.version;
|
|
4623
4709
|
if (requiredVersion && !hasWarnedVersionMismatch) {
|
|
4624
|
-
const cliVersion = true ? "0.0.
|
|
4710
|
+
const cliVersion = true ? "0.0.64" : "0.0.0-dev";
|
|
4625
4711
|
if (cliVersion !== "0.0.0-dev" && compareSemver(requiredVersion, cliVersion) > 0) {
|
|
4626
4712
|
hasWarnedVersionMismatch = true;
|
|
4627
4713
|
process.stderr.write(
|
|
@@ -5157,8 +5243,7 @@ function stopRetainedManagedDevsyWorkspace(options) {
|
|
|
5157
5243
|
throw new Error("Managed stop requires the exact retained workspace identity.");
|
|
5158
5244
|
}
|
|
5159
5245
|
if (state.stopBaseline) {
|
|
5160
|
-
stopFromManagedBaseline(state);
|
|
5161
|
-
return true;
|
|
5246
|
+
return stopFromManagedBaseline(state) === "proven-absent" ? "proven-absent" : true;
|
|
5162
5247
|
}
|
|
5163
5248
|
const workspaceEnv = workspace ? { token: workspace, gitCommonDir: resolveGitCommonDir(repoPath) } : void 0;
|
|
5164
5249
|
function registration2() {
|
|
@@ -5386,12 +5471,14 @@ function inspectExactOwnership(devsyId, worktreePath) {
|
|
|
5386
5471
|
}
|
|
5387
5472
|
function mutateOwnedDevsyWorkspace(action, devsyId, worktreePath) {
|
|
5388
5473
|
return withMutationLock(`Devsy ${action}`, worktreePath, () => {
|
|
5389
|
-
if (action === "stop"
|
|
5390
|
-
|
|
5391
|
-
|
|
5392
|
-
|
|
5393
|
-
|
|
5394
|
-
|
|
5474
|
+
if (action === "stop") {
|
|
5475
|
+
const stopped = stopRetainedManagedDevsyWorkspace({
|
|
5476
|
+
repoPath: worktreePath,
|
|
5477
|
+
devsyId,
|
|
5478
|
+
stopProvider: () => runDevsyAction("stop", devsyId)
|
|
5479
|
+
});
|
|
5480
|
+
if (stopped) return { status: stopped === "proven-absent" ? "proven-absent" : "changed" };
|
|
5481
|
+
}
|
|
5395
5482
|
const before = inspectExactOwnership(devsyId, worktreePath);
|
|
5396
5483
|
if (before.status === "absent") return { status: "absent" };
|
|
5397
5484
|
runDevsyAction(action, devsyId);
|
|
@@ -6206,7 +6293,7 @@ function applyReliabilityEvent(state, event, nowMs) {
|
|
|
6206
6293
|
case "not-started":
|
|
6207
6294
|
if (next.executionPolicy !== "manual" || next.operation?.kind !== "exec")
|
|
6208
6295
|
return unchanged(next, "blocked");
|
|
6209
|
-
if (next.operation.id !== event.operationId || next.operation.status
|
|
6296
|
+
if (next.operation.id !== event.operationId || !["RUNNING", "DISPATCH_RECORDED"].includes(next.operation.status))
|
|
6210
6297
|
return unchanged(next, "stale");
|
|
6211
6298
|
next.operation = { ...next.operation, status: "NOT_LAUNCHED", exitCode: null };
|
|
6212
6299
|
return transition(next, "accepted");
|
|
@@ -6427,1361 +6514,1370 @@ function workerGroupAbsent(pid) {
|
|
|
6427
6514
|
}
|
|
6428
6515
|
}
|
|
6429
6516
|
|
|
6430
|
-
// src/core/
|
|
6431
|
-
var import_node_child_process21 = require("child_process");
|
|
6432
|
-
var import_node_fs23 = __toESM(require("fs"));
|
|
6433
|
-
var import_node_path27 = __toESM(require("path"));
|
|
6434
|
-
|
|
6435
|
-
// src/core/devpod-mutation.ts
|
|
6517
|
+
// src/core/traefik-route-health.ts
|
|
6436
6518
|
var import_node_child_process16 = require("child_process");
|
|
6437
6519
|
var import_node_fs19 = __toESM(require("fs"));
|
|
6438
6520
|
var import_node_path24 = __toESM(require("path"));
|
|
6439
|
-
var
|
|
6440
|
-
var
|
|
6441
|
-
var
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
6448
|
-
activity,
|
|
6449
|
-
target: `'${target}'`,
|
|
6450
|
-
waitMs: DEVPOD_MUTATION_WAIT_MS,
|
|
6451
|
-
fair: true,
|
|
6452
|
-
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
6453
|
-
},
|
|
6454
|
-
operation
|
|
6455
|
-
);
|
|
6456
|
-
}
|
|
6457
|
-
function commandFailure2(result) {
|
|
6458
|
-
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
6521
|
+
var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
6522
|
+
var ROUTER_RELOAD_LOCK_FILE = import_node_path24.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
6523
|
+
var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
6524
|
+
var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
6525
|
+
var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
6526
|
+
var DEFAULT_POLL_INTERVAL_MS = 250;
|
|
6527
|
+
var ROUTER_API_PAGE_SIZE = 1e3;
|
|
6528
|
+
function sleep(ms) {
|
|
6529
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
6459
6530
|
}
|
|
6460
|
-
function
|
|
6461
|
-
const
|
|
6462
|
-
|
|
6463
|
-
|
|
6531
|
+
function inspectRouterApi(protocol) {
|
|
6532
|
+
const result = (0, import_node_child_process16.spawnSync)(
|
|
6533
|
+
"curl",
|
|
6534
|
+
[
|
|
6535
|
+
"--silent",
|
|
6536
|
+
"--show-error",
|
|
6537
|
+
"--fail",
|
|
6538
|
+
"--max-time",
|
|
6539
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
6540
|
+
`${ROUTER_API_BASE}/${protocol}/routers?per_page=${ROUTER_API_PAGE_SIZE}`
|
|
6541
|
+
],
|
|
6542
|
+
{ encoding: "utf-8" }
|
|
6543
|
+
);
|
|
6464
6544
|
if (result.status !== 0) {
|
|
6465
|
-
|
|
6466
|
-
|
|
6467
|
-
|
|
6545
|
+
return {
|
|
6546
|
+
ok: false,
|
|
6547
|
+
details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
|
|
6548
|
+
};
|
|
6468
6549
|
}
|
|
6550
|
+
let value;
|
|
6551
|
+
try {
|
|
6552
|
+
value = JSON.parse(result.stdout);
|
|
6553
|
+
} catch {
|
|
6554
|
+
return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
|
|
6555
|
+
}
|
|
6556
|
+
if (!Array.isArray(value)) {
|
|
6557
|
+
return { ok: false, details: `${protocol.toUpperCase()} router API returned a non-array` };
|
|
6558
|
+
}
|
|
6559
|
+
if (value.some((item) => !isRecord3(item) || typeof item.name !== "string" || !item.name))
|
|
6560
|
+
return {
|
|
6561
|
+
ok: false,
|
|
6562
|
+
details: `${protocol.toUpperCase()} router API returned malformed entries`
|
|
6563
|
+
};
|
|
6564
|
+
return {
|
|
6565
|
+
ok: true,
|
|
6566
|
+
reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
|
|
6567
|
+
names: new Set(
|
|
6568
|
+
value.flatMap((item) => {
|
|
6569
|
+
if (!item || typeof item !== "object") return [];
|
|
6570
|
+
const name = item.name;
|
|
6571
|
+
return typeof name === "string" ? [name] : [];
|
|
6572
|
+
})
|
|
6573
|
+
)
|
|
6574
|
+
};
|
|
6469
6575
|
}
|
|
6470
|
-
function
|
|
6471
|
-
|
|
6472
|
-
listDevpodWorkspaces(worktreePath),
|
|
6473
|
-
devpodId,
|
|
6474
|
-
worktreePath
|
|
6475
|
-
);
|
|
6476
|
-
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
6477
|
-
return ownership;
|
|
6478
|
-
}
|
|
6479
|
-
function mutateOwnedDevpodWorkspace(action, devpodId, worktreePath) {
|
|
6480
|
-
return withMutationLock2(`DevPod ${action}`, worktreePath, () => {
|
|
6481
|
-
const before = inspectExactOwnership2(devpodId, worktreePath);
|
|
6482
|
-
if (before.status === "absent") return { status: "absent" };
|
|
6483
|
-
runDevpodAction(action, devpodId);
|
|
6484
|
-
let after = inspectExactOwnership2(devpodId, worktreePath);
|
|
6485
|
-
if (action === "stop" && after.status !== "owned") {
|
|
6486
|
-
throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
|
|
6487
|
-
}
|
|
6488
|
-
if (action === "delete" && after.status === "owned") {
|
|
6489
|
-
const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
|
|
6490
|
-
if (runtime !== "not-found") {
|
|
6491
|
-
throw new Error(
|
|
6492
|
-
`DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
6493
|
-
);
|
|
6494
|
-
}
|
|
6495
|
-
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
6496
|
-
if (after.status === "owned") {
|
|
6497
|
-
runDevpodAction("delete", devpodId, true);
|
|
6498
|
-
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
6499
|
-
}
|
|
6500
|
-
if (after.status !== "absent") {
|
|
6501
|
-
throw new Error(
|
|
6502
|
-
`DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
|
|
6503
|
-
);
|
|
6504
|
-
}
|
|
6505
|
-
}
|
|
6506
|
-
return { status: "changed" };
|
|
6507
|
-
});
|
|
6576
|
+
function isRecord3(value) {
|
|
6577
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
6508
6578
|
}
|
|
6509
|
-
function
|
|
6579
|
+
function inspectNamedRouterApi(protocol, resource, name) {
|
|
6580
|
+
const result = (0, import_node_child_process16.spawnSync)(
|
|
6581
|
+
"curl",
|
|
6582
|
+
[
|
|
6583
|
+
"--silent",
|
|
6584
|
+
"--show-error",
|
|
6585
|
+
"--fail",
|
|
6586
|
+
"--max-time",
|
|
6587
|
+
ROUTER_API_REQUEST_TIMEOUT_SECONDS,
|
|
6588
|
+
`${ROUTER_API_BASE}/${protocol}/${resource}/${encodeURIComponent(name)}`
|
|
6589
|
+
],
|
|
6590
|
+
{ encoding: "utf-8" }
|
|
6591
|
+
);
|
|
6592
|
+
if (result.status !== 0) {
|
|
6593
|
+
return {
|
|
6594
|
+
ok: false,
|
|
6595
|
+
details: result.stderr?.trim() || `curl exited with status ${result.status ?? "unknown"}`
|
|
6596
|
+
};
|
|
6597
|
+
}
|
|
6598
|
+
let value;
|
|
6510
6599
|
try {
|
|
6511
|
-
|
|
6512
|
-
|
|
6513
|
-
|
|
6514
|
-
|
|
6515
|
-
|
|
6516
|
-
|
|
6517
|
-
resetWorkspaceRuntimeCaches();
|
|
6600
|
+
value = JSON.parse(result.stdout);
|
|
6601
|
+
} catch {
|
|
6602
|
+
return {
|
|
6603
|
+
ok: false,
|
|
6604
|
+
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
|
|
6605
|
+
};
|
|
6518
6606
|
}
|
|
6519
|
-
|
|
6520
|
-
|
|
6521
|
-
|
|
6522
|
-
|
|
6523
|
-
|
|
6524
|
-
return result2;
|
|
6607
|
+
if (!isRecord3(value)) {
|
|
6608
|
+
return {
|
|
6609
|
+
ok: false,
|
|
6610
|
+
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned a non-object`
|
|
6611
|
+
};
|
|
6525
6612
|
}
|
|
6526
|
-
|
|
6527
|
-
resetWorkspaceRuntimeCaches();
|
|
6528
|
-
return result;
|
|
6613
|
+
return { ok: true, value };
|
|
6529
6614
|
}
|
|
6530
|
-
|
|
6531
|
-
|
|
6532
|
-
|
|
6533
|
-
|
|
6534
|
-
|
|
6535
|
-
|
|
6536
|
-
|
|
6537
|
-
|
|
6538
|
-
|
|
6539
|
-
|
|
6540
|
-
|
|
6541
|
-
|
|
6542
|
-
|
|
6543
|
-
|
|
6544
|
-
|
|
6545
|
-
|
|
6546
|
-
|
|
6547
|
-
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
|
|
6551
|
-
|
|
6552
|
-
const
|
|
6553
|
-
|
|
6554
|
-
|
|
6555
|
-
|
|
6556
|
-
|
|
6557
|
-
|
|
6558
|
-
|
|
6559
|
-
|
|
6560
|
-
|
|
6561
|
-
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
const args = ["up", options.repoPath];
|
|
6566
|
-
if (devpodId) args.push("--id", devpodId);
|
|
6567
|
-
if (options.devcontainerPath) {
|
|
6568
|
-
args.push("--devcontainer-path", options.devcontainerPath);
|
|
6569
|
-
}
|
|
6570
|
-
args.push("--open-ide=false");
|
|
6571
|
-
if (options.workspace) {
|
|
6572
|
-
args.push(
|
|
6573
|
-
"--workspace-env",
|
|
6574
|
-
`WORKSPACE=${options.workspace.token}`,
|
|
6575
|
-
"--workspace-env",
|
|
6576
|
-
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
6577
|
-
);
|
|
6578
|
-
}
|
|
6579
|
-
if (options.recreate) args.push("--recreate");
|
|
6580
|
-
const env = { ...process.env };
|
|
6581
|
-
if (options.workspace) {
|
|
6582
|
-
env.WORKSPACE = options.workspace.token;
|
|
6583
|
-
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
6584
|
-
env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
|
|
6585
|
-
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
6586
|
-
} else {
|
|
6587
|
-
delete env.WORKSPACE;
|
|
6588
|
-
delete env.DEVROUTER_WORKSPACE;
|
|
6589
|
-
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
6590
|
-
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
6591
|
-
}
|
|
6592
|
-
claimLifecycleEffect();
|
|
6593
|
-
const result = (0, import_node_child_process16.spawnSync)("devpod", args, {
|
|
6594
|
-
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
6595
|
-
env
|
|
6596
|
-
});
|
|
6597
|
-
if (result.status !== 0) {
|
|
6598
|
-
resetWorkspaceRuntimeCaches();
|
|
6599
|
-
const message = `devpod up failed for '${devpodId ?? options.repoPath}'.`;
|
|
6600
|
-
if (options.devcontainerPath) throw new DevpodStartPostconditionError(message);
|
|
6601
|
-
throw new Error(message);
|
|
6602
|
-
}
|
|
6603
|
-
try {
|
|
6604
|
-
const attached = listDevpodWorkspaces(options.repoPath);
|
|
6605
|
-
devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
|
|
6606
|
-
if (!devpodId) {
|
|
6607
|
-
throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
|
|
6608
|
-
}
|
|
6609
|
-
const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
|
|
6610
|
-
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
6611
|
-
if (ownership.status !== "owned") {
|
|
6612
|
-
throw new Error(
|
|
6613
|
-
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
6614
|
-
);
|
|
6615
|
-
}
|
|
6616
|
-
resetWorkspaceRuntimeCaches();
|
|
6617
|
-
return devpodId;
|
|
6618
|
-
} catch (error) {
|
|
6619
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
6620
|
-
throw new DevpodStartPostconditionError(message);
|
|
6621
|
-
}
|
|
6622
|
-
});
|
|
6623
|
-
}
|
|
6624
|
-
|
|
6625
|
-
// src/core/http-route-probe.ts
|
|
6626
|
-
var import_node_child_process17 = require("child_process");
|
|
6627
|
-
var DEFAULT_MAX_TIME_SECONDS = 5;
|
|
6628
|
-
var PARENT_TIMEOUT_PADDING_MS = 250;
|
|
6629
|
-
var CONTRACT_MAX_BUFFER = 1024;
|
|
6630
|
-
var MIME_TOKEN_RE2 = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
6631
|
-
function httpRouteUrl(host) {
|
|
6632
|
-
return `${isTLSEnabled() ? "https" : "http"}://${host}`;
|
|
6633
|
-
}
|
|
6634
|
-
function probeHttpRoute(host, options = {}) {
|
|
6635
|
-
const maxTimeSeconds = options.maxTimeSeconds ?? DEFAULT_MAX_TIME_SECONDS;
|
|
6636
|
-
if (!Number.isFinite(maxTimeSeconds) || maxTimeSeconds <= 0) {
|
|
6637
|
-
throw new Error("maxTimeSeconds must be a positive finite number.");
|
|
6638
|
-
}
|
|
6639
|
-
if (options.readiness !== void 0) {
|
|
6640
|
-
return probeHttpReadinessSync(host, options.readiness, maxTimeSeconds, options.repoPath);
|
|
6641
|
-
}
|
|
6642
|
-
const tlsEnabled = isTLSEnabled();
|
|
6643
|
-
const url = `${tlsEnabled ? "https" : "http"}://${host}`;
|
|
6644
|
-
const args = [
|
|
6645
|
-
"--silent",
|
|
6646
|
-
"--show-error",
|
|
6647
|
-
"--output",
|
|
6648
|
-
"/dev/null",
|
|
6649
|
-
"--write-out",
|
|
6650
|
-
"%{http_code}",
|
|
6651
|
-
"--max-time",
|
|
6652
|
-
String(maxTimeSeconds)
|
|
6653
|
-
];
|
|
6654
|
-
if (tlsEnabled) {
|
|
6655
|
-
getMkcertRootCAPath({ repoPath: options.repoPath });
|
|
6656
|
-
args.push("--cacert", CERT_FILE);
|
|
6615
|
+
function renderExpectedRoute(route) {
|
|
6616
|
+
const protocol = route.protocol ?? "http";
|
|
6617
|
+
const routerName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
6618
|
+
const routeState = {
|
|
6619
|
+
id: buildHostRouteId(route.repoPath, route.name),
|
|
6620
|
+
name: route.name,
|
|
6621
|
+
host: route.host,
|
|
6622
|
+
protocol,
|
|
6623
|
+
tcpProtocol: route.tcpProtocol,
|
|
6624
|
+
repoPath: route.repoPath,
|
|
6625
|
+
port: route.port,
|
|
6626
|
+
mode: route.mode,
|
|
6627
|
+
upstreamHost: route.upstreamHost,
|
|
6628
|
+
pid: route.pid,
|
|
6629
|
+
command: route.command,
|
|
6630
|
+
workspace: route.workspace,
|
|
6631
|
+
createdAt: "",
|
|
6632
|
+
updatedAt: ""
|
|
6633
|
+
};
|
|
6634
|
+
const document = buildHostRoutesDocument([routeState], false);
|
|
6635
|
+
const protocolDocument = document[protocol];
|
|
6636
|
+
if (!isRecord3(protocolDocument)) return void 0;
|
|
6637
|
+
const routers = protocolDocument.routers;
|
|
6638
|
+
const services = protocolDocument.services;
|
|
6639
|
+
if (!isRecord3(routers) || !isRecord3(services)) return void 0;
|
|
6640
|
+
const routerKey = routerName.slice(0, routerName.lastIndexOf("@"));
|
|
6641
|
+
const router = routers[routerKey];
|
|
6642
|
+
const service = services[routerKey];
|
|
6643
|
+
if (!isRecord3(router) || !isRecord3(service)) return void 0;
|
|
6644
|
+
const rule = router.rule;
|
|
6645
|
+
const serviceReference = router.service;
|
|
6646
|
+
const loadBalancer = service.loadBalancer;
|
|
6647
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
6648
|
+
if (typeof rule !== "string" || serviceReference !== routerKey || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(servers[0])) {
|
|
6649
|
+
return void 0;
|
|
6657
6650
|
}
|
|
6658
|
-
|
|
6659
|
-
const
|
|
6660
|
-
|
|
6661
|
-
const
|
|
6662
|
-
|
|
6663
|
-
|
|
6664
|
-
|
|
6651
|
+
const serverField = protocol === "http" ? "url" : "address";
|
|
6652
|
+
const server = servers[0][serverField];
|
|
6653
|
+
if (typeof server !== "string") return void 0;
|
|
6654
|
+
const provider = routerName.slice(routerName.lastIndexOf("@")).trim();
|
|
6655
|
+
if (!provider.startsWith("@")) return void 0;
|
|
6656
|
+
return {
|
|
6657
|
+
protocol,
|
|
6658
|
+
routerName,
|
|
6659
|
+
serviceKey: routerKey,
|
|
6660
|
+
serviceName: `${routerKey}${provider}`,
|
|
6661
|
+
rule,
|
|
6662
|
+
serverField,
|
|
6663
|
+
server
|
|
6664
|
+
};
|
|
6665
6665
|
}
|
|
6666
|
-
function
|
|
6667
|
-
const
|
|
6668
|
-
|
|
6669
|
-
|
|
6670
|
-
|
|
6671
|
-
|
|
6672
|
-
|
|
6673
|
-
|
|
6674
|
-
|
|
6675
|
-
}
|
|
6676
|
-
let result;
|
|
6677
|
-
try {
|
|
6678
|
-
result = (0, import_node_child_process17.spawnSync)("curl", args, {
|
|
6679
|
-
encoding: "utf-8",
|
|
6680
|
-
maxBuffer: CONTRACT_MAX_BUFFER,
|
|
6681
|
-
timeout: Math.ceil(parentTimeoutMs)
|
|
6682
|
-
});
|
|
6683
|
-
} catch {
|
|
6684
|
-
return transportFailure();
|
|
6685
|
-
}
|
|
6686
|
-
if (result.status !== 0 || result.error) {
|
|
6687
|
-
return transportFailure();
|
|
6666
|
+
function inspectExpectedRouteMatch(route) {
|
|
6667
|
+
const expected = renderExpectedRoute(route);
|
|
6668
|
+
const routeName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
6669
|
+
if (!expected) {
|
|
6670
|
+
return {
|
|
6671
|
+
ok: false,
|
|
6672
|
+
routes: [routeName],
|
|
6673
|
+
details: "host-route renderer returned an incomplete route configuration"
|
|
6674
|
+
};
|
|
6688
6675
|
}
|
|
6689
|
-
const
|
|
6690
|
-
|
|
6691
|
-
}
|
|
6692
|
-
function buildHttpReadinessCurlArgs(host, readiness, maxTimeSeconds, tlsEnabled) {
|
|
6693
|
-
const url = `${tlsEnabled ? "https" : "http"}://${host}${readiness.path}`;
|
|
6694
|
-
const args = [
|
|
6695
|
-
"--disable",
|
|
6696
|
-
"--globoff",
|
|
6697
|
-
"--silent",
|
|
6698
|
-
"--show-error",
|
|
6699
|
-
"--no-location",
|
|
6700
|
-
"--noproxy",
|
|
6701
|
-
"*",
|
|
6702
|
-
"--output",
|
|
6703
|
-
"/dev/null",
|
|
6704
|
-
"--write-out",
|
|
6705
|
-
"%{http_code} %{content_type}",
|
|
6706
|
-
"--max-time",
|
|
6707
|
-
String(maxTimeSeconds)
|
|
6708
|
-
];
|
|
6709
|
-
if (tlsEnabled) args.push("--cacert", CERT_FILE);
|
|
6710
|
-
args.push(url);
|
|
6711
|
-
return args;
|
|
6712
|
-
}
|
|
6713
|
-
function parseHttpReadinessMetadata(metadata, readiness) {
|
|
6714
|
-
if (metadata.length > CONTRACT_MAX_BUFFER) {
|
|
6715
|
-
return transportFailure();
|
|
6676
|
+
const routerResult = inspectNamedRouterApi(expected.protocol, "routers", expected.routerName);
|
|
6677
|
+
if (!routerResult.ok) {
|
|
6678
|
+
return { ok: false, routes: [routeName], details: routerResult.details };
|
|
6716
6679
|
}
|
|
6717
|
-
|
|
6718
|
-
|
|
6719
|
-
|
|
6680
|
+
if (routerResult.value.status !== "enabled" || routerResult.value.name !== expected.routerName || routerResult.value.rule !== expected.rule || routerResult.value.service !== expected.serviceKey && routerResult.value.service !== expected.serviceName) {
|
|
6681
|
+
return {
|
|
6682
|
+
ok: false,
|
|
6683
|
+
routes: [routeName],
|
|
6684
|
+
details: "active Traefik router does not match the rendered route"
|
|
6685
|
+
};
|
|
6720
6686
|
}
|
|
6721
|
-
const
|
|
6722
|
-
if (
|
|
6723
|
-
return
|
|
6687
|
+
const serviceResult = inspectNamedRouterApi(expected.protocol, "services", expected.serviceName);
|
|
6688
|
+
if (!serviceResult.ok) {
|
|
6689
|
+
return { ok: false, routes: [routeName], details: serviceResult.details };
|
|
6724
6690
|
}
|
|
6725
|
-
const
|
|
6726
|
-
const
|
|
6727
|
-
const
|
|
6728
|
-
if (
|
|
6729
|
-
return {
|
|
6691
|
+
const loadBalancer = serviceResult.value.loadBalancer;
|
|
6692
|
+
const servers = isRecord3(loadBalancer) ? loadBalancer.servers : void 0;
|
|
6693
|
+
const server = Array.isArray(servers) && servers.length === 1 ? servers[0] : void 0;
|
|
6694
|
+
if (serviceResult.value.status !== "enabled" || serviceResult.value.name !== expected.serviceName || !isRecord3(loadBalancer) || !Array.isArray(servers) || servers.length !== 1 || !isRecord3(server) || server[expected.serverField] !== expected.server) {
|
|
6695
|
+
return {
|
|
6696
|
+
ok: false,
|
|
6697
|
+
routes: [routeName],
|
|
6698
|
+
details: "active Traefik service does not match the rendered route"
|
|
6699
|
+
};
|
|
6730
6700
|
}
|
|
6731
|
-
|
|
6732
|
-
...statusAllowed ? [] : ["status not allowed"],
|
|
6733
|
-
...mediaFailure ? [mediaFailure] : []
|
|
6734
|
-
];
|
|
6735
|
-
return {
|
|
6736
|
-
ok: false,
|
|
6737
|
-
status,
|
|
6738
|
-
details: `HTTP ${status}; ${reasons.join("; ")}`,
|
|
6739
|
-
classification: "application-contract"
|
|
6740
|
-
};
|
|
6701
|
+
return { ok: true };
|
|
6741
6702
|
}
|
|
6742
|
-
function
|
|
6743
|
-
const
|
|
6744
|
-
|
|
6745
|
-
|
|
6746
|
-
|
|
6747
|
-
|
|
6748
|
-
|
|
6749
|
-
|
|
6703
|
+
function inspectExpectedRouteMatches(routes) {
|
|
6704
|
+
const mismatched = [];
|
|
6705
|
+
const failures = [];
|
|
6706
|
+
for (const route of routes) {
|
|
6707
|
+
const result = inspectExpectedRouteMatch(route);
|
|
6708
|
+
if (!result.ok) {
|
|
6709
|
+
mismatched.push(...result.routes);
|
|
6710
|
+
failures.push(result.details);
|
|
6711
|
+
}
|
|
6750
6712
|
}
|
|
6751
|
-
return
|
|
6752
|
-
}
|
|
6753
|
-
function transportFailure() {
|
|
6754
|
-
return {
|
|
6713
|
+
return mismatched.length === 0 ? { ok: true } : {
|
|
6755
6714
|
ok: false,
|
|
6756
|
-
|
|
6757
|
-
|
|
6715
|
+
routes: mismatched,
|
|
6716
|
+
details: failures.join("; ")
|
|
6758
6717
|
};
|
|
6759
6718
|
}
|
|
6760
|
-
|
|
6761
|
-
|
|
6762
|
-
|
|
6763
|
-
|
|
6764
|
-
|
|
6765
|
-
|
|
6766
|
-
|
|
6767
|
-
|
|
6768
|
-
var FAILURE_ERROR = "Managed host preparation failed.";
|
|
6769
|
-
var TIMEOUT_ERROR = "Managed host preparation timed out after 60 seconds.";
|
|
6770
|
-
function runPreparationCommand(argv, cwd) {
|
|
6771
|
-
return new Promise((resolve) => {
|
|
6772
|
-
let child;
|
|
6773
|
-
try {
|
|
6774
|
-
child = (0, import_node_child_process18.spawn)(argv[0], argv.slice(1), {
|
|
6775
|
-
cwd,
|
|
6776
|
-
detached: true,
|
|
6777
|
-
shell: false,
|
|
6778
|
-
stdio: "ignore"
|
|
6779
|
-
});
|
|
6780
|
-
} catch {
|
|
6781
|
-
resolve("start-error");
|
|
6782
|
-
return;
|
|
6719
|
+
async function waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs) {
|
|
6720
|
+
const deadline = Date.now() + timeoutMs;
|
|
6721
|
+
let result;
|
|
6722
|
+
do {
|
|
6723
|
+
result = inspectExpectedRouteMatches(routes);
|
|
6724
|
+
if (result.ok) return result;
|
|
6725
|
+
if (Date.now() < deadline) {
|
|
6726
|
+
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
6783
6727
|
}
|
|
6784
|
-
|
|
6785
|
-
|
|
6786
|
-
timedOut = true;
|
|
6787
|
-
if (child.pid !== void 0 && child.exitCode === null && child.signalCode === null) {
|
|
6788
|
-
try {
|
|
6789
|
-
process.kill(-child.pid, "SIGKILL");
|
|
6790
|
-
} catch {
|
|
6791
|
-
}
|
|
6792
|
-
}
|
|
6793
|
-
}, PREPARATION_TIMEOUT_MS);
|
|
6794
|
-
child.once("error", () => {
|
|
6795
|
-
clearTimeout(deadline);
|
|
6796
|
-
resolve("start-error");
|
|
6797
|
-
});
|
|
6798
|
-
child.once("close", (code) => {
|
|
6799
|
-
clearTimeout(deadline);
|
|
6800
|
-
resolve(timedOut ? "timeout" : code === 0 ? "success" : "failure");
|
|
6801
|
-
});
|
|
6802
|
-
});
|
|
6728
|
+
} while (Date.now() < deadline);
|
|
6729
|
+
return result;
|
|
6803
6730
|
}
|
|
6804
|
-
function
|
|
6805
|
-
|
|
6806
|
-
|
|
6807
|
-
|
|
6808
|
-
|
|
6731
|
+
function inspectExpectedRoutes(routes, expectation) {
|
|
6732
|
+
const mismatched = [];
|
|
6733
|
+
const failures = [];
|
|
6734
|
+
for (const protocol of ["http", "tcp"]) {
|
|
6735
|
+
const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
|
|
6736
|
+
if (expected.length === 0) continue;
|
|
6737
|
+
const result = inspectRouterApi(protocol);
|
|
6738
|
+
if (!result.ok) {
|
|
6739
|
+
failures.push(result.details);
|
|
6740
|
+
mismatched.push(...expected);
|
|
6741
|
+
continue;
|
|
6742
|
+
}
|
|
6743
|
+
const unexpected = expected.filter(
|
|
6744
|
+
(name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
|
|
6745
|
+
);
|
|
6746
|
+
const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
|
|
6747
|
+
if (boundedAbsenceIsUncertain) {
|
|
6748
|
+
failures.push(
|
|
6749
|
+
`${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
|
|
6750
|
+
);
|
|
6751
|
+
}
|
|
6752
|
+
mismatched.push(...unexpected);
|
|
6753
|
+
if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
|
|
6754
|
+
mismatched.push(...expected);
|
|
6755
|
+
}
|
|
6809
6756
|
}
|
|
6757
|
+
return mismatched.length === 0 ? { ok: true } : {
|
|
6758
|
+
ok: false,
|
|
6759
|
+
routes: mismatched,
|
|
6760
|
+
details: failures.length > 0 ? failures.join("; ") : expectation === "loaded" ? "router names are absent" : "router names are still present"
|
|
6761
|
+
};
|
|
6810
6762
|
}
|
|
6811
|
-
async function
|
|
6812
|
-
const
|
|
6813
|
-
|
|
6814
|
-
|
|
6815
|
-
|
|
6816
|
-
|
|
6817
|
-
|
|
6818
|
-
|
|
6819
|
-
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
}
|
|
6823
|
-
const result = await runPreparationCommand(prepareCommand, resolvedRepoPath);
|
|
6824
|
-
const after = readConfigBytes(configPath);
|
|
6825
|
-
if (!after?.equals(before)) {
|
|
6826
|
-
throw new Error(CONFIG_CHANGED_ERROR);
|
|
6827
|
-
}
|
|
6828
|
-
if (result === "start-error") {
|
|
6829
|
-
throw new Error(START_ERROR);
|
|
6830
|
-
}
|
|
6831
|
-
if (result === "timeout") {
|
|
6832
|
-
throw new Error(TIMEOUT_ERROR);
|
|
6833
|
-
}
|
|
6834
|
-
if (result === "failure") {
|
|
6835
|
-
throw new Error(FAILURE_ERROR);
|
|
6836
|
-
}
|
|
6763
|
+
async function waitForExpectedRoutes(routes, expectation, timeoutMs, pollIntervalMs) {
|
|
6764
|
+
const deadline = Date.now() + timeoutMs;
|
|
6765
|
+
let result;
|
|
6766
|
+
do {
|
|
6767
|
+
result = inspectExpectedRoutes(routes, expectation);
|
|
6768
|
+
if (result.ok) return result;
|
|
6769
|
+
if (Date.now() < deadline) {
|
|
6770
|
+
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
6771
|
+
}
|
|
6772
|
+
} while (Date.now() < deadline);
|
|
6773
|
+
return result;
|
|
6837
6774
|
}
|
|
6838
|
-
|
|
6839
|
-
|
|
6840
|
-
|
|
6841
|
-
|
|
6842
|
-
|
|
6843
|
-
|
|
6844
|
-
|
|
6845
|
-
|
|
6846
|
-
|
|
6847
|
-
|
|
6848
|
-
|
|
6849
|
-
|
|
6850
|
-
|
|
6851
|
-
|
|
6852
|
-
|
|
6853
|
-
|
|
6854
|
-
|
|
6855
|
-
|
|
6856
|
-
|
|
6857
|
-
|
|
6858
|
-
|
|
6859
|
-
|
|
6860
|
-
|
|
6861
|
-
|
|
6862
|
-
|
|
6863
|
-
|
|
6864
|
-
|
|
6865
|
-
|
|
6866
|
-
|
|
6867
|
-
|
|
6868
|
-
|
|
6869
|
-
|
|
6870
|
-
|
|
6871
|
-
|
|
6872
|
-
|
|
6873
|
-
|
|
6874
|
-
|
|
6775
|
+
async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
6776
|
+
if (routes.length === 0) return { restarted: false };
|
|
6777
|
+
const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
6778
|
+
const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
|
|
6779
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
6780
|
+
const initial = await waitForExpectedRoutes(
|
|
6781
|
+
routes,
|
|
6782
|
+
expectation,
|
|
6783
|
+
initialTimeoutMs,
|
|
6784
|
+
pollIntervalMs
|
|
6785
|
+
);
|
|
6786
|
+
if (initial.ok) return { restarted: false };
|
|
6787
|
+
if (options.allowRestart === false)
|
|
6788
|
+
throw new Error(
|
|
6789
|
+
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
6790
|
+
);
|
|
6791
|
+
import_node_fs19.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
6792
|
+
return withFileLock(
|
|
6793
|
+
ROUTER_RELOAD_LOCK_FILE,
|
|
6794
|
+
{
|
|
6795
|
+
activity: "Traefik route reload recovery",
|
|
6796
|
+
waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
|
|
6797
|
+
fair: true,
|
|
6798
|
+
onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
|
|
6799
|
+
},
|
|
6800
|
+
async () => {
|
|
6801
|
+
const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
|
|
6802
|
+
if (recheck.ok) return { restarted: false };
|
|
6803
|
+
claimLifecycleEffect();
|
|
6804
|
+
restartRouterStack();
|
|
6805
|
+
const recovered = await waitForExpectedRoutes(
|
|
6806
|
+
routes,
|
|
6807
|
+
expectation,
|
|
6808
|
+
recoveryTimeoutMs,
|
|
6809
|
+
pollIntervalMs
|
|
6810
|
+
);
|
|
6811
|
+
if (recovered.ok) return { restarted: true };
|
|
6812
|
+
const action = expectation === "loaded" ? "load" : "remove";
|
|
6813
|
+
throw new Error(
|
|
6814
|
+
`Traefik did not ${action} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
|
|
6815
|
+
);
|
|
6816
|
+
}
|
|
6817
|
+
);
|
|
6875
6818
|
}
|
|
6876
|
-
function
|
|
6877
|
-
return
|
|
6819
|
+
async function ensureTraefikRoutesLoaded(routes, options = {}) {
|
|
6820
|
+
return ensureTraefikRouteExpectation(routes, "loaded", options);
|
|
6878
6821
|
}
|
|
6879
|
-
function
|
|
6880
|
-
|
|
6881
|
-
|
|
6882
|
-
|
|
6883
|
-
|
|
6884
|
-
)
|
|
6885
|
-
|
|
6886
|
-
|
|
6887
|
-
|
|
6822
|
+
async function ensureTraefikRoutesMatch(routes, options = {}) {
|
|
6823
|
+
if (routes.length === 0) return;
|
|
6824
|
+
const timeoutMs = options.initialTimeoutMs ?? options.recoveryTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
6825
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
6826
|
+
const result = await waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs);
|
|
6827
|
+
if (!result.ok) {
|
|
6828
|
+
throw new Error(
|
|
6829
|
+
`Traefik route configuration did not match rendered routes: ${result.routes.join(", ")} (${result.details})`
|
|
6830
|
+
);
|
|
6888
6831
|
}
|
|
6889
6832
|
}
|
|
6890
|
-
function
|
|
6891
|
-
|
|
6892
|
-
return import_node_fs21.default.readFileSync(filePath);
|
|
6833
|
+
async function ensureTraefikRoutesRemoved(routes, options = {}) {
|
|
6834
|
+
return ensureTraefikRouteExpectation(routes, "removed", options);
|
|
6893
6835
|
}
|
|
6894
|
-
function
|
|
6895
|
-
|
|
6836
|
+
function assertTraefikRoutesRemoved(routes) {
|
|
6837
|
+
const result = inspectExpectedRoutes(routes, "removed");
|
|
6838
|
+
if (!result.ok) throw new Error(`Workspace route cessation is not proven: ${result.details}`);
|
|
6896
6839
|
}
|
|
6897
|
-
|
|
6898
|
-
|
|
6840
|
+
|
|
6841
|
+
// src/core/workspace-ensure.ts
|
|
6842
|
+
var import_node_child_process21 = require("child_process");
|
|
6843
|
+
var import_node_fs23 = __toESM(require("fs"));
|
|
6844
|
+
var import_node_path27 = __toESM(require("path"));
|
|
6845
|
+
|
|
6846
|
+
// src/core/devpod-mutation.ts
|
|
6847
|
+
var import_node_child_process17 = require("child_process");
|
|
6848
|
+
var import_node_fs20 = __toESM(require("fs"));
|
|
6849
|
+
var import_node_path25 = __toESM(require("path"));
|
|
6850
|
+
var DEVPOD_MUTATION_LOCK_FILE = import_node_path25.default.join(DEVROUTER_HOME, "devpod-mutation.lock");
|
|
6851
|
+
var DEVPOD_MUTATION_WAIT_MS = 18e5;
|
|
6852
|
+
var DevpodStartPostconditionError = class extends Error {
|
|
6853
|
+
};
|
|
6854
|
+
function withMutationLock2(activity, target, operation) {
|
|
6855
|
+
import_node_fs20.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
6856
|
+
return withFileLockSync(
|
|
6857
|
+
DEVPOD_MUTATION_LOCK_FILE,
|
|
6858
|
+
{
|
|
6859
|
+
activity,
|
|
6860
|
+
target: `'${target}'`,
|
|
6861
|
+
waitMs: DEVPOD_MUTATION_WAIT_MS,
|
|
6862
|
+
fair: true,
|
|
6863
|
+
onWait: createStderrWaitReporter(activity, `'${target}'`)
|
|
6864
|
+
},
|
|
6865
|
+
operation
|
|
6866
|
+
);
|
|
6899
6867
|
}
|
|
6900
|
-
function
|
|
6901
|
-
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
];
|
|
6905
|
-
|
|
6906
|
-
|
|
6907
|
-
|
|
6868
|
+
function commandFailure2(result) {
|
|
6869
|
+
return [result.error?.message, result.stdout, result.stderr].filter(Boolean).join("\n").trim();
|
|
6870
|
+
}
|
|
6871
|
+
function runDevpodAction(action, devpodId, force = false) {
|
|
6872
|
+
const args = action === "delete" ? [action, devpodId, ...force ? ["--force"] : [], "--ignore-not-found"] : [action, devpodId];
|
|
6873
|
+
claimLifecycleEffect();
|
|
6874
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, { encoding: "utf-8" });
|
|
6875
|
+
if (result.status !== 0) {
|
|
6876
|
+
throw new Error(
|
|
6877
|
+
`devpod ${action}${force ? " --force" : ""} failed for '${devpodId}': ${commandFailure2(result) || "unknown error"}`
|
|
6878
|
+
);
|
|
6908
6879
|
}
|
|
6909
|
-
return helperPath;
|
|
6910
6880
|
}
|
|
6911
|
-
function
|
|
6912
|
-
const
|
|
6913
|
-
|
|
6914
|
-
|
|
6915
|
-
|
|
6916
|
-
const devcontainerPath = import_node_path25.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
6917
|
-
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
6918
|
-
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
6919
|
-
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
6920
|
-
(value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
|
|
6881
|
+
function inspectExactOwnership2(devpodId, worktreePath) {
|
|
6882
|
+
const ownership = inspectDevpodWorkspaceOwnership(
|
|
6883
|
+
listDevpodWorkspaces(worktreePath),
|
|
6884
|
+
devpodId,
|
|
6885
|
+
worktreePath
|
|
6921
6886
|
);
|
|
6922
|
-
if (
|
|
6923
|
-
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6887
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
6888
|
+
return ownership;
|
|
6889
|
+
}
|
|
6890
|
+
function mutateOwnedDevpodWorkspace(action, devpodId, worktreePath) {
|
|
6891
|
+
return withMutationLock2(`DevPod ${action}`, worktreePath, () => {
|
|
6892
|
+
const before = inspectExactOwnership2(devpodId, worktreePath);
|
|
6893
|
+
if (before.status === "absent") return { status: "absent" };
|
|
6894
|
+
runDevpodAction(action, devpodId);
|
|
6895
|
+
let after = inspectExactOwnership2(devpodId, worktreePath);
|
|
6896
|
+
if (action === "stop" && after.status !== "owned") {
|
|
6897
|
+
throw new Error(`DevPod '${devpodId}' no longer owns '${worktreePath}' after provider stop.`);
|
|
6927
6898
|
}
|
|
6928
|
-
|
|
6929
|
-
|
|
6930
|
-
|
|
6931
|
-
|
|
6932
|
-
|
|
6933
|
-
|
|
6934
|
-
|
|
6935
|
-
);
|
|
6899
|
+
if (action === "delete" && after.status === "owned") {
|
|
6900
|
+
const runtime = inspectDevpodRuntimeStatus(devpodId, worktreePath);
|
|
6901
|
+
if (runtime !== "not-found") {
|
|
6902
|
+
throw new Error(
|
|
6903
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after provider delete (runtime=${runtime}).`
|
|
6904
|
+
);
|
|
6905
|
+
}
|
|
6906
|
+
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
6907
|
+
if (after.status === "owned") {
|
|
6908
|
+
runDevpodAction("delete", devpodId, true);
|
|
6909
|
+
after = inspectExactOwnership2(devpodId, worktreePath);
|
|
6910
|
+
}
|
|
6911
|
+
if (after.status !== "absent") {
|
|
6912
|
+
throw new Error(
|
|
6913
|
+
`DevPod '${devpodId}' still owns '${worktreePath}' after forced provider delete.`
|
|
6914
|
+
);
|
|
6915
|
+
}
|
|
6936
6916
|
}
|
|
6937
|
-
return {
|
|
6938
|
-
}
|
|
6939
|
-
|
|
6940
|
-
|
|
6941
|
-
|
|
6942
|
-
|
|
6943
|
-
|
|
6944
|
-
|
|
6945
|
-
|
|
6946
|
-
|
|
6947
|
-
}
|
|
6948
|
-
|
|
6949
|
-
return { kind: "legacy" };
|
|
6917
|
+
return { status: "changed" };
|
|
6918
|
+
});
|
|
6919
|
+
}
|
|
6920
|
+
function stopOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
6921
|
+
try {
|
|
6922
|
+
resetWorkspaceRuntimeCaches();
|
|
6923
|
+
if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
|
|
6924
|
+
return stopOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
6925
|
+
}
|
|
6926
|
+
return mutateOwnedDevpodWorkspace("stop", devpodId, worktreePath);
|
|
6927
|
+
} finally {
|
|
6928
|
+
resetWorkspaceRuntimeCaches();
|
|
6950
6929
|
}
|
|
6951
|
-
throw new Error(
|
|
6952
|
-
"Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
6953
|
-
);
|
|
6954
6930
|
}
|
|
6955
|
-
function
|
|
6956
|
-
if (
|
|
6957
|
-
|
|
6958
|
-
|
|
6959
|
-
|
|
6960
|
-
RUNTIME_HELPER_PATH,
|
|
6961
|
-
helper,
|
|
6962
|
-
"the managed process helper"
|
|
6963
|
-
);
|
|
6964
|
-
const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
|
|
6965
|
-
deliverRuntimeFile(
|
|
6966
|
-
options.container.id,
|
|
6967
|
-
runtimeAdapterPath,
|
|
6968
|
-
options.plan.adapterContents,
|
|
6969
|
-
"the managed post-start adapter"
|
|
6970
|
-
);
|
|
6971
|
-
const started = (0, import_node_child_process19.spawnSync)(
|
|
6972
|
-
"docker",
|
|
6973
|
-
[
|
|
6974
|
-
"exec",
|
|
6975
|
-
"--workdir",
|
|
6976
|
-
options.container.workspacePath,
|
|
6977
|
-
"--env",
|
|
6978
|
-
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
6979
|
-
"--env",
|
|
6980
|
-
`DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
|
|
6981
|
-
...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
|
|
6982
|
-
...options.processes ? [
|
|
6983
|
-
"--env",
|
|
6984
|
-
`DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
|
|
6985
|
-
"--env",
|
|
6986
|
-
"DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
|
|
6987
|
-
] : [],
|
|
6988
|
-
options.container.id,
|
|
6989
|
-
"bash",
|
|
6990
|
-
"-c",
|
|
6991
|
-
ADAPTER_WRAPPER,
|
|
6992
|
-
options.plan.adapterPath,
|
|
6993
|
-
runtimeAdapterPath
|
|
6994
|
-
],
|
|
6995
|
-
{ stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit" }
|
|
6996
|
-
);
|
|
6997
|
-
if (started.status !== 0) {
|
|
6998
|
-
const details = commandFailure3(started);
|
|
6999
|
-
throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
|
|
6931
|
+
function deleteOwnedDevpodWorkspace(devpodId, worktreePath) {
|
|
6932
|
+
if (resolveWorkspaceRuntimeOrDefault(worktreePath) === "devsy") {
|
|
6933
|
+
const result2 = deleteOwnedDevsyWorkspace(devpodId, worktreePath);
|
|
6934
|
+
resetWorkspaceRuntimeCaches();
|
|
6935
|
+
return result2;
|
|
7000
6936
|
}
|
|
6937
|
+
const result = mutateOwnedDevpodWorkspace("delete", devpodId, worktreePath);
|
|
6938
|
+
resetWorkspaceRuntimeCaches();
|
|
6939
|
+
return result;
|
|
7001
6940
|
}
|
|
7002
|
-
function
|
|
7003
|
-
if (
|
|
7004
|
-
|
|
6941
|
+
async function startDevpodWorkspace(options) {
|
|
6942
|
+
if (resolveWorkspaceRuntimeOrDefault(options.repoPath) === "devsy") {
|
|
6943
|
+
try {
|
|
6944
|
+
const result = await startDevsyWorkspace({
|
|
6945
|
+
repoPath: options.repoPath,
|
|
6946
|
+
devsyId: options.devpodId,
|
|
6947
|
+
devcontainerPath: options.devcontainerPath,
|
|
6948
|
+
recreate: options.recreate,
|
|
6949
|
+
quiet: options.quiet,
|
|
6950
|
+
workspace: options.workspace,
|
|
6951
|
+
inactivityTimeout: readWorkspaceRuntimeConfig().devsyInactivityTimeout
|
|
6952
|
+
});
|
|
6953
|
+
resetWorkspaceRuntimeCaches();
|
|
6954
|
+
return result;
|
|
6955
|
+
} catch (error) {
|
|
6956
|
+
if (error instanceof DevsyStartPostconditionError) {
|
|
6957
|
+
resetWorkspaceRuntimeCaches();
|
|
6958
|
+
throw new DevpodStartPostconditionError(error.message);
|
|
6959
|
+
}
|
|
6960
|
+
throw error;
|
|
6961
|
+
}
|
|
7005
6962
|
}
|
|
7006
|
-
|
|
7007
|
-
|
|
7008
|
-
|
|
7009
|
-
|
|
7010
|
-
|
|
7011
|
-
|
|
7012
|
-
"
|
|
7013
|
-
"
|
|
7014
|
-
|
|
7015
|
-
|
|
7016
|
-
|
|
7017
|
-
|
|
7018
|
-
"bash",
|
|
7019
|
-
"-c",
|
|
7020
|
-
'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
|
|
7021
|
-
"devrouter-process",
|
|
7022
|
-
options.action,
|
|
7023
|
-
options.name
|
|
7024
|
-
],
|
|
7025
|
-
{
|
|
7026
|
-
encoding: "utf-8",
|
|
7027
|
-
stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"]
|
|
6963
|
+
const activity = options.recreate ? "DevPod recreate" : "DevPod start";
|
|
6964
|
+
return withMutationLock2(activity, options.repoPath, () => {
|
|
6965
|
+
const workspaces = listDevpodWorkspaces(options.repoPath);
|
|
6966
|
+
let devpodId = options.devpodId ?? selectDevpodWorkspace(workspaces, options.repoPath)?.id;
|
|
6967
|
+
if (devpodId) {
|
|
6968
|
+
const before = inspectDevpodWorkspaceOwnership(workspaces, devpodId, options.repoPath);
|
|
6969
|
+
if (before.status === "conflict") throw new Error(before.reason);
|
|
6970
|
+
if (options.recreate && before.status !== "owned") {
|
|
6971
|
+
throw new Error(`Cannot recreate DevPod '${devpodId}' without one exact owner.`);
|
|
6972
|
+
}
|
|
6973
|
+
} else if (options.recreate) {
|
|
6974
|
+
throw new Error("Cannot recreate a DevPod before its exact id is known.");
|
|
7028
6975
|
}
|
|
7029
|
-
|
|
7030
|
-
|
|
6976
|
+
const args = ["up", options.repoPath];
|
|
6977
|
+
if (devpodId) args.push("--id", devpodId);
|
|
6978
|
+
if (options.devcontainerPath) {
|
|
6979
|
+
args.push("--devcontainer-path", options.devcontainerPath);
|
|
6980
|
+
}
|
|
6981
|
+
args.push("--open-ide=false");
|
|
6982
|
+
if (options.workspace) {
|
|
6983
|
+
args.push(
|
|
6984
|
+
"--workspace-env",
|
|
6985
|
+
`WORKSPACE=${options.workspace.token}`,
|
|
6986
|
+
"--workspace-env",
|
|
6987
|
+
`DEVROUTER_WORKSPACE=${options.workspace.token}`
|
|
6988
|
+
);
|
|
6989
|
+
}
|
|
6990
|
+
if (options.recreate) args.push("--recreate");
|
|
6991
|
+
const env = { ...process.env };
|
|
6992
|
+
if (options.workspace) {
|
|
6993
|
+
env.WORKSPACE = options.workspace.token;
|
|
6994
|
+
env.DEVROUTER_WORKSPACE = options.workspace.token;
|
|
6995
|
+
env.DEVROUTER_GIT_COMMON_DIR = options.workspace.gitCommonDir;
|
|
6996
|
+
env.DEVCONTAINER_COMPOSE_OVERLAY = "docker-compose.devrouter.yml";
|
|
6997
|
+
} else {
|
|
6998
|
+
delete env.WORKSPACE;
|
|
6999
|
+
delete env.DEVROUTER_WORKSPACE;
|
|
7000
|
+
delete env.DEVROUTER_GIT_COMMON_DIR;
|
|
7001
|
+
delete env.DEVCONTAINER_COMPOSE_OVERLAY;
|
|
7002
|
+
}
|
|
7003
|
+
claimLifecycleEffect();
|
|
7004
|
+
const result = (0, import_node_child_process17.spawnSync)("devpod", args, {
|
|
7005
|
+
stdio: options.quiet ? ["inherit", 2, "inherit"] : "inherit",
|
|
7006
|
+
env
|
|
7007
|
+
});
|
|
7031
7008
|
if (result.status !== 0) {
|
|
7032
|
-
|
|
7009
|
+
resetWorkspaceRuntimeCaches();
|
|
7010
|
+
const message = `devpod up failed for '${devpodId ?? options.repoPath}'.`;
|
|
7011
|
+
if (options.devcontainerPath) throw new DevpodStartPostconditionError(message);
|
|
7012
|
+
throw new Error(message);
|
|
7033
7013
|
}
|
|
7034
|
-
|
|
7035
|
-
|
|
7036
|
-
|
|
7037
|
-
|
|
7038
|
-
|
|
7039
|
-
|
|
7014
|
+
try {
|
|
7015
|
+
const attached = listDevpodWorkspaces(options.repoPath);
|
|
7016
|
+
devpodId ??= selectDevpodWorkspace(attached, options.repoPath)?.id;
|
|
7017
|
+
if (!devpodId) {
|
|
7018
|
+
throw new Error(`DevPod did not attach '${options.repoPath}' after startup.`);
|
|
7019
|
+
}
|
|
7020
|
+
const ownership = inspectDevpodWorkspaceOwnership(attached, devpodId, options.repoPath);
|
|
7021
|
+
if (ownership.status === "conflict") throw new Error(ownership.reason);
|
|
7022
|
+
if (ownership.status !== "owned") {
|
|
7023
|
+
throw new Error(
|
|
7024
|
+
`DevPod did not attach '${options.repoPath}' as '${devpodId}' after startup.`
|
|
7025
|
+
);
|
|
7026
|
+
}
|
|
7027
|
+
resetWorkspaceRuntimeCaches();
|
|
7028
|
+
return devpodId;
|
|
7029
|
+
} catch (error) {
|
|
7030
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
7031
|
+
throw new DevpodStartPostconditionError(message);
|
|
7040
7032
|
}
|
|
7041
|
-
}
|
|
7042
|
-
}
|
|
7043
|
-
return "drifted";
|
|
7033
|
+
});
|
|
7044
7034
|
}
|
|
7045
7035
|
|
|
7046
|
-
// src/core/
|
|
7047
|
-
var
|
|
7048
|
-
|
|
7049
|
-
|
|
7050
|
-
|
|
7051
|
-
|
|
7052
|
-
function
|
|
7053
|
-
return
|
|
7054
|
-
}
|
|
7055
|
-
function selectedResources(values, allValues, hasResolvedProfile) {
|
|
7056
|
-
if (values === void 0) return hasResolvedProfile ? [] : sortedUnique(allValues);
|
|
7057
|
-
return values.length === 1 && values[0] === "*" ? sortedUnique(allValues) : sortedUnique(values);
|
|
7058
|
-
}
|
|
7059
|
-
function containerResourceStatus(container) {
|
|
7060
|
-
if (!container) return "missing";
|
|
7061
|
-
if (!container.state.Running) return "stopped";
|
|
7062
|
-
const health = container.state.Health?.Status;
|
|
7063
|
-
if (health === "starting") return "starting";
|
|
7064
|
-
if (health === "unhealthy") return "unhealthy";
|
|
7065
|
-
return "healthy";
|
|
7066
|
-
}
|
|
7067
|
-
function isActiveResourceStatus(status) {
|
|
7068
|
-
return status === "running" || status === "healthy" || status === "starting" || status === "unhealthy";
|
|
7069
|
-
}
|
|
7070
|
-
function isReadyResourceStatus(status) {
|
|
7071
|
-
return status === "running" || status === "healthy";
|
|
7072
|
-
}
|
|
7073
|
-
function isStoppedOrMissingResourceStatus(status) {
|
|
7074
|
-
return status === "stopped" || status === "missing";
|
|
7075
|
-
}
|
|
7076
|
-
function exactRoutes(repoPath, workspace) {
|
|
7077
|
-
try {
|
|
7078
|
-
const routes = listHostRouteState().filter(
|
|
7079
|
-
(route) => sameWorkspacePath(route.repoPath, repoPath) && route.workspace === workspace
|
|
7080
|
-
);
|
|
7081
|
-
return { apps: sortedUnique(routes.map((route) => route.name)), drift: [] };
|
|
7082
|
-
} catch {
|
|
7083
|
-
return { apps: [], drift: ["managed route state could not be inspected"] };
|
|
7084
|
-
}
|
|
7036
|
+
// src/core/http-route-probe.ts
|
|
7037
|
+
var import_node_child_process18 = require("child_process");
|
|
7038
|
+
var DEFAULT_MAX_TIME_SECONDS = 5;
|
|
7039
|
+
var PARENT_TIMEOUT_PADDING_MS = 250;
|
|
7040
|
+
var CONTRACT_MAX_BUFFER = 1024;
|
|
7041
|
+
var MIME_TOKEN_RE2 = /^[!#$%&'*+\-.^_`|~0-9A-Za-z]+$/;
|
|
7042
|
+
function httpRouteUrl(host) {
|
|
7043
|
+
return `${isTLSEnabled() ? "https" : "http"}://${host}`;
|
|
7085
7044
|
}
|
|
7086
|
-
function
|
|
7087
|
-
|
|
7088
|
-
|
|
7045
|
+
function probeHttpRoute(host, options = {}) {
|
|
7046
|
+
const maxTimeSeconds = options.maxTimeSeconds ?? DEFAULT_MAX_TIME_SECONDS;
|
|
7047
|
+
if (!Number.isFinite(maxTimeSeconds) || maxTimeSeconds <= 0) {
|
|
7048
|
+
throw new Error("maxTimeSeconds must be a positive finite number.");
|
|
7089
7049
|
}
|
|
7090
|
-
|
|
7091
|
-
|
|
7092
|
-
function resourceStatusesForMissing(names, status) {
|
|
7093
|
-
return Object.fromEntries(names.map((name) => [name, status]));
|
|
7094
|
-
}
|
|
7095
|
-
function inspectManagedRuntime(options) {
|
|
7096
|
-
const {
|
|
7097
|
-
repoPath,
|
|
7098
|
-
config,
|
|
7099
|
-
registeredServices,
|
|
7100
|
-
registeredProcesses,
|
|
7101
|
-
desiredServices,
|
|
7102
|
-
desiredProcesses,
|
|
7103
|
-
workspace,
|
|
7104
|
-
state
|
|
7105
|
-
} = options;
|
|
7106
|
-
const drift = [];
|
|
7107
|
-
const baseServices = sortedUnique(config.managedRuntime?.devcontainer.baseServices ?? []);
|
|
7108
|
-
const serviceStatuses = resourceStatusesForMissing(registeredServices, "missing");
|
|
7109
|
-
const baseStatuses = resourceStatusesForMissing(baseServices, "missing");
|
|
7110
|
-
const processStatuses = resourceStatusesForMissing(registeredProcesses, "missing");
|
|
7111
|
-
const activeServices = [];
|
|
7112
|
-
const activeProcesses = [];
|
|
7113
|
-
let plan;
|
|
7114
|
-
try {
|
|
7115
|
-
plan = inspectManagedDevcontainerConfig({
|
|
7116
|
-
repoPath,
|
|
7117
|
-
config,
|
|
7118
|
-
profile: {
|
|
7119
|
-
apps: ["*"],
|
|
7120
|
-
devcontainerServices: desiredServices,
|
|
7121
|
-
processes: ["*"]
|
|
7122
|
-
},
|
|
7123
|
-
linked: workspace !== void 0
|
|
7124
|
-
});
|
|
7125
|
-
} catch {
|
|
7126
|
-
drift.push("managed Dev Container configuration could not be inspected");
|
|
7127
|
-
return {
|
|
7128
|
-
plan,
|
|
7129
|
-
primaryReady: false,
|
|
7130
|
-
primaryActive: false,
|
|
7131
|
-
baseStatuses,
|
|
7132
|
-
serviceStatuses,
|
|
7133
|
-
processStatuses,
|
|
7134
|
-
activeServices,
|
|
7135
|
-
activeProcesses,
|
|
7136
|
-
drift
|
|
7137
|
-
};
|
|
7050
|
+
if (options.readiness !== void 0) {
|
|
7051
|
+
return probeHttpReadinessSync(host, options.readiness, maxTimeSeconds, options.repoPath);
|
|
7138
7052
|
}
|
|
7139
|
-
|
|
7140
|
-
|
|
7141
|
-
|
|
7142
|
-
|
|
7143
|
-
|
|
7144
|
-
|
|
7145
|
-
|
|
7146
|
-
|
|
7147
|
-
|
|
7148
|
-
|
|
7149
|
-
|
|
7150
|
-
|
|
7151
|
-
|
|
7152
|
-
|
|
7153
|
-
|
|
7154
|
-
};
|
|
7053
|
+
const tlsEnabled = isTLSEnabled();
|
|
7054
|
+
const url = `${tlsEnabled ? "https" : "http"}://${host}`;
|
|
7055
|
+
const args = [
|
|
7056
|
+
"--silent",
|
|
7057
|
+
"--show-error",
|
|
7058
|
+
"--output",
|
|
7059
|
+
"/dev/null",
|
|
7060
|
+
"--write-out",
|
|
7061
|
+
"%{http_code}",
|
|
7062
|
+
"--max-time",
|
|
7063
|
+
String(maxTimeSeconds)
|
|
7064
|
+
];
|
|
7065
|
+
if (tlsEnabled) {
|
|
7066
|
+
getMkcertRootCAPath({ repoPath: options.repoPath });
|
|
7067
|
+
args.push("--cacert", CERT_FILE);
|
|
7155
7068
|
}
|
|
7156
|
-
|
|
7157
|
-
|
|
7158
|
-
|
|
7159
|
-
|
|
7160
|
-
|
|
7161
|
-
|
|
7162
|
-
|
|
7163
|
-
|
|
7164
|
-
|
|
7165
|
-
|
|
7166
|
-
|
|
7167
|
-
|
|
7168
|
-
};
|
|
7069
|
+
args.push(url);
|
|
7070
|
+
const result = (0, import_node_child_process18.spawnSync)("curl", args, { encoding: "utf-8" });
|
|
7071
|
+
const stdout = result.stdout?.trim() ?? "";
|
|
7072
|
+
const status = /^\d{3}$/.test(stdout) ? Number(stdout) : void 0;
|
|
7073
|
+
const ok = result.status === 0 && status !== void 0 && status >= 100 && status < 500;
|
|
7074
|
+
const details = ok ? `HTTP ${status}` : [status === void 0 ? void 0 : `HTTP ${status}`, result.stderr?.trim()].filter(Boolean).join(": ") || `curl exited with status ${result.status ?? "unknown"}`;
|
|
7075
|
+
return { ok, status, details };
|
|
7076
|
+
}
|
|
7077
|
+
function probeHttpReadinessSync(host, readiness, maxTimeSeconds, repoPath) {
|
|
7078
|
+
const parentTimeoutMs = maxTimeSeconds * 1e3 + PARENT_TIMEOUT_PADDING_MS;
|
|
7079
|
+
if (!Number.isFinite(parentTimeoutMs)) {
|
|
7080
|
+
throw new Error("maxTimeSeconds is too large for a bounded probe timeout.");
|
|
7169
7081
|
}
|
|
7170
|
-
const
|
|
7171
|
-
|
|
7172
|
-
|
|
7082
|
+
const tlsEnabled = isTLSEnabled();
|
|
7083
|
+
const args = buildHttpReadinessCurlArgs(host, readiness, maxTimeSeconds, tlsEnabled);
|
|
7084
|
+
if (tlsEnabled) {
|
|
7085
|
+
getMkcertRootCAPath({ repoPath });
|
|
7086
|
+
}
|
|
7087
|
+
let result;
|
|
7173
7088
|
try {
|
|
7174
|
-
|
|
7175
|
-
|
|
7176
|
-
|
|
7177
|
-
|
|
7178
|
-
}
|
|
7179
|
-
drift.push("managed generated Dev Container configuration is not devrouter-owned");
|
|
7180
|
-
} else if (generated.status === "drifted") {
|
|
7181
|
-
drift.push("managed generated Dev Container configuration changed");
|
|
7182
|
-
}
|
|
7089
|
+
result = (0, import_node_child_process18.spawnSync)("curl", args, {
|
|
7090
|
+
encoding: "utf-8",
|
|
7091
|
+
maxBuffer: CONTRACT_MAX_BUFFER,
|
|
7092
|
+
timeout: Math.ceil(parentTimeoutMs)
|
|
7093
|
+
});
|
|
7183
7094
|
} catch {
|
|
7184
|
-
|
|
7185
|
-
}
|
|
7186
|
-
const appContainers = workspaceAppContainers(containers, repoPath).filter(
|
|
7187
|
-
(container) => hasExactComposeIdentity(container, {
|
|
7188
|
-
repoPath,
|
|
7189
|
-
service: resolvedPlan.primaryService,
|
|
7190
|
-
composeFiles: resolvedPlan.composeFiles
|
|
7191
|
-
})
|
|
7192
|
-
);
|
|
7193
|
-
const projectScopedPrimary = state?.composeProject ? appContainers.filter(
|
|
7194
|
-
(container) => container.labels["com.docker.compose.project"] === state.composeProject
|
|
7195
|
-
) : appContainers;
|
|
7196
|
-
let primary;
|
|
7197
|
-
let composeProject = state?.composeProject;
|
|
7198
|
-
if (projectScopedPrimary.length === 1) {
|
|
7199
|
-
primary = projectScopedPrimary[0];
|
|
7200
|
-
composeProject = primary.labels["com.docker.compose.project"] ?? composeProject;
|
|
7201
|
-
} else if (projectScopedPrimary.length > 1) {
|
|
7202
|
-
drift.push("managed primary container identity is ambiguous");
|
|
7203
|
-
} else if (appContainers.length > 0) {
|
|
7204
|
-
drift.push(
|
|
7205
|
-
state?.composeProject ? "managed primary container is foreign" : "managed primary container identity is ambiguous"
|
|
7206
|
-
);
|
|
7095
|
+
return transportFailure();
|
|
7207
7096
|
}
|
|
7208
|
-
if (
|
|
7209
|
-
|
|
7097
|
+
if (result.status !== 0 || result.error) {
|
|
7098
|
+
return transportFailure();
|
|
7210
7099
|
}
|
|
7211
|
-
|
|
7212
|
-
|
|
7100
|
+
const metadata = typeof result.stdout === "string" ? result.stdout : "";
|
|
7101
|
+
return parseHttpReadinessMetadata(metadata, readiness);
|
|
7102
|
+
}
|
|
7103
|
+
function buildHttpReadinessCurlArgs(host, readiness, maxTimeSeconds, tlsEnabled) {
|
|
7104
|
+
const url = `${tlsEnabled ? "https" : "http"}://${host}${readiness.path}`;
|
|
7105
|
+
const args = [
|
|
7106
|
+
"--disable",
|
|
7107
|
+
"--globoff",
|
|
7108
|
+
"--silent",
|
|
7109
|
+
"--show-error",
|
|
7110
|
+
"--no-location",
|
|
7111
|
+
"--noproxy",
|
|
7112
|
+
"*",
|
|
7113
|
+
"--output",
|
|
7114
|
+
"/dev/null",
|
|
7115
|
+
"--write-out",
|
|
7116
|
+
"%{http_code} %{content_type}",
|
|
7117
|
+
"--max-time",
|
|
7118
|
+
String(maxTimeSeconds)
|
|
7119
|
+
];
|
|
7120
|
+
if (tlsEnabled) args.push("--cacert", CERT_FILE);
|
|
7121
|
+
args.push(url);
|
|
7122
|
+
return args;
|
|
7123
|
+
}
|
|
7124
|
+
function parseHttpReadinessMetadata(metadata, readiness) {
|
|
7125
|
+
if (metadata.length > CONTRACT_MAX_BUFFER) {
|
|
7126
|
+
return transportFailure();
|
|
7213
7127
|
}
|
|
7214
|
-
const
|
|
7215
|
-
|
|
7216
|
-
|
|
7217
|
-
service,
|
|
7218
|
-
composeProject,
|
|
7219
|
-
composeFiles: resolvedPlan.composeFiles
|
|
7220
|
-
})
|
|
7221
|
-
);
|
|
7222
|
-
const unscopedService = (service) => containers.filter(
|
|
7223
|
-
(container) => hasExactComposeIdentity(container, {
|
|
7224
|
-
repoPath,
|
|
7225
|
-
service,
|
|
7226
|
-
composeFiles: resolvedPlan.composeFiles
|
|
7227
|
-
})
|
|
7228
|
-
);
|
|
7229
|
-
const inspectService = (service, statuses) => {
|
|
7230
|
-
const exact = exactService(service);
|
|
7231
|
-
if (exact.length > 1) {
|
|
7232
|
-
statuses[service] = "drifted";
|
|
7233
|
-
drift.push(`managed service '${service}' identity is ambiguous`);
|
|
7234
|
-
return;
|
|
7235
|
-
}
|
|
7236
|
-
if (exact.length === 0) {
|
|
7237
|
-
const foreign = unscopedService(service);
|
|
7238
|
-
statuses[service] = foreign.length > 0 ? "foreign" : "missing";
|
|
7239
|
-
if (foreign.length > 0) drift.push(`managed service '${service}' is foreign`);
|
|
7240
|
-
return;
|
|
7241
|
-
}
|
|
7242
|
-
const status = containerResourceStatus(exact[0]);
|
|
7243
|
-
statuses[service] = status;
|
|
7244
|
-
if (isActiveResourceStatus(status)) {
|
|
7245
|
-
if (statuses === serviceStatuses) activeServices.push(service);
|
|
7246
|
-
}
|
|
7247
|
-
};
|
|
7248
|
-
for (const service of registeredServices) inspectService(service, serviceStatuses);
|
|
7249
|
-
for (const service of baseServices) inspectService(service, baseStatuses);
|
|
7250
|
-
const managedServicesActive = activeServices.length > 0 || Object.values(baseStatuses).some(isActiveResourceStatus);
|
|
7251
|
-
const primaryWorkspacePath = primary?.mounts.find(
|
|
7252
|
-
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
7253
|
-
)?.Destination;
|
|
7254
|
-
const primaryActive = Boolean(primary?.state.Running);
|
|
7255
|
-
if (generatedConfigMissing && (state || primaryActive || managedServicesActive)) {
|
|
7256
|
-
drift.push("managed generated Dev Container configuration is missing");
|
|
7128
|
+
const match = /^(\d{3})\t([^\r\n]*)$/.exec(metadata);
|
|
7129
|
+
if (!match) {
|
|
7130
|
+
return transportFailure();
|
|
7257
7131
|
}
|
|
7258
|
-
const
|
|
7259
|
-
|
|
7260
|
-
|
|
7261
|
-
if (managedServicesActive) {
|
|
7262
|
-
drift.push("managed primary container is missing");
|
|
7263
|
-
}
|
|
7264
|
-
} else if (!primaryActive) {
|
|
7265
|
-
if (managedServicesActive) {
|
|
7266
|
-
drift.push("managed primary container is stopped");
|
|
7267
|
-
}
|
|
7268
|
-
} else if (primaryHealth === "unhealthy") {
|
|
7269
|
-
drift.push("managed primary container is unhealthy");
|
|
7132
|
+
const status = Number(match[1]);
|
|
7133
|
+
if (status < 100 || status > 599) {
|
|
7134
|
+
return transportFailure();
|
|
7270
7135
|
}
|
|
7271
|
-
|
|
7272
|
-
|
|
7273
|
-
|
|
7274
|
-
|
|
7275
|
-
|
|
7276
|
-
container: { id: primary.id, workspacePath: primaryWorkspacePath },
|
|
7277
|
-
name: process2,
|
|
7278
|
-
action: "status",
|
|
7279
|
-
quiet: true
|
|
7280
|
-
});
|
|
7281
|
-
} catch {
|
|
7282
|
-
status = "drifted";
|
|
7283
|
-
}
|
|
7284
|
-
processStatuses[process2] = status;
|
|
7285
|
-
if (status === "running") activeProcesses.push(process2);
|
|
7286
|
-
if (desiredProcesses.includes(process2) && status !== "running") {
|
|
7287
|
-
drift.push(`managed process '${process2}' is not running`);
|
|
7288
|
-
}
|
|
7289
|
-
}
|
|
7290
|
-
} else {
|
|
7291
|
-
if (primary && !primaryActive) {
|
|
7292
|
-
for (const process2 of registeredProcesses) processStatuses[process2] = "stopped";
|
|
7293
|
-
}
|
|
7294
|
-
if (desiredProcesses.length > 0 && (primaryActive || managedServicesActive)) {
|
|
7295
|
-
drift.push("managed processes cannot be proven without the primary container");
|
|
7296
|
-
}
|
|
7136
|
+
const statuses = readiness.statuses ?? [200];
|
|
7137
|
+
const statusAllowed = statuses.includes(status);
|
|
7138
|
+
const mediaFailure = readiness.contentType ? contentTypeFailure(match[2], readiness.contentType) : void 0;
|
|
7139
|
+
if (statusAllowed && mediaFailure === void 0) {
|
|
7140
|
+
return { ok: true, status, details: `HTTP ${status}` };
|
|
7297
7141
|
}
|
|
7298
|
-
|
|
7299
|
-
|
|
7300
|
-
|
|
7301
|
-
|
|
7302
|
-
|
|
7142
|
+
const reasons = [
|
|
7143
|
+
...statusAllowed ? [] : ["status not allowed"],
|
|
7144
|
+
...mediaFailure ? [mediaFailure] : []
|
|
7145
|
+
];
|
|
7146
|
+
return {
|
|
7147
|
+
ok: false,
|
|
7148
|
+
status,
|
|
7149
|
+
details: `HTTP ${status}; ${reasons.join("; ")}`,
|
|
7150
|
+
classification: "application-contract"
|
|
7151
|
+
};
|
|
7152
|
+
}
|
|
7153
|
+
function contentTypeFailure(responseContentType, expectedContentType) {
|
|
7154
|
+
const normalized = responseContentType.split(";", 1)[0]?.trim().toLowerCase() ?? "";
|
|
7155
|
+
if (normalized.length === 0) {
|
|
7156
|
+
return "content type missing";
|
|
7303
7157
|
}
|
|
7304
|
-
|
|
7305
|
-
|
|
7306
|
-
|
|
7307
|
-
drift.push(`managed base service '${service}' is not ready`);
|
|
7308
|
-
}
|
|
7158
|
+
const separator = normalized.indexOf("/");
|
|
7159
|
+
if (separator <= 0 || separator === normalized.length - 1 || normalized.indexOf("/", separator + 1) !== -1 || !MIME_TOKEN_RE2.test(normalized.slice(0, separator)) || !MIME_TOKEN_RE2.test(normalized.slice(separator + 1))) {
|
|
7160
|
+
return "content type invalid";
|
|
7309
7161
|
}
|
|
7162
|
+
return normalized === expectedContentType.toLowerCase() ? void 0 : "content type mismatch";
|
|
7163
|
+
}
|
|
7164
|
+
function transportFailure() {
|
|
7310
7165
|
return {
|
|
7311
|
-
|
|
7312
|
-
|
|
7313
|
-
|
|
7314
|
-
primaryReady,
|
|
7315
|
-
primaryActive,
|
|
7316
|
-
baseStatuses,
|
|
7317
|
-
serviceStatuses,
|
|
7318
|
-
processStatuses,
|
|
7319
|
-
activeServices: sortedUnique(activeServices),
|
|
7320
|
-
activeProcesses: sortedUnique(activeProcesses),
|
|
7321
|
-
drift
|
|
7166
|
+
ok: false,
|
|
7167
|
+
details: "HTTP route probe transport failure",
|
|
7168
|
+
classification: "transport"
|
|
7322
7169
|
};
|
|
7323
7170
|
}
|
|
7324
|
-
|
|
7325
|
-
|
|
7326
|
-
|
|
7327
|
-
|
|
7328
|
-
|
|
7329
|
-
|
|
7330
|
-
|
|
7331
|
-
|
|
7332
|
-
|
|
7333
|
-
|
|
7334
|
-
|
|
7335
|
-
|
|
7336
|
-
|
|
7337
|
-
|
|
7338
|
-
|
|
7339
|
-
|
|
7340
|
-
|
|
7341
|
-
|
|
7342
|
-
|
|
7343
|
-
|
|
7344
|
-
|
|
7345
|
-
|
|
7346
|
-
|
|
7347
|
-
|
|
7348
|
-
|
|
7349
|
-
|
|
7350
|
-
|
|
7351
|
-
|
|
7352
|
-
|
|
7353
|
-
|
|
7354
|
-
|
|
7355
|
-
|
|
7356
|
-
|
|
7357
|
-
|
|
7358
|
-
|
|
7359
|
-
|
|
7171
|
+
|
|
7172
|
+
// src/core/managed-host-preparation.ts
|
|
7173
|
+
var import_node_child_process19 = require("child_process");
|
|
7174
|
+
var import_node_fs21 = __toESM(require("fs"));
|
|
7175
|
+
var PREPARATION_TIMEOUT_MS = 6e4;
|
|
7176
|
+
var CONFIG_READ_ERROR = "Managed host preparation could not read .devrouter.yml.";
|
|
7177
|
+
var CONFIG_CHANGED_ERROR = "Managed host preparation changed .devrouter.yml; refusing to continue.";
|
|
7178
|
+
var START_ERROR = "Managed host preparation could not start.";
|
|
7179
|
+
var FAILURE_ERROR = "Managed host preparation failed.";
|
|
7180
|
+
var TIMEOUT_ERROR = "Managed host preparation timed out after 60 seconds.";
|
|
7181
|
+
function runPreparationCommand(argv, cwd) {
|
|
7182
|
+
return new Promise((resolve) => {
|
|
7183
|
+
let child;
|
|
7184
|
+
try {
|
|
7185
|
+
child = (0, import_node_child_process19.spawn)(argv[0], argv.slice(1), {
|
|
7186
|
+
cwd,
|
|
7187
|
+
detached: true,
|
|
7188
|
+
shell: false,
|
|
7189
|
+
stdio: "ignore"
|
|
7190
|
+
});
|
|
7191
|
+
} catch {
|
|
7192
|
+
resolve("start-error");
|
|
7193
|
+
return;
|
|
7194
|
+
}
|
|
7195
|
+
let timedOut = false;
|
|
7196
|
+
const deadline = setTimeout(() => {
|
|
7197
|
+
timedOut = true;
|
|
7198
|
+
if (child.pid !== void 0 && child.exitCode === null && child.signalCode === null) {
|
|
7199
|
+
try {
|
|
7200
|
+
process.kill(-child.pid, "SIGKILL");
|
|
7201
|
+
} catch {
|
|
7202
|
+
}
|
|
7203
|
+
}
|
|
7204
|
+
}, PREPARATION_TIMEOUT_MS);
|
|
7205
|
+
child.once("error", () => {
|
|
7206
|
+
clearTimeout(deadline);
|
|
7207
|
+
resolve("start-error");
|
|
7208
|
+
});
|
|
7209
|
+
child.once("close", (code) => {
|
|
7210
|
+
clearTimeout(deadline);
|
|
7211
|
+
resolve(timedOut ? "timeout" : code === 0 ? "success" : "failure");
|
|
7212
|
+
});
|
|
7213
|
+
});
|
|
7214
|
+
}
|
|
7215
|
+
function readConfigBytes(configPath) {
|
|
7360
7216
|
try {
|
|
7361
|
-
|
|
7217
|
+
return import_node_fs21.default.readFileSync(configPath);
|
|
7362
7218
|
} catch {
|
|
7363
|
-
|
|
7219
|
+
return void 0;
|
|
7364
7220
|
}
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
if (
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
services: sortedUnique(state.desired.services),
|
|
7372
|
-
processes: sortedUnique(state.desired.processes)
|
|
7373
|
-
};
|
|
7374
|
-
const desired = {
|
|
7375
|
-
apps: desiredApps,
|
|
7376
|
-
services: desiredServices,
|
|
7377
|
-
processes: desiredProcesses
|
|
7378
|
-
};
|
|
7379
|
-
for (const [label, expected, actual] of [
|
|
7380
|
-
["apps", desired.apps, desiredState.apps],
|
|
7381
|
-
["services", desired.services, desiredState.services],
|
|
7382
|
-
["processes", desired.processes, desiredState.processes]
|
|
7383
|
-
]) {
|
|
7384
|
-
const mismatch = compareResourceSet(label, expected, actual);
|
|
7385
|
-
if (mismatch) drift.push(mismatch);
|
|
7386
|
-
}
|
|
7387
|
-
if (state.profile !== options.profile)
|
|
7388
|
-
drift.push("active profile differs from the selected profile");
|
|
7221
|
+
}
|
|
7222
|
+
async function runManagedHostPreparation(repoPath, config) {
|
|
7223
|
+
const prepareCommand = config.managedRuntime?.devcontainer.prepareCommand;
|
|
7224
|
+
if (!prepareCommand) return;
|
|
7225
|
+
if (process.platform === "win32") {
|
|
7226
|
+
throw new Error("Managed host preparation requires POSIX process-group ownership.");
|
|
7389
7227
|
}
|
|
7390
|
-
const
|
|
7391
|
-
|
|
7392
|
-
|
|
7393
|
-
|
|
7394
|
-
|
|
7395
|
-
desiredServices,
|
|
7396
|
-
desiredProcesses,
|
|
7397
|
-
workspace: options.workspace,
|
|
7398
|
-
state
|
|
7399
|
-
});
|
|
7400
|
-
drift.push(...inspection.drift);
|
|
7401
|
-
if (state && inspection.plan && state.sourceConfigSha256 !== inspection.plan.sourceConfigSha256) {
|
|
7402
|
-
drift.push("managed Dev Container source configuration changed");
|
|
7228
|
+
const resolvedRepoPath = resolveRepoPath(repoPath);
|
|
7229
|
+
const configPath = getRepoConfigPath(resolvedRepoPath);
|
|
7230
|
+
const before = readConfigBytes(configPath);
|
|
7231
|
+
if (!before) {
|
|
7232
|
+
throw new Error(CONFIG_READ_ERROR);
|
|
7403
7233
|
}
|
|
7404
|
-
|
|
7405
|
-
|
|
7234
|
+
const result = await runPreparationCommand(prepareCommand, resolvedRepoPath);
|
|
7235
|
+
const after = readConfigBytes(configPath);
|
|
7236
|
+
if (!after?.equals(before)) {
|
|
7237
|
+
throw new Error(CONFIG_CHANGED_ERROR);
|
|
7406
7238
|
}
|
|
7407
|
-
if (
|
|
7408
|
-
|
|
7239
|
+
if (result === "start-error") {
|
|
7240
|
+
throw new Error(START_ERROR);
|
|
7409
7241
|
}
|
|
7410
|
-
|
|
7411
|
-
|
|
7412
|
-
const activeApps = routes.apps;
|
|
7413
|
-
const anyActive = inspection.primaryActive || inspection.activeServices.length > 0 || Object.values(inspection.baseStatuses).some(isActiveResourceStatus) || inspection.activeProcesses.length > 0 || activeApps.length > 0;
|
|
7414
|
-
if (activeApps.length > 0 && !inspection.primaryActive) {
|
|
7415
|
-
drift.push("active app routes exist without a running primary container");
|
|
7242
|
+
if (result === "timeout") {
|
|
7243
|
+
throw new Error(TIMEOUT_ERROR);
|
|
7416
7244
|
}
|
|
7417
|
-
if (
|
|
7418
|
-
|
|
7245
|
+
if (result === "failure") {
|
|
7246
|
+
throw new Error(FAILURE_ERROR);
|
|
7419
7247
|
}
|
|
7420
|
-
|
|
7421
|
-
|
|
7248
|
+
}
|
|
7249
|
+
|
|
7250
|
+
// src/core/managed-post-start.ts
|
|
7251
|
+
var import_node_child_process20 = require("child_process");
|
|
7252
|
+
var import_node_crypto10 = require("crypto");
|
|
7253
|
+
var import_node_fs22 = __toESM(require("fs"));
|
|
7254
|
+
var import_node_path26 = __toESM(require("path"));
|
|
7255
|
+
var MANAGED_MARKER = "devrouter:managed devcontainer";
|
|
7256
|
+
var MANAGED_ADAPTER_PATH = ".devcontainer/post-start.sh";
|
|
7257
|
+
var RUNTIME_HELPER_PATH = "/tmp/devrouter/bin/devrouter-process";
|
|
7258
|
+
var ADAPTER_WRAPPER = `adapter_snapshot="$1"
|
|
7259
|
+
shift
|
|
7260
|
+
readonly DEVROUTER_PROCESS_HELPER DEVROUTER_PROCESS_ADAPTER_SHA256
|
|
7261
|
+
source "$adapter_snapshot"
|
|
7262
|
+
`;
|
|
7263
|
+
function renderDeliveryScript(targetPath) {
|
|
7264
|
+
if (!/^\/tmp\/devrouter\/bin\/[a-zA-Z0-9._-]+$/.test(targetPath)) {
|
|
7265
|
+
throw new Error(`Unsafe managed runtime delivery path: ${targetPath}`);
|
|
7422
7266
|
}
|
|
7423
|
-
|
|
7424
|
-
|
|
7267
|
+
const temporaryPrefix = import_node_path26.default.posix.basename(targetPath);
|
|
7268
|
+
return `set -eu
|
|
7269
|
+
umask 077
|
|
7270
|
+
runtime_root=/tmp/devrouter
|
|
7271
|
+
runtime_bin="$runtime_root/bin"
|
|
7272
|
+
target="${targetPath}"
|
|
7273
|
+
if [ -L "$runtime_root" ] || [ -L "$runtime_bin" ]; then
|
|
7274
|
+
echo "Refusing symlinked devrouter runtime path." >&2
|
|
7275
|
+
exit 1
|
|
7276
|
+
fi
|
|
7277
|
+
mkdir -p "$runtime_bin"
|
|
7278
|
+
chmod 700 "$runtime_root" "$runtime_bin"
|
|
7279
|
+
temporary="$(mktemp "$runtime_bin/.${temporaryPrefix}.XXXXXX")"
|
|
7280
|
+
trap 'rm -f "$temporary"' EXIT
|
|
7281
|
+
cat > "$temporary"
|
|
7282
|
+
chmod 700 "$temporary"
|
|
7283
|
+
mv -f "$temporary" "$target"
|
|
7284
|
+
trap - EXIT
|
|
7285
|
+
`;
|
|
7286
|
+
}
|
|
7287
|
+
function commandFailure3(result) {
|
|
7288
|
+
return [result.error?.message, result.stderr, result.stdout].filter(Boolean).map((value) => String(value).trim()).filter(Boolean).join("\n");
|
|
7289
|
+
}
|
|
7290
|
+
function deliverRuntimeFile(containerId, targetPath, contents, label) {
|
|
7291
|
+
const result = (0, import_node_child_process20.spawnSync)(
|
|
7292
|
+
"docker",
|
|
7293
|
+
["exec", "-i", containerId, "sh", "-c", renderDeliveryScript(targetPath)],
|
|
7294
|
+
{ input: contents, encoding: "utf-8" }
|
|
7295
|
+
);
|
|
7296
|
+
if (result.status !== 0) {
|
|
7297
|
+
const details = commandFailure3(result);
|
|
7298
|
+
throw new Error(`Could not deliver ${label}${details ? `: ${details}` : "."}`);
|
|
7425
7299
|
}
|
|
7426
|
-
|
|
7427
|
-
|
|
7300
|
+
}
|
|
7301
|
+
function readRegularFileBytes(filePath) {
|
|
7302
|
+
if (!import_node_fs22.default.existsSync(filePath) || !import_node_fs22.default.lstatSync(filePath).isFile()) return void 0;
|
|
7303
|
+
return import_node_fs22.default.readFileSync(filePath);
|
|
7304
|
+
}
|
|
7305
|
+
function readRegularFile(filePath) {
|
|
7306
|
+
return readRegularFileBytes(filePath)?.toString("utf-8");
|
|
7307
|
+
}
|
|
7308
|
+
function adapterFingerprint(adapter) {
|
|
7309
|
+
return (0, import_node_crypto10.createHash)("sha256").update(adapter).digest("hex");
|
|
7310
|
+
}
|
|
7311
|
+
function resolveProcessHelperPath() {
|
|
7312
|
+
const candidates = [
|
|
7313
|
+
import_node_path26.default.resolve(__dirname, "..", "bin", "devrouter-process"),
|
|
7314
|
+
import_node_path26.default.resolve(__dirname, "..", "..", "bin", "devrouter-process")
|
|
7315
|
+
];
|
|
7316
|
+
const helperPath = candidates.find((candidate) => import_node_fs22.default.existsSync(candidate));
|
|
7317
|
+
if (!helperPath) {
|
|
7318
|
+
throw new Error("Could not locate the packaged devrouter-process helper.");
|
|
7428
7319
|
}
|
|
7429
|
-
|
|
7430
|
-
|
|
7431
|
-
|
|
7432
|
-
|
|
7320
|
+
return helperPath;
|
|
7321
|
+
}
|
|
7322
|
+
function resolveManagedPostStartPlan(repoPath) {
|
|
7323
|
+
const adapterPath = import_node_path26.default.join(repoPath, MANAGED_ADAPTER_PATH);
|
|
7324
|
+
const adapterBytes = readRegularFileBytes(adapterPath);
|
|
7325
|
+
const adapterText = adapterBytes?.toString("utf-8");
|
|
7326
|
+
const dockerfilePath = import_node_path26.default.join(repoPath, ".devcontainer", "Dockerfile");
|
|
7327
|
+
const devcontainerPath = import_node_path26.default.join(repoPath, ".devcontainer", "devcontainer.json");
|
|
7328
|
+
const dockerfile = readRegularFile(dockerfilePath) ?? "";
|
|
7329
|
+
const devcontainer = readRegularFile(devcontainerPath) ?? "";
|
|
7330
|
+
const devrouterPattern = [adapterText, dockerfile, devcontainer].filter((value) => value !== void 0).some(
|
|
7331
|
+
(value) => value.includes("DEVROUTER_PROCESS_HELPER") || value.includes("devrouter-process")
|
|
7433
7332
|
);
|
|
7434
|
-
|
|
7435
|
-
|
|
7436
|
-
|
|
7333
|
+
if (adapterBytes === void 0) {
|
|
7334
|
+
if (devrouterPattern) {
|
|
7335
|
+
throw new Error(
|
|
7336
|
+
"Devrouter lifecycle wiring is incomplete: add the managed post-start adapter or remove the stale devrouter-process references."
|
|
7337
|
+
);
|
|
7338
|
+
}
|
|
7339
|
+
return { kind: "unmanaged" };
|
|
7340
|
+
}
|
|
7341
|
+
const adapter = adapterBytes.toString("utf-8");
|
|
7342
|
+
if (!adapter.includes(MANAGED_MARKER)) {
|
|
7343
|
+
if (devrouterPattern) {
|
|
7344
|
+
throw new Error(
|
|
7345
|
+
"Devrouter-looking post-start adapter is missing the 'devrouter:managed devcontainer' marker. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
7346
|
+
);
|
|
7347
|
+
}
|
|
7348
|
+
return { kind: "unmanaged" };
|
|
7349
|
+
}
|
|
7350
|
+
assertManagedDevcontainerLifecycle(repoPath);
|
|
7351
|
+
if (adapter.includes("DEVROUTER_PROCESS_HELPER")) {
|
|
7352
|
+
return {
|
|
7353
|
+
kind: "runtime",
|
|
7354
|
+
adapterPath: MANAGED_ADAPTER_PATH,
|
|
7355
|
+
adapterSha256: adapterFingerprint(adapterBytes),
|
|
7356
|
+
adapterContents: adapterBytes
|
|
7357
|
+
};
|
|
7358
|
+
}
|
|
7359
|
+
if (dockerfile.includes("devrouter-process") && devcontainer.includes("postStartCommand")) {
|
|
7360
|
+
return { kind: "legacy" };
|
|
7361
|
+
}
|
|
7362
|
+
throw new Error(
|
|
7363
|
+
"Managed post-start must use DEVROUTER_PROCESS_HELPER before removing the legacy image helper and postStartCommand. Regenerate or migrate the devcontainer, then retry devrouter ensure."
|
|
7364
|
+
);
|
|
7365
|
+
}
|
|
7366
|
+
function runManagedPostStart(options) {
|
|
7367
|
+
if (options.plan.kind !== "runtime") return;
|
|
7368
|
+
const helper = import_node_fs22.default.readFileSync(resolveProcessHelperPath());
|
|
7369
|
+
deliverRuntimeFile(
|
|
7370
|
+
options.container.id,
|
|
7371
|
+
RUNTIME_HELPER_PATH,
|
|
7372
|
+
helper,
|
|
7373
|
+
"the managed process helper"
|
|
7374
|
+
);
|
|
7375
|
+
const runtimeAdapterPath = `/tmp/devrouter/bin/managed-post-start-${options.plan.adapterSha256}`;
|
|
7376
|
+
deliverRuntimeFile(
|
|
7377
|
+
options.container.id,
|
|
7378
|
+
runtimeAdapterPath,
|
|
7379
|
+
options.plan.adapterContents,
|
|
7380
|
+
"the managed post-start adapter"
|
|
7381
|
+
);
|
|
7382
|
+
const started2 = (0, import_node_child_process20.spawnSync)(
|
|
7383
|
+
"docker",
|
|
7384
|
+
[
|
|
7385
|
+
"exec",
|
|
7386
|
+
"--workdir",
|
|
7387
|
+
options.container.workspacePath,
|
|
7388
|
+
"--env",
|
|
7389
|
+
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
7390
|
+
"--env",
|
|
7391
|
+
`DEVROUTER_PROCESS_ADAPTER_SHA256=${options.plan.adapterSha256}`,
|
|
7392
|
+
...options.profile ? ["--env", `DEVROUTER_PROFILE=${options.profile}`] : [],
|
|
7393
|
+
...options.processes ? [
|
|
7394
|
+
"--env",
|
|
7395
|
+
`DEVROUTER_PROCESS_SET=${options.processes.join(",")}`,
|
|
7396
|
+
"--env",
|
|
7397
|
+
"DEVROUTER_PROCESS_FINGERPRINT_ENV=DEVROUTER_PROFILE,DEVROUTER_PROCESS_SET"
|
|
7398
|
+
] : [],
|
|
7399
|
+
options.container.id,
|
|
7400
|
+
"bash",
|
|
7401
|
+
"-c",
|
|
7402
|
+
ADAPTER_WRAPPER,
|
|
7403
|
+
options.plan.adapterPath,
|
|
7404
|
+
runtimeAdapterPath
|
|
7405
|
+
],
|
|
7406
|
+
{ stdio: options.quiet ? ["ignore", 2, "inherit"] : "inherit" }
|
|
7437
7407
|
);
|
|
7438
|
-
|
|
7439
|
-
|
|
7440
|
-
|
|
7441
|
-
|
|
7442
|
-
apps: activeApps,
|
|
7443
|
-
services: inspection.activeServices,
|
|
7444
|
-
processes: inspection.activeProcesses
|
|
7445
|
-
};
|
|
7446
|
-
return {
|
|
7447
|
-
mode: "managed",
|
|
7448
|
-
status,
|
|
7449
|
-
profile: options.profile,
|
|
7450
|
-
...state?.profile ? { activeProfile: state.profile } : {},
|
|
7451
|
-
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
7452
|
-
...state?.devpodId ? { devpodId: state.devpodId } : {},
|
|
7453
|
-
...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
|
|
7454
|
-
desired: {
|
|
7455
|
-
apps: desiredApps,
|
|
7456
|
-
services: desiredServices,
|
|
7457
|
-
processes: desiredProcesses
|
|
7458
|
-
},
|
|
7459
|
-
active,
|
|
7460
|
-
serviceStatuses: inspection.serviceStatuses,
|
|
7461
|
-
baseServiceStatuses: inspection.baseStatuses,
|
|
7462
|
-
processStatuses: inspection.processStatuses,
|
|
7463
|
-
drift: sortedUnique(drift),
|
|
7464
|
-
...inspection.plan?.sourceConfigSha256 || state?.sourceConfigSha256 ? { sourceConfigSha256: inspection.plan?.sourceConfigSha256 ?? state?.sourceConfigSha256 } : {},
|
|
7465
|
-
...inspection.plan?.effectiveConfigSha256 || state?.effectiveConfigSha256 ? {
|
|
7466
|
-
effectiveConfigSha256: inspection.plan?.effectiveConfigSha256 ?? state?.effectiveConfigSha256
|
|
7467
|
-
} : {},
|
|
7468
|
-
...state?.transitionPhase ? { transitionPhase: state.transitionPhase } : {}
|
|
7469
|
-
};
|
|
7408
|
+
if (started2.status !== 0) {
|
|
7409
|
+
const details = commandFailure3(started2);
|
|
7410
|
+
throw new Error(`Managed post-start failed${details ? `: ${details}.` : "."}`);
|
|
7411
|
+
}
|
|
7470
7412
|
}
|
|
7471
|
-
|
|
7472
|
-
|
|
7473
|
-
|
|
7474
|
-
|
|
7475
|
-
var import_node_path26 = __toESM(require("path"));
|
|
7476
|
-
var ROUTER_API_BASE = "http://127.0.0.1:8080/api";
|
|
7477
|
-
var ROUTER_RELOAD_LOCK_FILE = import_node_path26.default.join(DEVROUTER_HOME, "router-reload.lock");
|
|
7478
|
-
var ROUTER_API_REQUEST_TIMEOUT_SECONDS = "2";
|
|
7479
|
-
var DEFAULT_INITIAL_TIMEOUT_MS = 3e3;
|
|
7480
|
-
var DEFAULT_RECOVERY_TIMEOUT_MS = 1e4;
|
|
7481
|
-
var DEFAULT_POLL_INTERVAL_MS = 250;
|
|
7482
|
-
var ROUTER_API_PAGE_SIZE = 1e3;
|
|
7483
|
-
function sleep(ms) {
|
|
7484
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
7413
|
+
function assertSafeProcessName(name) {
|
|
7414
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9._-]*$/.test(name)) {
|
|
7415
|
+
throw new Error(`Managed process '${name}' is not a safe exact identifier.`);
|
|
7416
|
+
}
|
|
7485
7417
|
}
|
|
7486
|
-
function
|
|
7418
|
+
function runManagedProcessAction(options) {
|
|
7419
|
+
assertSafeProcessName(options.name);
|
|
7487
7420
|
const result = (0, import_node_child_process20.spawnSync)(
|
|
7488
|
-
"
|
|
7421
|
+
"docker",
|
|
7489
7422
|
[
|
|
7490
|
-
"
|
|
7491
|
-
"--
|
|
7492
|
-
|
|
7493
|
-
"--
|
|
7494
|
-
|
|
7495
|
-
|
|
7423
|
+
"exec",
|
|
7424
|
+
"--workdir",
|
|
7425
|
+
options.container.workspacePath,
|
|
7426
|
+
"--env",
|
|
7427
|
+
`DEVROUTER_PROCESS_HELPER=${RUNTIME_HELPER_PATH}`,
|
|
7428
|
+
options.container.id,
|
|
7429
|
+
"bash",
|
|
7430
|
+
"-c",
|
|
7431
|
+
'exec "$DEVROUTER_PROCESS_HELPER" "$1" --name "$2"',
|
|
7432
|
+
"devrouter-process",
|
|
7433
|
+
options.action,
|
|
7434
|
+
options.name
|
|
7496
7435
|
],
|
|
7497
|
-
{
|
|
7436
|
+
{
|
|
7437
|
+
encoding: "utf-8",
|
|
7438
|
+
stdio: options.action === "status" ? ["ignore", "pipe", "pipe"] : ["ignore", 2, "inherit"]
|
|
7439
|
+
}
|
|
7498
7440
|
);
|
|
7499
|
-
if (
|
|
7500
|
-
|
|
7501
|
-
|
|
7502
|
-
|
|
7503
|
-
|
|
7441
|
+
if (options.action === "stop") {
|
|
7442
|
+
if (result.status !== 0) {
|
|
7443
|
+
throw new Error(`Could not stop managed process '${options.name}'.`);
|
|
7444
|
+
}
|
|
7445
|
+
return "stopped";
|
|
7504
7446
|
}
|
|
7505
|
-
let value;
|
|
7506
7447
|
try {
|
|
7507
|
-
|
|
7448
|
+
const parsed = JSON.parse(result.stdout.trim());
|
|
7449
|
+
if (parsed.status === "running" || parsed.status === "stopped" || parsed.status === "foreign" || parsed.status === "drifted") {
|
|
7450
|
+
return parsed.status;
|
|
7451
|
+
}
|
|
7508
7452
|
} catch {
|
|
7509
|
-
return { ok: false, details: `${protocol.toUpperCase()} router API returned invalid JSON` };
|
|
7510
7453
|
}
|
|
7511
|
-
|
|
7512
|
-
|
|
7454
|
+
return "drifted";
|
|
7455
|
+
}
|
|
7456
|
+
|
|
7457
|
+
// src/core/managed-runtime-status.ts
|
|
7458
|
+
var EMPTY_RESOURCES = {
|
|
7459
|
+
apps: [],
|
|
7460
|
+
services: [],
|
|
7461
|
+
processes: []
|
|
7462
|
+
};
|
|
7463
|
+
function sortedUnique(values) {
|
|
7464
|
+
return Array.from(new Set(values)).sort((left, right) => left.localeCompare(right));
|
|
7465
|
+
}
|
|
7466
|
+
function selectedResources(values, allValues, hasResolvedProfile) {
|
|
7467
|
+
if (values === void 0) return hasResolvedProfile ? [] : sortedUnique(allValues);
|
|
7468
|
+
return values.length === 1 && values[0] === "*" ? sortedUnique(allValues) : sortedUnique(values);
|
|
7469
|
+
}
|
|
7470
|
+
function containerResourceStatus(container) {
|
|
7471
|
+
if (!container) return "missing";
|
|
7472
|
+
if (!container.state.Running) return "stopped";
|
|
7473
|
+
const health = container.state.Health?.Status;
|
|
7474
|
+
if (health === "starting") return "starting";
|
|
7475
|
+
if (health === "unhealthy") return "unhealthy";
|
|
7476
|
+
return "healthy";
|
|
7477
|
+
}
|
|
7478
|
+
function isActiveResourceStatus(status) {
|
|
7479
|
+
return status === "running" || status === "healthy" || status === "starting" || status === "unhealthy";
|
|
7480
|
+
}
|
|
7481
|
+
function isReadyResourceStatus(status) {
|
|
7482
|
+
return status === "running" || status === "healthy";
|
|
7483
|
+
}
|
|
7484
|
+
function isStoppedOrMissingResourceStatus(status) {
|
|
7485
|
+
return status === "stopped" || status === "missing";
|
|
7486
|
+
}
|
|
7487
|
+
function exactRoutes(repoPath, workspace) {
|
|
7488
|
+
try {
|
|
7489
|
+
const routes = listHostRouteState().filter(
|
|
7490
|
+
(route) => sameWorkspacePath(route.repoPath, repoPath) && route.workspace === workspace
|
|
7491
|
+
);
|
|
7492
|
+
return { apps: sortedUnique(routes.map((route) => route.name)), drift: [] };
|
|
7493
|
+
} catch {
|
|
7494
|
+
return { apps: [], drift: ["managed route state could not be inspected"] };
|
|
7513
7495
|
}
|
|
7514
|
-
return {
|
|
7515
|
-
ok: true,
|
|
7516
|
-
reachedPageLimit: value.length >= ROUTER_API_PAGE_SIZE,
|
|
7517
|
-
names: new Set(
|
|
7518
|
-
value.flatMap((item) => {
|
|
7519
|
-
if (!item || typeof item !== "object") return [];
|
|
7520
|
-
const name = item.name;
|
|
7521
|
-
return typeof name === "string" ? [name] : [];
|
|
7522
|
-
})
|
|
7523
|
-
)
|
|
7524
|
-
};
|
|
7525
7496
|
}
|
|
7526
|
-
function
|
|
7527
|
-
|
|
7497
|
+
function compareResourceSet(label, expected, actual) {
|
|
7498
|
+
if (expected.length !== actual.length || expected.some((value, index) => value !== actual[index])) {
|
|
7499
|
+
return `active ${label} differ from the selected profile`;
|
|
7500
|
+
}
|
|
7501
|
+
return void 0;
|
|
7528
7502
|
}
|
|
7529
|
-
function
|
|
7530
|
-
|
|
7531
|
-
|
|
7532
|
-
|
|
7533
|
-
|
|
7534
|
-
|
|
7535
|
-
|
|
7536
|
-
|
|
7537
|
-
|
|
7538
|
-
|
|
7539
|
-
|
|
7540
|
-
|
|
7541
|
-
|
|
7542
|
-
|
|
7503
|
+
function resourceStatusesForMissing(names, status) {
|
|
7504
|
+
return Object.fromEntries(names.map((name) => [name, status]));
|
|
7505
|
+
}
|
|
7506
|
+
function inspectManagedRuntime(options) {
|
|
7507
|
+
const {
|
|
7508
|
+
repoPath,
|
|
7509
|
+
config,
|
|
7510
|
+
registeredServices,
|
|
7511
|
+
registeredProcesses,
|
|
7512
|
+
desiredServices,
|
|
7513
|
+
desiredProcesses,
|
|
7514
|
+
workspace,
|
|
7515
|
+
state
|
|
7516
|
+
} = options;
|
|
7517
|
+
const drift = [];
|
|
7518
|
+
const baseServices = sortedUnique(config.managedRuntime?.devcontainer.baseServices ?? []);
|
|
7519
|
+
const serviceStatuses = resourceStatusesForMissing(registeredServices, "missing");
|
|
7520
|
+
const baseStatuses = resourceStatusesForMissing(baseServices, "missing");
|
|
7521
|
+
const processStatuses = resourceStatusesForMissing(registeredProcesses, "missing");
|
|
7522
|
+
const activeServices = [];
|
|
7523
|
+
const activeProcesses = [];
|
|
7524
|
+
let plan;
|
|
7525
|
+
try {
|
|
7526
|
+
plan = inspectManagedDevcontainerConfig({
|
|
7527
|
+
repoPath,
|
|
7528
|
+
config,
|
|
7529
|
+
profile: {
|
|
7530
|
+
apps: ["*"],
|
|
7531
|
+
devcontainerServices: desiredServices,
|
|
7532
|
+
processes: ["*"]
|
|
7533
|
+
},
|
|
7534
|
+
linked: workspace !== void 0
|
|
7535
|
+
});
|
|
7536
|
+
} catch {
|
|
7537
|
+
drift.push("managed Dev Container configuration could not be inspected");
|
|
7543
7538
|
return {
|
|
7544
|
-
|
|
7545
|
-
|
|
7539
|
+
plan,
|
|
7540
|
+
primaryReady: false,
|
|
7541
|
+
primaryActive: false,
|
|
7542
|
+
baseStatuses,
|
|
7543
|
+
serviceStatuses,
|
|
7544
|
+
processStatuses,
|
|
7545
|
+
activeServices,
|
|
7546
|
+
activeProcesses,
|
|
7547
|
+
drift
|
|
7548
|
+
};
|
|
7549
|
+
}
|
|
7550
|
+
let containers;
|
|
7551
|
+
try {
|
|
7552
|
+
containers = inspectWorkspaceContainers();
|
|
7553
|
+
} catch {
|
|
7554
|
+
drift.push("managed workspace containers could not be inspected");
|
|
7555
|
+
return {
|
|
7556
|
+
plan,
|
|
7557
|
+
primaryReady: false,
|
|
7558
|
+
primaryActive: false,
|
|
7559
|
+
baseStatuses,
|
|
7560
|
+
serviceStatuses,
|
|
7561
|
+
processStatuses,
|
|
7562
|
+
activeServices,
|
|
7563
|
+
activeProcesses,
|
|
7564
|
+
drift
|
|
7565
|
+
};
|
|
7566
|
+
}
|
|
7567
|
+
if (!plan) {
|
|
7568
|
+
drift.push("managed Dev Container configuration could not be inspected");
|
|
7569
|
+
return {
|
|
7570
|
+
plan,
|
|
7571
|
+
primaryReady: false,
|
|
7572
|
+
primaryActive: false,
|
|
7573
|
+
baseStatuses,
|
|
7574
|
+
serviceStatuses,
|
|
7575
|
+
processStatuses,
|
|
7576
|
+
activeServices,
|
|
7577
|
+
activeProcesses,
|
|
7578
|
+
drift
|
|
7546
7579
|
};
|
|
7547
7580
|
}
|
|
7548
|
-
|
|
7581
|
+
const resolvedPlan = plan;
|
|
7582
|
+
let generatedConfigSha256;
|
|
7583
|
+
let generatedConfigMissing = false;
|
|
7549
7584
|
try {
|
|
7550
|
-
|
|
7585
|
+
const generated = inspectManagedDevcontainerGeneratedConfig(resolvedPlan);
|
|
7586
|
+
generatedConfigSha256 = generated.sha256;
|
|
7587
|
+
if (generated.status === "missing") {
|
|
7588
|
+
generatedConfigMissing = true;
|
|
7589
|
+
} else if (generated.status === "foreign") {
|
|
7590
|
+
drift.push("managed generated Dev Container configuration is not devrouter-owned");
|
|
7591
|
+
} else if (generated.status === "drifted") {
|
|
7592
|
+
drift.push("managed generated Dev Container configuration changed");
|
|
7593
|
+
}
|
|
7551
7594
|
} catch {
|
|
7552
|
-
|
|
7553
|
-
ok: false,
|
|
7554
|
-
details: `${protocol.toUpperCase()} ${resource.slice(0, -1)} API returned invalid JSON`
|
|
7555
|
-
};
|
|
7595
|
+
drift.push("managed generated Dev Container configuration could not be inspected");
|
|
7556
7596
|
}
|
|
7557
|
-
|
|
7558
|
-
|
|
7559
|
-
|
|
7560
|
-
|
|
7561
|
-
|
|
7597
|
+
const appContainers = workspaceAppContainers(containers, repoPath).filter(
|
|
7598
|
+
(container) => hasExactComposeIdentity(container, {
|
|
7599
|
+
repoPath,
|
|
7600
|
+
service: resolvedPlan.primaryService,
|
|
7601
|
+
composeFiles: resolvedPlan.composeFiles
|
|
7602
|
+
})
|
|
7603
|
+
);
|
|
7604
|
+
const projectScopedPrimary = state?.composeProject ? appContainers.filter(
|
|
7605
|
+
(container) => container.labels["com.docker.compose.project"] === state.composeProject
|
|
7606
|
+
) : appContainers;
|
|
7607
|
+
let primary;
|
|
7608
|
+
let composeProject = state?.composeProject;
|
|
7609
|
+
if (projectScopedPrimary.length === 1) {
|
|
7610
|
+
primary = projectScopedPrimary[0];
|
|
7611
|
+
composeProject = primary.labels["com.docker.compose.project"] ?? composeProject;
|
|
7612
|
+
} else if (projectScopedPrimary.length > 1) {
|
|
7613
|
+
drift.push("managed primary container identity is ambiguous");
|
|
7614
|
+
} else if (appContainers.length > 0) {
|
|
7615
|
+
drift.push(
|
|
7616
|
+
state?.composeProject ? "managed primary container is foreign" : "managed primary container identity is ambiguous"
|
|
7617
|
+
);
|
|
7562
7618
|
}
|
|
7563
|
-
|
|
7564
|
-
|
|
7565
|
-
|
|
7566
|
-
|
|
7567
|
-
|
|
7568
|
-
|
|
7569
|
-
|
|
7570
|
-
|
|
7571
|
-
|
|
7572
|
-
|
|
7573
|
-
|
|
7574
|
-
|
|
7575
|
-
|
|
7576
|
-
|
|
7577
|
-
|
|
7578
|
-
|
|
7579
|
-
|
|
7580
|
-
|
|
7581
|
-
|
|
7582
|
-
|
|
7619
|
+
if (!composeProject && appContainers.length === 1) {
|
|
7620
|
+
composeProject = appContainers[0].labels["com.docker.compose.project"];
|
|
7621
|
+
}
|
|
7622
|
+
if (primary && !composeProject) {
|
|
7623
|
+
drift.push("managed primary container has no Compose project identity");
|
|
7624
|
+
}
|
|
7625
|
+
const exactService = (service) => containers.filter(
|
|
7626
|
+
(container) => hasExactComposeIdentity(container, {
|
|
7627
|
+
repoPath,
|
|
7628
|
+
service,
|
|
7629
|
+
composeProject,
|
|
7630
|
+
composeFiles: resolvedPlan.composeFiles
|
|
7631
|
+
})
|
|
7632
|
+
);
|
|
7633
|
+
const unscopedService = (service) => containers.filter(
|
|
7634
|
+
(container) => hasExactComposeIdentity(container, {
|
|
7635
|
+
repoPath,
|
|
7636
|
+
service,
|
|
7637
|
+
composeFiles: resolvedPlan.composeFiles
|
|
7638
|
+
})
|
|
7639
|
+
);
|
|
7640
|
+
const inspectService = (service, statuses) => {
|
|
7641
|
+
const exact = exactService(service);
|
|
7642
|
+
if (exact.length > 1) {
|
|
7643
|
+
statuses[service] = "drifted";
|
|
7644
|
+
drift.push(`managed service '${service}' identity is ambiguous`);
|
|
7645
|
+
return;
|
|
7646
|
+
}
|
|
7647
|
+
if (exact.length === 0) {
|
|
7648
|
+
const foreign = unscopedService(service);
|
|
7649
|
+
statuses[service] = foreign.length > 0 ? "foreign" : "missing";
|
|
7650
|
+
if (foreign.length > 0) drift.push(`managed service '${service}' is foreign`);
|
|
7651
|
+
return;
|
|
7652
|
+
}
|
|
7653
|
+
const status = containerResourceStatus(exact[0]);
|
|
7654
|
+
statuses[service] = status;
|
|
7655
|
+
if (isActiveResourceStatus(status)) {
|
|
7656
|
+
if (statuses === serviceStatuses) activeServices.push(service);
|
|
7657
|
+
}
|
|
7583
7658
|
};
|
|
7584
|
-
const
|
|
7585
|
-
const
|
|
7586
|
-
|
|
7587
|
-
const
|
|
7588
|
-
|
|
7589
|
-
|
|
7590
|
-
const
|
|
7591
|
-
|
|
7592
|
-
|
|
7593
|
-
|
|
7594
|
-
const
|
|
7595
|
-
const
|
|
7596
|
-
|
|
7597
|
-
|
|
7598
|
-
|
|
7599
|
-
|
|
7659
|
+
for (const service of registeredServices) inspectService(service, serviceStatuses);
|
|
7660
|
+
for (const service of baseServices) inspectService(service, baseStatuses);
|
|
7661
|
+
const managedServicesActive = activeServices.length > 0 || Object.values(baseStatuses).some(isActiveResourceStatus);
|
|
7662
|
+
const primaryWorkspacePath = primary?.mounts.find(
|
|
7663
|
+
(mount) => mount.Type === "bind" && sameWorkspacePath(mount.Source, repoPath)
|
|
7664
|
+
)?.Destination;
|
|
7665
|
+
const primaryActive = Boolean(primary?.state.Running);
|
|
7666
|
+
if (generatedConfigMissing && (state || primaryActive || managedServicesActive)) {
|
|
7667
|
+
drift.push("managed generated Dev Container configuration is missing");
|
|
7668
|
+
}
|
|
7669
|
+
const primaryHealth = primary?.state.Health?.Status;
|
|
7670
|
+
const primaryReady = primaryActive && primaryHealth !== "starting" && primaryHealth !== "unhealthy";
|
|
7671
|
+
if (!primary) {
|
|
7672
|
+
if (managedServicesActive) {
|
|
7673
|
+
drift.push("managed primary container is missing");
|
|
7674
|
+
}
|
|
7675
|
+
} else if (!primaryActive) {
|
|
7676
|
+
if (managedServicesActive) {
|
|
7677
|
+
drift.push("managed primary container is stopped");
|
|
7678
|
+
}
|
|
7679
|
+
} else if (primaryHealth === "unhealthy") {
|
|
7680
|
+
drift.push("managed primary container is unhealthy");
|
|
7681
|
+
}
|
|
7682
|
+
if (primary && primaryActive && primaryWorkspacePath) {
|
|
7683
|
+
for (const process2 of registeredProcesses) {
|
|
7684
|
+
let status;
|
|
7685
|
+
try {
|
|
7686
|
+
status = runManagedProcessAction({
|
|
7687
|
+
container: { id: primary.id, workspacePath: primaryWorkspacePath },
|
|
7688
|
+
name: process2,
|
|
7689
|
+
action: "status",
|
|
7690
|
+
quiet: true
|
|
7691
|
+
});
|
|
7692
|
+
} catch {
|
|
7693
|
+
status = "drifted";
|
|
7694
|
+
}
|
|
7695
|
+
processStatuses[process2] = status;
|
|
7696
|
+
if (status === "running") activeProcesses.push(process2);
|
|
7697
|
+
if (desiredProcesses.includes(process2) && status !== "running") {
|
|
7698
|
+
drift.push(`managed process '${process2}' is not running`);
|
|
7699
|
+
}
|
|
7700
|
+
}
|
|
7701
|
+
} else {
|
|
7702
|
+
if (primary && !primaryActive) {
|
|
7703
|
+
for (const process2 of registeredProcesses) processStatuses[process2] = "stopped";
|
|
7704
|
+
}
|
|
7705
|
+
if (desiredProcesses.length > 0 && (primaryActive || managedServicesActive)) {
|
|
7706
|
+
drift.push("managed processes cannot be proven without the primary container");
|
|
7707
|
+
}
|
|
7708
|
+
}
|
|
7709
|
+
for (const service of desiredServices) {
|
|
7710
|
+
const status = serviceStatuses[service] ?? "missing";
|
|
7711
|
+
if (!isReadyResourceStatus(status) && status !== "starting" && (primaryActive || managedServicesActive)) {
|
|
7712
|
+
drift.push(`managed service '${service}' is not ready`);
|
|
7713
|
+
}
|
|
7714
|
+
}
|
|
7715
|
+
for (const service of baseServices) {
|
|
7716
|
+
const status = baseStatuses[service] ?? "missing";
|
|
7717
|
+
if (!isReadyResourceStatus(status) && status !== "starting" && (primaryActive || managedServicesActive)) {
|
|
7718
|
+
drift.push(`managed base service '${service}' is not ready`);
|
|
7719
|
+
}
|
|
7600
7720
|
}
|
|
7601
|
-
const serverField = protocol === "http" ? "url" : "address";
|
|
7602
|
-
const server = servers[0][serverField];
|
|
7603
|
-
if (typeof server !== "string") return void 0;
|
|
7604
|
-
const provider = routerName.slice(routerName.lastIndexOf("@")).trim();
|
|
7605
|
-
if (!provider.startsWith("@")) return void 0;
|
|
7606
7721
|
return {
|
|
7607
|
-
|
|
7608
|
-
|
|
7609
|
-
|
|
7610
|
-
|
|
7611
|
-
|
|
7612
|
-
|
|
7613
|
-
|
|
7722
|
+
plan,
|
|
7723
|
+
generatedConfigSha256,
|
|
7724
|
+
composeProject,
|
|
7725
|
+
primaryReady,
|
|
7726
|
+
primaryActive,
|
|
7727
|
+
baseStatuses,
|
|
7728
|
+
serviceStatuses,
|
|
7729
|
+
processStatuses,
|
|
7730
|
+
activeServices: sortedUnique(activeServices),
|
|
7731
|
+
activeProcesses: sortedUnique(activeProcesses),
|
|
7732
|
+
drift
|
|
7614
7733
|
};
|
|
7615
7734
|
}
|
|
7616
|
-
function
|
|
7617
|
-
|
|
7618
|
-
const routeName = buildHostRouteRouterName(route.repoPath, route.name);
|
|
7619
|
-
if (!expected) {
|
|
7735
|
+
function collectManagedRuntimeStatus(options) {
|
|
7736
|
+
if (!options.config.managedRuntime) {
|
|
7620
7737
|
return {
|
|
7621
|
-
|
|
7622
|
-
|
|
7623
|
-
|
|
7738
|
+
mode: "legacy",
|
|
7739
|
+
status: "legacy",
|
|
7740
|
+
profile: options.profile,
|
|
7741
|
+
desired: { ...EMPTY_RESOURCES },
|
|
7742
|
+
active: { ...EMPTY_RESOURCES },
|
|
7743
|
+
serviceStatuses: {},
|
|
7744
|
+
baseServiceStatuses: {},
|
|
7745
|
+
processStatuses: {},
|
|
7746
|
+
drift: []
|
|
7747
|
+
};
|
|
7748
|
+
}
|
|
7749
|
+
const managedRuntime = options.config.managedRuntime;
|
|
7750
|
+
const hasResolvedProfile = options.resolvedProfile !== void 0;
|
|
7751
|
+
const routedApps = options.config.apps.filter((app) => app.kind !== "dependency").map((app) => app.name);
|
|
7752
|
+
const registeredServices = sortedUnique(managedRuntime.devcontainer.profileServices);
|
|
7753
|
+
const registeredProcesses = sortedUnique(managedRuntime.processes);
|
|
7754
|
+
const desiredApps = selectedResources(
|
|
7755
|
+
options.resolvedProfile?.apps,
|
|
7756
|
+
routedApps,
|
|
7757
|
+
hasResolvedProfile
|
|
7758
|
+
);
|
|
7759
|
+
const desiredServices = selectedResources(
|
|
7760
|
+
options.resolvedProfile?.devcontainerServices,
|
|
7761
|
+
registeredServices,
|
|
7762
|
+
hasResolvedProfile
|
|
7763
|
+
);
|
|
7764
|
+
const desiredProcesses = selectedResources(
|
|
7765
|
+
options.resolvedProfile?.processes,
|
|
7766
|
+
registeredProcesses,
|
|
7767
|
+
hasResolvedProfile
|
|
7768
|
+
);
|
|
7769
|
+
let state;
|
|
7770
|
+
let stateReadFailed = false;
|
|
7771
|
+
try {
|
|
7772
|
+
state = options.candidateState ?? readManagedRuntimeState(options.repoPath, options.workspace);
|
|
7773
|
+
} catch {
|
|
7774
|
+
stateReadFailed = true;
|
|
7775
|
+
}
|
|
7776
|
+
const drift = [];
|
|
7777
|
+
if (stateReadFailed) drift.push("managed runtime state could not be read");
|
|
7778
|
+
if (state?.status === "degraded") drift.push("the last managed transition is degraded");
|
|
7779
|
+
if (state) {
|
|
7780
|
+
const desiredState = {
|
|
7781
|
+
apps: sortedUnique(state.desired.apps),
|
|
7782
|
+
services: sortedUnique(state.desired.services),
|
|
7783
|
+
processes: sortedUnique(state.desired.processes)
|
|
7784
|
+
};
|
|
7785
|
+
const desired = {
|
|
7786
|
+
apps: desiredApps,
|
|
7787
|
+
services: desiredServices,
|
|
7788
|
+
processes: desiredProcesses
|
|
7624
7789
|
};
|
|
7790
|
+
for (const [label, expected, actual] of [
|
|
7791
|
+
["apps", desired.apps, desiredState.apps],
|
|
7792
|
+
["services", desired.services, desiredState.services],
|
|
7793
|
+
["processes", desired.processes, desiredState.processes]
|
|
7794
|
+
]) {
|
|
7795
|
+
const mismatch = compareResourceSet(label, expected, actual);
|
|
7796
|
+
if (mismatch) drift.push(mismatch);
|
|
7797
|
+
}
|
|
7798
|
+
if (state.profile !== options.profile)
|
|
7799
|
+
drift.push("active profile differs from the selected profile");
|
|
7625
7800
|
}
|
|
7626
|
-
const
|
|
7627
|
-
|
|
7628
|
-
|
|
7801
|
+
const inspection = inspectManagedRuntime({
|
|
7802
|
+
repoPath: options.repoPath,
|
|
7803
|
+
config: options.config,
|
|
7804
|
+
registeredServices,
|
|
7805
|
+
registeredProcesses,
|
|
7806
|
+
desiredServices,
|
|
7807
|
+
desiredProcesses,
|
|
7808
|
+
workspace: options.workspace,
|
|
7809
|
+
state
|
|
7810
|
+
});
|
|
7811
|
+
drift.push(...inspection.drift);
|
|
7812
|
+
if (state && inspection.plan && state.sourceConfigSha256 !== inspection.plan.sourceConfigSha256) {
|
|
7813
|
+
drift.push("managed Dev Container source configuration changed");
|
|
7629
7814
|
}
|
|
7630
|
-
if (
|
|
7631
|
-
|
|
7632
|
-
ok: false,
|
|
7633
|
-
routes: [routeName],
|
|
7634
|
-
details: "active Traefik router does not match the rendered route"
|
|
7635
|
-
};
|
|
7815
|
+
if (state && inspection.plan && state.effectiveConfigSha256 !== inspection.plan.effectiveConfigSha256) {
|
|
7816
|
+
drift.push("managed Dev Container effective configuration changed");
|
|
7636
7817
|
}
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
return { ok: false, routes: [routeName], details: serviceResult.details };
|
|
7818
|
+
if (state && inspection.generatedConfigSha256 && state.effectiveConfigSha256 !== inspection.generatedConfigSha256) {
|
|
7819
|
+
drift.push("managed generated Dev Container configuration differs from last successful state");
|
|
7640
7820
|
}
|
|
7641
|
-
const
|
|
7642
|
-
|
|
7643
|
-
const
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
routes: [routeName],
|
|
7648
|
-
details: "active Traefik service does not match the rendered route"
|
|
7649
|
-
};
|
|
7821
|
+
const routes = exactRoutes(options.repoPath, options.workspace);
|
|
7822
|
+
drift.push(...routes.drift);
|
|
7823
|
+
const activeApps = routes.apps;
|
|
7824
|
+
const anyActive = inspection.primaryActive || inspection.activeServices.length > 0 || Object.values(inspection.baseStatuses).some(isActiveResourceStatus) || inspection.activeProcesses.length > 0 || activeApps.length > 0;
|
|
7825
|
+
if (activeApps.length > 0 && !inspection.primaryActive) {
|
|
7826
|
+
drift.push("active app routes exist without a running primary container");
|
|
7650
7827
|
}
|
|
7651
|
-
|
|
7652
|
-
|
|
7653
|
-
function inspectExpectedRouteMatches(routes) {
|
|
7654
|
-
const mismatched = [];
|
|
7655
|
-
const failures = [];
|
|
7656
|
-
for (const route of routes) {
|
|
7657
|
-
const result = inspectExpectedRouteMatch(route);
|
|
7658
|
-
if (!result.ok) {
|
|
7659
|
-
mismatched.push(...result.routes);
|
|
7660
|
-
failures.push(result.details);
|
|
7661
|
-
}
|
|
7828
|
+
if (desiredApps.some((app) => !activeApps.includes(app)) && anyActive) {
|
|
7829
|
+
drift.push("one or more selected app routes are missing");
|
|
7662
7830
|
}
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
routes: mismatched,
|
|
7666
|
-
details: failures.join("; ")
|
|
7667
|
-
};
|
|
7668
|
-
}
|
|
7669
|
-
async function waitForExpectedRouteMatches(routes, timeoutMs, pollIntervalMs) {
|
|
7670
|
-
const deadline = Date.now() + timeoutMs;
|
|
7671
|
-
let result;
|
|
7672
|
-
do {
|
|
7673
|
-
result = inspectExpectedRouteMatches(routes);
|
|
7674
|
-
if (result.ok) return result;
|
|
7675
|
-
if (Date.now() < deadline) {
|
|
7676
|
-
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
7677
|
-
}
|
|
7678
|
-
} while (Date.now() < deadline);
|
|
7679
|
-
return result;
|
|
7680
|
-
}
|
|
7681
|
-
function inspectExpectedRoutes(routes, expectation) {
|
|
7682
|
-
const mismatched = [];
|
|
7683
|
-
const failures = [];
|
|
7684
|
-
for (const protocol of ["http", "tcp"]) {
|
|
7685
|
-
const expected = routes.filter((route) => (route.protocol ?? "http") === protocol).map((route) => buildHostRouteRouterName(route.repoPath, route.name));
|
|
7686
|
-
if (expected.length === 0) continue;
|
|
7687
|
-
const result = inspectRouterApi(protocol);
|
|
7688
|
-
if (!result.ok) {
|
|
7689
|
-
failures.push(result.details);
|
|
7690
|
-
mismatched.push(...expected);
|
|
7691
|
-
continue;
|
|
7692
|
-
}
|
|
7693
|
-
const unexpected = expected.filter(
|
|
7694
|
-
(name) => expectation === "loaded" ? !result.names.has(name) : result.names.has(name)
|
|
7695
|
-
);
|
|
7696
|
-
const boundedAbsenceIsUncertain = result.reachedPageLimit && (expectation === "loaded" ? unexpected.length > 0 : unexpected.length === 0);
|
|
7697
|
-
if (boundedAbsenceIsUncertain) {
|
|
7698
|
-
failures.push(
|
|
7699
|
-
`${protocol.toUpperCase()} router API reached the ${ROUTER_API_PAGE_SIZE}-entry safety limit`
|
|
7700
|
-
);
|
|
7701
|
-
}
|
|
7702
|
-
mismatched.push(...unexpected);
|
|
7703
|
-
if (expectation === "removed" && unexpected.length === 0 && result.reachedPageLimit) {
|
|
7704
|
-
mismatched.push(...expected);
|
|
7705
|
-
}
|
|
7831
|
+
if (activeApps.some((app) => !desiredApps.includes(app))) {
|
|
7832
|
+
drift.push("one or more active app routes are outside the selected profile");
|
|
7706
7833
|
}
|
|
7707
|
-
|
|
7708
|
-
|
|
7709
|
-
|
|
7710
|
-
|
|
7711
|
-
|
|
7712
|
-
}
|
|
7713
|
-
|
|
7714
|
-
const
|
|
7715
|
-
|
|
7716
|
-
|
|
7717
|
-
result = inspectExpectedRoutes(routes, expectation);
|
|
7718
|
-
if (result.ok) return result;
|
|
7719
|
-
if (Date.now() < deadline) {
|
|
7720
|
-
await sleep(Math.min(pollIntervalMs, Math.max(1, deadline - Date.now())));
|
|
7721
|
-
}
|
|
7722
|
-
} while (Date.now() < deadline);
|
|
7723
|
-
return result;
|
|
7724
|
-
}
|
|
7725
|
-
async function ensureTraefikRouteExpectation(routes, expectation, options) {
|
|
7726
|
-
if (routes.length === 0) return { restarted: false };
|
|
7727
|
-
const initialTimeoutMs = options.initialTimeoutMs ?? DEFAULT_INITIAL_TIMEOUT_MS;
|
|
7728
|
-
const recoveryTimeoutMs = options.recoveryTimeoutMs ?? DEFAULT_RECOVERY_TIMEOUT_MS;
|
|
7729
|
-
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
7730
|
-
const initial = await waitForExpectedRoutes(
|
|
7731
|
-
routes,
|
|
7732
|
-
expectation,
|
|
7733
|
-
initialTimeoutMs,
|
|
7734
|
-
pollIntervalMs
|
|
7834
|
+
if (inspection.activeServices.some((service) => !desiredServices.includes(service))) {
|
|
7835
|
+
drift.push("one or more active services are outside the selected profile");
|
|
7836
|
+
}
|
|
7837
|
+
if (inspection.activeProcesses.some((process2) => !desiredProcesses.includes(process2))) {
|
|
7838
|
+
drift.push("one or more active processes are outside the selected profile");
|
|
7839
|
+
}
|
|
7840
|
+
const positivelyStopped = !inspection.primaryActive && Object.values(inspection.baseStatuses).every(isStoppedOrMissingResourceStatus) && Object.values(inspection.serviceStatuses).every(isStoppedOrMissingResourceStatus) && activeApps.length === 0;
|
|
7841
|
+
const onlyDegradedIncident = state?.status === "degraded" && drift.length === 1;
|
|
7842
|
+
const requiredServicesReady = desiredServices.every(
|
|
7843
|
+
(service) => isReadyResourceStatus(inspection.serviceStatuses[service] ?? "missing")
|
|
7735
7844
|
);
|
|
7736
|
-
|
|
7737
|
-
|
|
7738
|
-
|
|
7739
|
-
"Traefik routes did not reload; shared router restart is disabled for this operation."
|
|
7740
|
-
);
|
|
7741
|
-
import_node_fs22.default.mkdirSync(DEVROUTER_HOME, { recursive: true });
|
|
7742
|
-
return withFileLock(
|
|
7743
|
-
ROUTER_RELOAD_LOCK_FILE,
|
|
7744
|
-
{
|
|
7745
|
-
activity: "Traefik route reload recovery",
|
|
7746
|
-
waitMs: initialTimeoutMs + recoveryTimeoutMs + 1e4,
|
|
7747
|
-
fair: true,
|
|
7748
|
-
onWait: createStderrWaitReporter("Traefik route reload recovery", "shared router")
|
|
7749
|
-
},
|
|
7750
|
-
async () => {
|
|
7751
|
-
const recheck = await waitForExpectedRoutes(routes, expectation, 0, pollIntervalMs);
|
|
7752
|
-
if (recheck.ok) return { restarted: false };
|
|
7753
|
-
claimLifecycleEffect();
|
|
7754
|
-
restartRouterStack();
|
|
7755
|
-
const recovered = await waitForExpectedRoutes(
|
|
7756
|
-
routes,
|
|
7757
|
-
expectation,
|
|
7758
|
-
recoveryTimeoutMs,
|
|
7759
|
-
pollIntervalMs
|
|
7760
|
-
);
|
|
7761
|
-
if (recovered.ok) return { restarted: true };
|
|
7762
|
-
const action = expectation === "loaded" ? "load" : "remove";
|
|
7763
|
-
throw new Error(
|
|
7764
|
-
`Traefik did not ${action} file-provider routes after one restart: ${recovered.routes.join(", ")} (${recovered.details}). Inspect: devrouter logs --tail 100`
|
|
7765
|
-
);
|
|
7766
|
-
}
|
|
7845
|
+
const baseServicesReady = Object.values(inspection.baseStatuses).every(isReadyResourceStatus);
|
|
7846
|
+
const processesReady = desiredProcesses.every(
|
|
7847
|
+
(process2) => isReadyResourceStatus(inspection.processStatuses[process2] ?? "missing")
|
|
7767
7848
|
);
|
|
7768
|
-
|
|
7769
|
-
|
|
7770
|
-
|
|
7771
|
-
|
|
7772
|
-
|
|
7773
|
-
|
|
7774
|
-
|
|
7775
|
-
|
|
7776
|
-
|
|
7777
|
-
|
|
7778
|
-
|
|
7779
|
-
|
|
7780
|
-
|
|
7781
|
-
|
|
7782
|
-
}
|
|
7783
|
-
|
|
7784
|
-
|
|
7849
|
+
const routesReady = desiredApps.every((app) => activeApps.includes(app));
|
|
7850
|
+
const ready = inspection.primaryReady && requiredServicesReady && baseServicesReady && processesReady && routesReady && !stateReadFailed && !drift.length;
|
|
7851
|
+
const status = positivelyStopped && onlyDegradedIncident ? "stopped" : state?.status === "degraded" ? "failed-transition" : drift.length > 0 ? "drifted" : ready ? "ready" : anyActive ? "starting" : "stopped";
|
|
7852
|
+
const active = {
|
|
7853
|
+
apps: activeApps,
|
|
7854
|
+
services: inspection.activeServices,
|
|
7855
|
+
processes: inspection.activeProcesses
|
|
7856
|
+
};
|
|
7857
|
+
return {
|
|
7858
|
+
mode: "managed",
|
|
7859
|
+
status,
|
|
7860
|
+
profile: options.profile,
|
|
7861
|
+
...state?.profile ? { activeProfile: state.profile } : {},
|
|
7862
|
+
...options.workspace !== void 0 ? { workspace: options.workspace } : {},
|
|
7863
|
+
...state?.devpodId ? { devpodId: state.devpodId } : {},
|
|
7864
|
+
...inspection.composeProject ? { composeProject: inspection.composeProject } : {},
|
|
7865
|
+
desired: {
|
|
7866
|
+
apps: desiredApps,
|
|
7867
|
+
services: desiredServices,
|
|
7868
|
+
processes: desiredProcesses
|
|
7869
|
+
},
|
|
7870
|
+
active,
|
|
7871
|
+
serviceStatuses: inspection.serviceStatuses,
|
|
7872
|
+
baseServiceStatuses: inspection.baseStatuses,
|
|
7873
|
+
processStatuses: inspection.processStatuses,
|
|
7874
|
+
drift: sortedUnique(drift),
|
|
7875
|
+
...inspection.plan?.sourceConfigSha256 || state?.sourceConfigSha256 ? { sourceConfigSha256: inspection.plan?.sourceConfigSha256 ?? state?.sourceConfigSha256 } : {},
|
|
7876
|
+
...inspection.plan?.effectiveConfigSha256 || state?.effectiveConfigSha256 ? {
|
|
7877
|
+
effectiveConfigSha256: inspection.plan?.effectiveConfigSha256 ?? state?.effectiveConfigSha256
|
|
7878
|
+
} : {},
|
|
7879
|
+
...state?.transitionPhase ? { transitionPhase: state.transitionPhase } : {}
|
|
7880
|
+
};
|
|
7785
7881
|
}
|
|
7786
7882
|
|
|
7787
7883
|
// src/core/workspace-ensure.ts
|
|
@@ -9348,6 +9444,7 @@ var cancelled = false;
|
|
|
9348
9444
|
var lockHeld = false;
|
|
9349
9445
|
var stopProjects = [];
|
|
9350
9446
|
var stopBaselineState;
|
|
9447
|
+
var BUSY_LIFECYCLE_WAIT_MS = 30 * 60 * 1e3;
|
|
9351
9448
|
function matchesFence(record4, fence) {
|
|
9352
9449
|
return Object.entries(reliabilityFence(record4.state)).every(
|
|
9353
9450
|
([key, value]) => value === fence[key]
|
|
@@ -9465,7 +9562,7 @@ async function executeLifecycleWorker(request, operation) {
|
|
|
9465
9562
|
withMutationLock(
|
|
9466
9563
|
"Verify retained stop",
|
|
9467
9564
|
request.repoPath,
|
|
9468
|
-
() =>
|
|
9565
|
+
() => proveManagedStop(retained)
|
|
9469
9566
|
);
|
|
9470
9567
|
} else {
|
|
9471
9568
|
const containers = inspectWorkspaceContainers();
|
|
@@ -9546,9 +9643,11 @@ function proveLifecycleStopped() {
|
|
|
9546
9643
|
const settle = () => {
|
|
9547
9644
|
claimLifecycleEffect();
|
|
9548
9645
|
if (stopBaselineState) {
|
|
9549
|
-
|
|
9646
|
+
const proof = proveManagedStop(stopBaselineState);
|
|
9647
|
+
if (proof.containers.some((container) => container.state.Running))
|
|
9550
9648
|
throw new Error("Retained workspace workloads remain running.");
|
|
9551
|
-
|
|
9649
|
+
if (proof.status === "proven-absent")
|
|
9650
|
+
assertTraefikRoutesRemoved(managedStopRouteReferences(stopBaselineState));
|
|
9552
9651
|
} else {
|
|
9553
9652
|
for (const project of stopProjects) {
|
|
9554
9653
|
const containers = inspectManagedStopContainers(project);
|
|
@@ -9911,6 +10010,9 @@ var import_node_path29 = __toESM(require("path"));
|
|
|
9911
10010
|
|
|
9912
10011
|
// src/core/route-state.ts
|
|
9913
10012
|
var import_node_fs24 = __toESM(require("fs"));
|
|
10013
|
+
function listRoutesForWorktreePath(worktreePath, routes = listHostRouteState()) {
|
|
10014
|
+
return routes.filter((route) => sameWorkspacePath(route.repoPath, worktreePath));
|
|
10015
|
+
}
|
|
9914
10016
|
function removeWorkspaceRoutesForWorktree(workspace, worktreePath) {
|
|
9915
10017
|
return removeHostRoutesWhere(
|
|
9916
10018
|
(route) => route.workspace === workspace && sameWorkspacePath(route.repoPath, worktreePath)
|
|
@@ -9962,8 +10064,24 @@ async function mutateWorkspaceRuntime(action, resolved, worktrees, quiet = false
|
|
|
9962
10064
|
"Retained managed runtime has no exact provider registration; routes were preserved."
|
|
9963
10065
|
);
|
|
9964
10066
|
}
|
|
9965
|
-
const
|
|
9966
|
-
|
|
10067
|
+
const retained = action === "stop" ? readManagedRuntimeState(resolved.worktreePath, resolved.workspace) : void 0;
|
|
10068
|
+
let absent = false;
|
|
10069
|
+
const remove = () => {
|
|
10070
|
+
if (retained?.stopBaseline) {
|
|
10071
|
+
absent = proveManagedStop(retained).status === "proven-absent";
|
|
10072
|
+
if (absent && listRoutesForWorktreePath(resolved.worktreePath).some(
|
|
10073
|
+
(route) => route.workspace !== resolved.workspace || !retained.desired.apps.includes(route.name)
|
|
10074
|
+
))
|
|
10075
|
+
throw new Error("Absent stop contains routes outside the retained desired set.");
|
|
10076
|
+
}
|
|
10077
|
+
claimLifecycleEffect();
|
|
10078
|
+
return removeWorkspaceRoutesForWorktree(resolved.workspace, resolved.worktreePath);
|
|
10079
|
+
};
|
|
10080
|
+
const routes = retained?.stopBaseline ? withMutationLock("Revalidate stop routes", resolved.worktreePath, remove) : remove();
|
|
10081
|
+
await ensureTraefikRoutesRemoved([
|
|
10082
|
+
...routes,
|
|
10083
|
+
...absent && retained ? managedStopRouteReferences(retained) : []
|
|
10084
|
+
]);
|
|
9967
10085
|
if (!quiet) {
|
|
9968
10086
|
process.stdout.write(
|
|
9969
10087
|
`Freed ${routes.length} route(s) for workspace '${resolved.workspace}'.
|
|
@@ -9974,6 +10092,7 @@ async function mutateWorkspaceRuntime(action, resolved, worktrees, quiet = false
|
|
|
9974
10092
|
...mutation.status === "changed" ? { devpodId } : {},
|
|
9975
10093
|
freedRoutes: routes.length,
|
|
9976
10094
|
providerChanged: mutation.status === "changed",
|
|
10095
|
+
...absent ? { runtimeAbsent: true } : {},
|
|
9977
10096
|
workspace: resolved.workspace
|
|
9978
10097
|
};
|
|
9979
10098
|
}
|
|
@@ -10037,7 +10156,7 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
10037
10156
|
repoPath,
|
|
10038
10157
|
workspace: result.workspace,
|
|
10039
10158
|
...result.devpodId ? { devpodId: result.devpodId } : {},
|
|
10040
|
-
stopped: !options.delete && result.providerChanged,
|
|
10159
|
+
stopped: !options.delete && (result.providerChanged || result.runtimeAbsent === true),
|
|
10041
10160
|
...options.delete ? { deleted: result.providerChanged } : {},
|
|
10042
10161
|
freedRoutes: result.freedRoutes
|
|
10043
10162
|
};
|
|
@@ -10070,10 +10189,16 @@ async function environmentStop(repoPath, options = {}) {
|
|
|
10070
10189
|
}
|
|
10071
10190
|
|
|
10072
10191
|
// src/lifecycle-worker.ts
|
|
10073
|
-
|
|
10074
|
-
|
|
10075
|
-
|
|
10192
|
+
var started = false;
|
|
10193
|
+
function cancel() {
|
|
10194
|
+
cancelLifecycleWorker();
|
|
10195
|
+
if (!started && process.connected) process.disconnect();
|
|
10196
|
+
}
|
|
10197
|
+
process.on("SIGTERM", cancel);
|
|
10198
|
+
process.on("SIGINT", cancel);
|
|
10199
|
+
process.on("disconnect", cancel);
|
|
10076
10200
|
process.once("message", async (message) => {
|
|
10201
|
+
started = true;
|
|
10077
10202
|
let result;
|
|
10078
10203
|
try {
|
|
10079
10204
|
const request = message.request;
|