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