@tridha643/hestia 1.2.0 → 1.3.1
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/README.md +3 -2
- package/dist/assets/portless/dist/cli.js +4 -3
- package/dist/assets/portless/hestia-hardening.patch +7 -6
- package/dist/assets/portless/provenance.json +1 -1
- package/dist/cli.js +935 -378
- package/dist/cli.js.map +32 -28
- package/dist/daemon.js +624 -225
- package/dist/daemon.js.map +21 -19
- package/package.json +1 -1
- package/skills/hestia/SKILL.md +12 -1
package/dist/daemon.js
CHANGED
|
@@ -6947,7 +6947,7 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6947
6947
|
// packages/engine/src/daemon/main.ts
|
|
6948
6948
|
import { chmodSync as chmodSync6, rmSync as rmSync11 } from "fs";
|
|
6949
6949
|
import { randomBytes } from "crypto";
|
|
6950
|
-
import { join as
|
|
6950
|
+
import { join as join26 } from "path";
|
|
6951
6951
|
|
|
6952
6952
|
// packages/session-broker-core/src/limits.ts
|
|
6953
6953
|
var MAX_HTTP_BODY_BYTES = 4 * 1024 * 1024;
|
|
@@ -7893,7 +7893,7 @@ function parsePidfile(source, path) {
|
|
|
7893
7893
|
if (pidfile.schemaVersion !== undefined && pidfile.schemaVersion !== STATE_SCHEMA_VERSION) {
|
|
7894
7894
|
throw new HestiaError("state-corrupt", `invalid pidfile ${path}: unsupported schemaVersion`, { path });
|
|
7895
7895
|
}
|
|
7896
|
-
if (typeof pidfile.name !== "string" || !Number.
|
|
7896
|
+
if (typeof pidfile.name !== "string" || !Number.isSafeInteger(pidfile.pid) || pidfile.pid <= 0 || typeof pidfile.startTime !== "string" || typeof pidfile.logPath !== "string") {
|
|
7897
7897
|
throw new HestiaError("state-corrupt", `invalid pidfile ${path}: missing process identity`, { path });
|
|
7898
7898
|
}
|
|
7899
7899
|
if (pidfile.schemaVersion === STATE_SCHEMA_VERSION && typeof pidfile.specFingerprint !== "string") {
|
|
@@ -7911,12 +7911,25 @@ function removePidfile(worktreeRoot, name) {
|
|
|
7911
7911
|
rmSync2(pidfilePath(worktreeRoot, name), { force: true });
|
|
7912
7912
|
}
|
|
7913
7913
|
function listPidfiles(dir) {
|
|
7914
|
+
const scan = scanPidfiles(dir);
|
|
7915
|
+
if (scan.errors[0] !== undefined)
|
|
7916
|
+
throw scan.errors[0];
|
|
7917
|
+
return scan.pidfiles;
|
|
7918
|
+
}
|
|
7919
|
+
function scanPidfiles(dir) {
|
|
7914
7920
|
if (!existsSync(dir))
|
|
7915
|
-
return [];
|
|
7916
|
-
|
|
7917
|
-
|
|
7918
|
-
|
|
7919
|
-
|
|
7921
|
+
return { pidfiles: [], errors: [] };
|
|
7922
|
+
const pidfiles = [];
|
|
7923
|
+
const errors = [];
|
|
7924
|
+
for (const file of readdirSync(dir).filter((candidate) => candidate.endsWith(".json"))) {
|
|
7925
|
+
const path = join(dir, file);
|
|
7926
|
+
try {
|
|
7927
|
+
pidfiles.push(parsePidfile(readFileSync(path, "utf8"), path));
|
|
7928
|
+
} catch (error) {
|
|
7929
|
+
errors.push(error);
|
|
7930
|
+
}
|
|
7931
|
+
}
|
|
7932
|
+
return { pidfiles, errors };
|
|
7920
7933
|
}
|
|
7921
7934
|
function procSpecFingerprint(spec) {
|
|
7922
7935
|
const env = Object.entries(spec.env ?? {}).sort(([left], [right]) => left.localeCompare(right));
|
|
@@ -7929,23 +7942,28 @@ function procSpecFingerprint(spec) {
|
|
|
7929
7942
|
backend: spec.backend ?? "proc",
|
|
7930
7943
|
resolver: spec.varlock ? "varlock" : "direct",
|
|
7931
7944
|
inspectorPort: spec.inspectorPort ?? null,
|
|
7945
|
+
healthPath: spec.healthPath ?? null,
|
|
7932
7946
|
configPath: spec.configPath ?? null,
|
|
7933
7947
|
originService: spec.originService ?? null,
|
|
7934
7948
|
originEndpoint: spec.originEndpoint ?? null
|
|
7935
7949
|
});
|
|
7936
7950
|
return createHash2("sha256").update(canonical).digest("hex");
|
|
7937
7951
|
}
|
|
7938
|
-
function
|
|
7952
|
+
function probeProcessStartTime(pid, env) {
|
|
7939
7953
|
try {
|
|
7940
7954
|
const out = execFileSync("ps", ["-o", "lstart=", "-p", String(pid)], {
|
|
7941
7955
|
encoding: "utf8",
|
|
7942
7956
|
env
|
|
7943
7957
|
}).trim();
|
|
7944
|
-
return out === "" ? null : out;
|
|
7958
|
+
return { status: "ok", value: out === "" ? null : out };
|
|
7945
7959
|
} catch {
|
|
7946
|
-
return
|
|
7960
|
+
return { status: "error" };
|
|
7947
7961
|
}
|
|
7948
7962
|
}
|
|
7963
|
+
function readProcessStartTime(pid, env) {
|
|
7964
|
+
const result = probeProcessStartTime(pid, env);
|
|
7965
|
+
return result.status === "ok" ? result.value : null;
|
|
7966
|
+
}
|
|
7949
7967
|
function startTimeOf(pid) {
|
|
7950
7968
|
return readProcessStartTime(pid, { ...process.env, LC_ALL: "C", LANG: "C" });
|
|
7951
7969
|
}
|
|
@@ -8009,30 +8027,40 @@ function plausibleProcessLocales(canonicalStartTime, recordedStartTime) {
|
|
|
8009
8027
|
});
|
|
8010
8028
|
return [...new Set(plausible)];
|
|
8011
8029
|
}
|
|
8012
|
-
function
|
|
8030
|
+
function probeLegacyLocalizedStartTime(pid, canonicalStartTime, recordedStartTime) {
|
|
8013
8031
|
const cacheKey = `${pid}\x00${canonicalStartTime}\x00${recordedStartTime}`;
|
|
8014
8032
|
const cached = legacyStartTimeMatches.get(cacheKey);
|
|
8015
8033
|
if (cached !== undefined)
|
|
8016
|
-
return cached;
|
|
8034
|
+
return cached ? "live" : "dead";
|
|
8017
8035
|
let matched = false;
|
|
8036
|
+
let readFailed = false;
|
|
8018
8037
|
for (const locale of plausibleProcessLocales(canonicalStartTime, recordedStartTime)) {
|
|
8019
|
-
const
|
|
8038
|
+
const read = probeProcessStartTime(pid, {
|
|
8020
8039
|
...process.env,
|
|
8021
8040
|
LC_ALL: locale,
|
|
8022
8041
|
LANG: locale
|
|
8023
8042
|
});
|
|
8024
|
-
if (
|
|
8043
|
+
if (read.status === "error") {
|
|
8044
|
+
readFailed = true;
|
|
8045
|
+
continue;
|
|
8046
|
+
}
|
|
8047
|
+
if (read.value !== null && processStartTimeMatches(read.value, recordedStartTime)) {
|
|
8025
8048
|
matched = true;
|
|
8026
8049
|
break;
|
|
8027
8050
|
}
|
|
8028
8051
|
}
|
|
8052
|
+
if (!matched && readFailed)
|
|
8053
|
+
return "unknown";
|
|
8029
8054
|
if (legacyStartTimeMatches.size >= 512) {
|
|
8030
8055
|
const oldest = legacyStartTimeMatches.keys().next().value;
|
|
8031
8056
|
if (oldest !== undefined)
|
|
8032
8057
|
legacyStartTimeMatches.delete(oldest);
|
|
8033
8058
|
}
|
|
8034
8059
|
legacyStartTimeMatches.set(cacheKey, matched);
|
|
8035
|
-
return matched;
|
|
8060
|
+
return matched ? "live" : "dead";
|
|
8061
|
+
}
|
|
8062
|
+
function legacyLocalizedStartTimeMatches(pid, canonicalStartTime, recordedStartTime) {
|
|
8063
|
+
return probeLegacyLocalizedStartTime(pid, canonicalStartTime, recordedStartTime) === "live";
|
|
8036
8064
|
}
|
|
8037
8065
|
function processStartTimeMatches(current, recorded) {
|
|
8038
8066
|
if (current === recorded)
|
|
@@ -8054,6 +8082,27 @@ function isLive(pf) {
|
|
|
8054
8082
|
return true;
|
|
8055
8083
|
return current !== null && legacyLocalizedStartTimeMatches(pf.pid, current, pf.startTime);
|
|
8056
8084
|
}
|
|
8085
|
+
function probeProcessIdentity(pf) {
|
|
8086
|
+
try {
|
|
8087
|
+
process.kill(pf.pid, 0);
|
|
8088
|
+
} catch (error) {
|
|
8089
|
+
return error.code === "ESRCH" ? "dead" : "unknown";
|
|
8090
|
+
}
|
|
8091
|
+
let current;
|
|
8092
|
+
try {
|
|
8093
|
+
current = execFileSync("ps", ["-o", "lstart=", "-p", String(pf.pid)], {
|
|
8094
|
+
encoding: "utf8",
|
|
8095
|
+
env: { ...process.env, LC_ALL: "C", LANG: "C" }
|
|
8096
|
+
}).trim();
|
|
8097
|
+
} catch {
|
|
8098
|
+
return "unknown";
|
|
8099
|
+
}
|
|
8100
|
+
if (current === "")
|
|
8101
|
+
return "dead";
|
|
8102
|
+
if (processStartTimeMatches(current, pf.startTime))
|
|
8103
|
+
return "live";
|
|
8104
|
+
return probeLegacyLocalizedStartTime(pf.pid, current, pf.startTime);
|
|
8105
|
+
}
|
|
8057
8106
|
|
|
8058
8107
|
// packages/engine/src/proc/lock.ts
|
|
8059
8108
|
var RETRY_MS = 100;
|
|
@@ -8208,6 +8257,13 @@ function readMirrorState(project) {
|
|
|
8208
8257
|
chmodSync3(p, 384);
|
|
8209
8258
|
return record;
|
|
8210
8259
|
}
|
|
8260
|
+
function readMirrorStateSafe(project) {
|
|
8261
|
+
try {
|
|
8262
|
+
return { status: "ok", record: readMirrorState(project) };
|
|
8263
|
+
} catch (error) {
|
|
8264
|
+
return { status: "error", error };
|
|
8265
|
+
}
|
|
8266
|
+
}
|
|
8211
8267
|
function clearState(worktreeRoot, project) {
|
|
8212
8268
|
const p = statePath(worktreeRoot);
|
|
8213
8269
|
if (existsSync2(p))
|
|
@@ -8221,7 +8277,7 @@ function clearState(worktreeRoot, project) {
|
|
|
8221
8277
|
import { existsSync as existsSync21, rmSync as rmSync10 } from "fs";
|
|
8222
8278
|
import { execFile as execFile10 } from "child_process";
|
|
8223
8279
|
import { promisify as promisify9 } from "util";
|
|
8224
|
-
import { join as
|
|
8280
|
+
import { join as join25, resolve as resolve3 } from "path";
|
|
8225
8281
|
import { createHash as createHash7 } from "crypto";
|
|
8226
8282
|
import { resolve4, resolve6, resolveCname } from "dns/promises";
|
|
8227
8283
|
|
|
@@ -8543,6 +8599,27 @@ import { existsSync as existsSync4, readFileSync as readFileSync6 } from "fs";
|
|
|
8543
8599
|
import { join as join6 } from "path";
|
|
8544
8600
|
var WORKLOAD_NAME = /^[A-Za-z0-9][A-Za-z0-9._-]{0,127}$/;
|
|
8545
8601
|
var BINDING = /^(?:main|[1-9][0-9]{0,4})\/(?:tcp|udp)$/;
|
|
8602
|
+
var ENVIRONMENT_KEY = /^[A-Za-z_][A-Za-z0-9_]*$/;
|
|
8603
|
+
function parseEnvironment(value, path) {
|
|
8604
|
+
if (value === undefined)
|
|
8605
|
+
return {};
|
|
8606
|
+
const environment = {};
|
|
8607
|
+
for (const [name, rawValue] of Object.entries(table(value, path))) {
|
|
8608
|
+
if (!ENVIRONMENT_KEY.test(name))
|
|
8609
|
+
throw new Error(`${path} has invalid environment key ${JSON.stringify(name)}`);
|
|
8610
|
+
if (typeof rawValue === "string") {
|
|
8611
|
+
environment[name] = rawValue;
|
|
8612
|
+
continue;
|
|
8613
|
+
}
|
|
8614
|
+
const fileValue = table(rawValue, `${path}.${name}`);
|
|
8615
|
+
knownKeys(fileValue, ["file"], `${path}.${name}`);
|
|
8616
|
+
if (typeof fileValue.file !== "string" || !/^\.hestia\//.test(fileValue.file) || fileValue.file.includes("..")) {
|
|
8617
|
+
throw new Error(`${path}.${name}.file must be a path below .hestia/`);
|
|
8618
|
+
}
|
|
8619
|
+
environment[name] = { file: fileValue.file };
|
|
8620
|
+
}
|
|
8621
|
+
return environment;
|
|
8622
|
+
}
|
|
8546
8623
|
function emptyConfig() {
|
|
8547
8624
|
return { version: 1, workloads: {} };
|
|
8548
8625
|
}
|
|
@@ -8602,7 +8679,19 @@ function parseRepositoryWorkloadConfig(source, path) {
|
|
|
8602
8679
|
if (!WORKLOAD_NAME.test(name))
|
|
8603
8680
|
throw new Error(`invalid workload name ${JSON.stringify(name)}`);
|
|
8604
8681
|
const workload = table(rawWorkload, `workloads.${name}`);
|
|
8605
|
-
knownKeys(workload, [
|
|
8682
|
+
knownKeys(workload, [
|
|
8683
|
+
"source",
|
|
8684
|
+
"compose_service",
|
|
8685
|
+
"dockerfile",
|
|
8686
|
+
"command",
|
|
8687
|
+
"wrangler_config",
|
|
8688
|
+
"port",
|
|
8689
|
+
"cwd",
|
|
8690
|
+
"varlock",
|
|
8691
|
+
"health_path",
|
|
8692
|
+
"env",
|
|
8693
|
+
"endpoints"
|
|
8694
|
+
], `workloads.${name}`);
|
|
8606
8695
|
if (!["compose", "dockerfile", "proc", "wrangler"].includes(workload.source)) {
|
|
8607
8696
|
throw new Error(`workloads.${name}.source must be compose, dockerfile, proc, or wrangler`);
|
|
8608
8697
|
}
|
|
@@ -8618,6 +8707,18 @@ function parseRepositoryWorkloadConfig(source, path) {
|
|
|
8618
8707
|
throw new Error(`workloads.${name}.${key} must be a string`);
|
|
8619
8708
|
}
|
|
8620
8709
|
}
|
|
8710
|
+
if (workload.cwd !== undefined && typeof workload.cwd !== "string") {
|
|
8711
|
+
throw new Error(`workloads.${name}.cwd must be a string`);
|
|
8712
|
+
}
|
|
8713
|
+
if (workload.varlock !== undefined && typeof workload.varlock !== "boolean") {
|
|
8714
|
+
throw new Error(`workloads.${name}.varlock must be a boolean`);
|
|
8715
|
+
}
|
|
8716
|
+
if (workload.health_path !== undefined && (typeof workload.health_path !== "string" || !workload.health_path.startsWith("/"))) {
|
|
8717
|
+
throw new Error(`workloads.${name}.health_path must start with /`);
|
|
8718
|
+
}
|
|
8719
|
+
if (workload.health_path !== undefined && workload.port === "none") {
|
|
8720
|
+
throw new Error(`workloads.${name}.health_path requires a port`);
|
|
8721
|
+
}
|
|
8621
8722
|
if (sourceKind === "compose" && typeof workload.compose_service !== "string") {
|
|
8622
8723
|
throw new Error(`workloads.${name}.compose_service is required for compose`);
|
|
8623
8724
|
}
|
|
@@ -8631,6 +8732,10 @@ function parseRepositoryWorkloadConfig(source, path) {
|
|
|
8631
8732
|
command: workload.command,
|
|
8632
8733
|
wranglerConfig: workload.wrangler_config,
|
|
8633
8734
|
port: workload.port,
|
|
8735
|
+
cwd: workload.cwd,
|
|
8736
|
+
varlock: workload.varlock,
|
|
8737
|
+
healthPath: workload.health_path,
|
|
8738
|
+
env: parseEnvironment(workload.env, `workloads.${name}.env`),
|
|
8634
8739
|
endpoints: parseEndpoints(workload.endpoints, `workloads.${name}.endpoints`)
|
|
8635
8740
|
};
|
|
8636
8741
|
}
|
|
@@ -8689,7 +8794,12 @@ function sameEndpoint(left, right) {
|
|
|
8689
8794
|
return left.binding === right.binding && left.kind === right.kind && left.local === right.local;
|
|
8690
8795
|
}
|
|
8691
8796
|
function cloneWorkload(workload) {
|
|
8692
|
-
return {
|
|
8797
|
+
return {
|
|
8798
|
+
...workload,
|
|
8799
|
+
command: workload.command?.slice(),
|
|
8800
|
+
env: { ...workload.env },
|
|
8801
|
+
endpoints: { ...workload.endpoints }
|
|
8802
|
+
};
|
|
8693
8803
|
}
|
|
8694
8804
|
function mergeConfigLayers(repository, machine) {
|
|
8695
8805
|
const workloads = {};
|
|
@@ -8715,6 +8825,10 @@ function mergeConfigLayers(repository, machine) {
|
|
|
8715
8825
|
conflicts.push(`machine workload ${name} cannot replace committed source ${committed.source} with ${overlay.source}`);
|
|
8716
8826
|
continue;
|
|
8717
8827
|
}
|
|
8828
|
+
committed.cwd = overlay.cwd ?? committed.cwd;
|
|
8829
|
+
committed.varlock = overlay.varlock ?? committed.varlock;
|
|
8830
|
+
committed.healthPath = overlay.healthPath ?? committed.healthPath;
|
|
8831
|
+
committed.env = { ...committed.env, ...overlay.env };
|
|
8718
8832
|
for (const [alias, endpoint] of Object.entries(overlay.endpoints)) {
|
|
8719
8833
|
const existing = committed.endpoints[alias];
|
|
8720
8834
|
if (existing !== undefined && !sameEndpoint(existing, endpoint)) {
|
|
@@ -8993,7 +9107,7 @@ async function composeUp(ctx, services) {
|
|
|
8993
9107
|
async function composeDown(ctx, destroy) {
|
|
8994
9108
|
const rest = ["down", "--remove-orphans"];
|
|
8995
9109
|
if (destroy)
|
|
8996
|
-
rest.push("-v");
|
|
9110
|
+
rest.push("-v", "--rmi", "local");
|
|
8997
9111
|
await docker(ctx, rest);
|
|
8998
9112
|
}
|
|
8999
9113
|
async function composePs(ctx) {
|
|
@@ -9074,7 +9188,7 @@ function allocatePort() {
|
|
|
9074
9188
|
});
|
|
9075
9189
|
});
|
|
9076
9190
|
}
|
|
9077
|
-
function
|
|
9191
|
+
function tryPsSnapshot() {
|
|
9078
9192
|
try {
|
|
9079
9193
|
const out = execFileSync2("ps", ["-A", "-o", "pid=,pgid=,ppid="], {
|
|
9080
9194
|
encoding: "utf8",
|
|
@@ -9089,11 +9203,10 @@ function psSnapshot() {
|
|
|
9089
9203
|
}
|
|
9090
9204
|
return rows;
|
|
9091
9205
|
} catch {
|
|
9092
|
-
return
|
|
9206
|
+
return null;
|
|
9093
9207
|
}
|
|
9094
9208
|
}
|
|
9095
|
-
function
|
|
9096
|
-
const rows = psSnapshot();
|
|
9209
|
+
function processTreeFromSnapshot(rootPid, rows) {
|
|
9097
9210
|
const byParent = new Map;
|
|
9098
9211
|
for (const r of rows) {
|
|
9099
9212
|
const list = byParent.get(r.ppid) ?? [];
|
|
@@ -9116,6 +9229,9 @@ function processTree(rootPid) {
|
|
|
9116
9229
|
}
|
|
9117
9230
|
return tree;
|
|
9118
9231
|
}
|
|
9232
|
+
function processTree(rootPid) {
|
|
9233
|
+
return processTreeFromSnapshot(rootPid, tryPsSnapshot() ?? []);
|
|
9234
|
+
}
|
|
9119
9235
|
function parseLsof(out) {
|
|
9120
9236
|
const listeners = [];
|
|
9121
9237
|
let pid = 0;
|
|
@@ -9152,12 +9268,12 @@ async function allListeners() {
|
|
|
9152
9268
|
return parseLsof(stdout);
|
|
9153
9269
|
} catch (err) {
|
|
9154
9270
|
const e = err;
|
|
9155
|
-
if (typeof e.stdout === "string" && e.code
|
|
9271
|
+
if (typeof e.stdout === "string" && e.code === 1 && (e.stderr ?? "").trim() === "") {
|
|
9156
9272
|
tool = "lsof";
|
|
9157
9273
|
return parseLsof(e.stdout);
|
|
9158
9274
|
}
|
|
9159
|
-
if (
|
|
9160
|
-
|
|
9275
|
+
if (e.code !== "ENOENT")
|
|
9276
|
+
throw err;
|
|
9161
9277
|
}
|
|
9162
9278
|
}
|
|
9163
9279
|
try {
|
|
@@ -9166,16 +9282,24 @@ async function allListeners() {
|
|
|
9166
9282
|
});
|
|
9167
9283
|
tool = "ss";
|
|
9168
9284
|
return parseSs(stdout);
|
|
9169
|
-
} catch {
|
|
9285
|
+
} catch (error) {
|
|
9286
|
+
if (error.code !== "ENOENT")
|
|
9287
|
+
throw error;
|
|
9170
9288
|
throw new HestiaError("ownership-tool-missing", "neither lsof nor ss is available \u2014 cannot verify port ownership " + "(required: tools like next dev silently bind a different port when " + "theirs is taken, so a bare listen-check would report the wrong port healthy)");
|
|
9171
9289
|
}
|
|
9172
9290
|
}
|
|
9173
9291
|
async function inspectPort(rootPid, port) {
|
|
9174
|
-
const
|
|
9292
|
+
const firstSnapshot = tryPsSnapshot();
|
|
9293
|
+
if (firstSnapshot === null)
|
|
9294
|
+
throw new Error("Port ownership process-tree inspection failed: ps unavailable");
|
|
9295
|
+
const members = new Set(processTreeFromSnapshot(rootPid, firstSnapshot).map((r) => r.pid));
|
|
9175
9296
|
const listeners = await allListeners();
|
|
9176
9297
|
const owner = listeners.find((l) => l.port === port);
|
|
9177
9298
|
if (owner !== undefined && !members.has(owner.pid)) {
|
|
9178
|
-
|
|
9299
|
+
const secondSnapshot = tryPsSnapshot();
|
|
9300
|
+
if (secondSnapshot === null)
|
|
9301
|
+
throw new Error("Port ownership process-tree recheck failed: ps unavailable");
|
|
9302
|
+
for (const member of processTreeFromSnapshot(rootPid, secondSnapshot))
|
|
9179
9303
|
members.add(member.pid);
|
|
9180
9304
|
}
|
|
9181
9305
|
return {
|
|
@@ -9237,19 +9361,32 @@ async function stopProcTree(t, graceMs = GRACE_MS) {
|
|
|
9237
9361
|
|
|
9238
9362
|
// packages/engine/src/proc/resolver.ts
|
|
9239
9363
|
import { existsSync as existsSync6 } from "fs";
|
|
9240
|
-
import { join as join8 } from "path";
|
|
9241
|
-
function detectVarlock(worktreeRoot) {
|
|
9242
|
-
const
|
|
9243
|
-
const
|
|
9244
|
-
|
|
9364
|
+
import { dirname as dirname3, join as join8, resolve as resolve2 } from "path";
|
|
9365
|
+
function detectVarlock(worktreeRoot, workingDirectory = worktreeRoot) {
|
|
9366
|
+
const root = resolve2(worktreeRoot);
|
|
9367
|
+
const cwd = resolve2(workingDirectory);
|
|
9368
|
+
if (!existsSync6(join8(cwd, ".env.schema")))
|
|
9369
|
+
return null;
|
|
9370
|
+
let directory = cwd;
|
|
9371
|
+
for (;; ) {
|
|
9372
|
+
const bin = join8(directory, "node_modules", ".bin", "varlock");
|
|
9373
|
+
if (existsSync6(bin))
|
|
9374
|
+
return bin;
|
|
9375
|
+
if (directory === root)
|
|
9376
|
+
return null;
|
|
9377
|
+
const parent = dirname3(directory);
|
|
9378
|
+
if (parent === directory || !parent.startsWith(root))
|
|
9379
|
+
return null;
|
|
9380
|
+
directory = parent;
|
|
9381
|
+
}
|
|
9245
9382
|
}
|
|
9246
9383
|
function wrapWithVarlock(bin, argv) {
|
|
9247
9384
|
return [bin, "run", "--no-redact-stdout", "--", ...argv];
|
|
9248
9385
|
}
|
|
9249
|
-
function requireVarlock(worktreeRoot) {
|
|
9250
|
-
const bin = detectVarlock(worktreeRoot);
|
|
9386
|
+
function requireVarlock(worktreeRoot, workingDirectory = worktreeRoot) {
|
|
9387
|
+
const bin = detectVarlock(worktreeRoot, workingDirectory);
|
|
9251
9388
|
if (bin === null) {
|
|
9252
|
-
throw new HestiaError("varlock-missing", `--varlock requires
|
|
9389
|
+
throw new HestiaError("varlock-missing", `--varlock requires ${join8(workingDirectory, ".env.schema")} and a local ` + `node_modules/.bin/varlock between that directory and the worktree root`);
|
|
9253
9390
|
}
|
|
9254
9391
|
return bin;
|
|
9255
9392
|
}
|
|
@@ -9281,7 +9418,7 @@ function spawnOnce(argv, cwd, env, logPath, attempt) {
|
|
|
9281
9418
|
closeSync2(openProcAttemptLog(logPath, attempt));
|
|
9282
9419
|
const relaySpec = Buffer.from(JSON.stringify({ argv, cwd, env, logPath })).toString("base64");
|
|
9283
9420
|
const relayFd = openSync2(logPath, "a", 384);
|
|
9284
|
-
return new Promise((
|
|
9421
|
+
return new Promise((resolve3, reject) => {
|
|
9285
9422
|
const child = spawn(process.execPath, [PROC_RELAY_ENTRYPOINT], {
|
|
9286
9423
|
cwd,
|
|
9287
9424
|
env: { ...process.env, [RELAY_SPEC_ENV]: relaySpec },
|
|
@@ -9295,7 +9432,7 @@ function spawnOnce(argv, cwd, env, logPath, attempt) {
|
|
|
9295
9432
|
child.once("spawn", () => {
|
|
9296
9433
|
closeSync2(relayFd);
|
|
9297
9434
|
child.unref();
|
|
9298
|
-
|
|
9435
|
+
resolve3(child.pid);
|
|
9299
9436
|
});
|
|
9300
9437
|
});
|
|
9301
9438
|
}
|
|
@@ -9312,18 +9449,22 @@ async function startProc(worktreeRoot, spec, stackEnv, mirrorPidfile2) {
|
|
|
9312
9449
|
const cwd = spec.cwd ? join9(worktreeRoot, spec.cwd) : worktreeRoot;
|
|
9313
9450
|
const wantPort = spec.port !== "none";
|
|
9314
9451
|
const readyTimeoutMs = spec.readyTimeoutMs ?? READY_TIMEOUT_MS;
|
|
9315
|
-
const varlockBin = spec.varlock ? requireVarlock(worktreeRoot) : null;
|
|
9452
|
+
const varlockBin = spec.varlock ? requireVarlock(worktreeRoot, cwd) : null;
|
|
9316
9453
|
let lastFailure = "";
|
|
9317
9454
|
for (let attempt = 1;attempt <= MAX_ATTEMPTS; attempt++) {
|
|
9318
9455
|
const port = wantPort ? await allocatePort() : undefined;
|
|
9319
9456
|
let argv = port === undefined ? spec.argv : substitutePort(spec.argv, port);
|
|
9320
9457
|
if (varlockBin !== null)
|
|
9321
9458
|
argv = wrapWithVarlock(varlockBin, argv);
|
|
9459
|
+
const resolvedSpecEnvironment = port === undefined ? spec.env : Object.fromEntries(Object.entries(spec.env ?? {}).map(([name, value]) => [
|
|
9460
|
+
name,
|
|
9461
|
+
substitutePort([value], port)[0]
|
|
9462
|
+
]));
|
|
9322
9463
|
const env = {
|
|
9323
9464
|
...process.env,
|
|
9324
9465
|
...stackEnv,
|
|
9325
9466
|
...port !== undefined ? { PORT: String(port), [`HESTIA_${envKey(spec.name)}_PORT`]: String(port) } : {},
|
|
9326
|
-
...
|
|
9467
|
+
...resolvedSpecEnvironment
|
|
9327
9468
|
};
|
|
9328
9469
|
const pid = await spawnOnce(argv, cwd, env, logPath, attempt);
|
|
9329
9470
|
const startTime = startTimeOf(pid) ?? "";
|
|
@@ -9343,7 +9484,7 @@ async function startProc(worktreeRoot, spec, stackEnv, mirrorPidfile2) {
|
|
|
9343
9484
|
};
|
|
9344
9485
|
writePidfile(worktreeRoot, pf);
|
|
9345
9486
|
mirrorPidfile2?.(pf);
|
|
9346
|
-
const outcome = await waitUntilReady(pf, port, readyTimeoutMs);
|
|
9487
|
+
const outcome = await waitUntilReady(pf, port, readyTimeoutMs, spec.healthPath);
|
|
9347
9488
|
const record = {
|
|
9348
9489
|
name: spec.name,
|
|
9349
9490
|
backend: pf.backend,
|
|
@@ -9379,7 +9520,7 @@ async function startProc(worktreeRoot, spec, stackEnv, mirrorPidfile2) {
|
|
|
9379
9520
|
continue;
|
|
9380
9521
|
case "timeout": {
|
|
9381
9522
|
record.state = "unhealthy";
|
|
9382
|
-
const bound = outcome.memberPorts.length > 0 ? `it is listening on port(s) ${outcome.memberPorts.join(", ")} instead` : `it never opened a listening socket (if it isn't a server, use --no-port)`;
|
|
9523
|
+
const bound = outcome.healthPending ? `it owned port ${port}, but ${spec.healthPath} never returned a 2xx response` : outcome.memberPorts.length > 0 ? `it is listening on port(s) ${outcome.memberPorts.join(", ")} instead` : `it never opened a listening socket (if it isn't a server, use --no-port)`;
|
|
9383
9524
|
return {
|
|
9384
9525
|
record,
|
|
9385
9526
|
pidfile: pf,
|
|
@@ -9390,33 +9531,46 @@ async function startProc(worktreeRoot, spec, stackEnv, mirrorPidfile2) {
|
|
|
9390
9531
|
}
|
|
9391
9532
|
throw new HestiaError("port-allocation-failed", `could not hold a port for "${spec.name}" after ${MAX_ATTEMPTS} attempts (${lastFailure})`);
|
|
9392
9533
|
}
|
|
9393
|
-
async function waitUntilReady(pf, port, timeoutMs) {
|
|
9534
|
+
async function waitUntilReady(pf, port, timeoutMs, healthPath) {
|
|
9394
9535
|
const deadline = Date.now() + timeoutMs;
|
|
9395
9536
|
if (port === undefined) {
|
|
9396
9537
|
await new Promise((r) => setTimeout(r, NO_PORT_GRACE_MS));
|
|
9397
9538
|
return isLive(pf) ? { kind: "ready" } : { kind: "exited" };
|
|
9398
9539
|
}
|
|
9399
9540
|
let memberPorts = [];
|
|
9541
|
+
let ownershipProven = false;
|
|
9400
9542
|
while (Date.now() < deadline) {
|
|
9401
9543
|
if (!isLive(pf))
|
|
9402
9544
|
return { kind: "exited" };
|
|
9403
|
-
|
|
9404
|
-
|
|
9405
|
-
|
|
9406
|
-
|
|
9407
|
-
|
|
9408
|
-
|
|
9545
|
+
if (!ownershipProven) {
|
|
9546
|
+
const view = await inspectPort(pf.pgid, port);
|
|
9547
|
+
memberPorts = view.memberPorts;
|
|
9548
|
+
if (view.ownerIsMember)
|
|
9549
|
+
ownershipProven = true;
|
|
9550
|
+
else if (view.owner !== undefined)
|
|
9551
|
+
return { kind: "stolen", byPid: view.owner.pid };
|
|
9552
|
+
}
|
|
9553
|
+
if (ownershipProven) {
|
|
9554
|
+
if (healthPath === undefined)
|
|
9555
|
+
return { kind: "ready" };
|
|
9556
|
+
try {
|
|
9557
|
+
const response = await fetch(`http://127.0.0.1:${port}${healthPath}`);
|
|
9558
|
+
if (response.ok)
|
|
9559
|
+
return { kind: "ready" };
|
|
9560
|
+
} catch {}
|
|
9409
9561
|
}
|
|
9410
9562
|
await new Promise((r) => setTimeout(r, POLL_MS2));
|
|
9411
9563
|
}
|
|
9412
|
-
return {
|
|
9564
|
+
return {
|
|
9565
|
+
kind: "timeout",
|
|
9566
|
+
memberPorts,
|
|
9567
|
+
healthPending: healthPath !== undefined && ownershipProven
|
|
9568
|
+
};
|
|
9413
9569
|
}
|
|
9414
9570
|
|
|
9415
9571
|
// packages/engine/src/router/local-http-router.ts
|
|
9416
|
-
import { execFile as execFile7 } from "child_process";
|
|
9417
9572
|
import { Agent, createServer as createServer2, request as httpRequest } from "http";
|
|
9418
9573
|
import { createConnection, createServer as createNetServer } from "net";
|
|
9419
|
-
import { promisify as promisify6 } from "util";
|
|
9420
9574
|
import { existsSync as existsSync14, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "fs";
|
|
9421
9575
|
import { chmodSync as chmodSync5, lstatSync, mkdirSync as mkdirSync7, rmSync as rmSync9 } from "fs";
|
|
9422
9576
|
import { tmpdir } from "os";
|
|
@@ -9630,7 +9784,7 @@ function resolveLocalRouteHostnames(records, config = readHestiaMachineConfig().
|
|
|
9630
9784
|
import { execFile as execFile6, spawnSync } from "child_process";
|
|
9631
9785
|
import { existsSync as existsSync12, readFileSync as readFileSync12, rmSync as rmSync7, statSync } from "fs";
|
|
9632
9786
|
import { promisify as promisify5 } from "util";
|
|
9633
|
-
import { dirname as
|
|
9787
|
+
import { dirname as dirname5, join as join15 } from "path";
|
|
9634
9788
|
|
|
9635
9789
|
// packages/engine/src/daemon/ensure.ts
|
|
9636
9790
|
import { closeSync as closeSync3, openSync as openSync3 } from "fs";
|
|
@@ -9693,12 +9847,12 @@ function resolveMaxStacks() {
|
|
|
9693
9847
|
}
|
|
9694
9848
|
return { maxStacks: DEFAULT_MAX_STACKS, warnings };
|
|
9695
9849
|
}
|
|
9696
|
-
var dockerProbe = (project) => new Promise((
|
|
9850
|
+
var dockerProbe = (project) => new Promise((resolve3) => {
|
|
9697
9851
|
execFile5("docker", ["ps", "-q", "--filter", `label=${LABELS.stack}=${project}`], { timeout: DOCKER_PROBE_TIMEOUT_MS }, (err, stdout) => {
|
|
9698
9852
|
if (err)
|
|
9699
|
-
|
|
9853
|
+
resolve3(null);
|
|
9700
9854
|
else
|
|
9701
|
-
|
|
9855
|
+
resolve3(stdout.trim() === "" ? "dead" : "live");
|
|
9702
9856
|
});
|
|
9703
9857
|
});
|
|
9704
9858
|
|
|
@@ -9929,7 +10083,7 @@ async function releaseSharedForProject(port, project, service) {
|
|
|
9929
10083
|
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync6 } from "fs";
|
|
9930
10084
|
import { execFileSync as execFileSync3 } from "child_process";
|
|
9931
10085
|
import { homedir as homedir2 } from "os";
|
|
9932
|
-
import { dirname as
|
|
10086
|
+
import { dirname as dirname4, join as join13 } from "path";
|
|
9933
10087
|
import { fileURLToPath } from "url";
|
|
9934
10088
|
var LAUNCHD_LABEL = "dev.hestia.daemon";
|
|
9935
10089
|
function launchAgentsDir() {
|
|
@@ -9939,7 +10093,7 @@ function plistPath() {
|
|
|
9939
10093
|
return join13(launchAgentsDir(), `${LAUNCHD_LABEL}.plist`);
|
|
9940
10094
|
}
|
|
9941
10095
|
function daemonMainPath() {
|
|
9942
|
-
const bundled = join13(
|
|
10096
|
+
const bundled = join13(dirname4(fileURLToPath(import.meta.url)), "daemon.js");
|
|
9943
10097
|
if (existsSync11(bundled))
|
|
9944
10098
|
return bundled;
|
|
9945
10099
|
return fileURLToPath(new URL("./main.ts", import.meta.url));
|
|
@@ -9976,10 +10130,10 @@ function kickstart() {
|
|
|
9976
10130
|
}
|
|
9977
10131
|
|
|
9978
10132
|
// packages/engine/src/daemon/routes.ts
|
|
9979
|
-
var HESTIAD_PROTOCOL_VERSION =
|
|
10133
|
+
var HESTIAD_PROTOCOL_VERSION = 6;
|
|
9980
10134
|
var MAX_JSON_BODY_BYTES = 16 * 1024;
|
|
9981
10135
|
var MAX_LOG_STREAMS = 16;
|
|
9982
|
-
var MAX_LOG_TAIL =
|
|
10136
|
+
var MAX_LOG_TAIL = 2000;
|
|
9983
10137
|
function admissionIdentity(identity2) {
|
|
9984
10138
|
if (typeof identity2 !== "string")
|
|
9985
10139
|
return identity2;
|
|
@@ -10016,14 +10170,14 @@ class Admission {
|
|
|
10016
10170
|
const first = await this.#locked(() => this.#try(identity2, holder));
|
|
10017
10171
|
if (first.granted || waitMs <= 0)
|
|
10018
10172
|
return first;
|
|
10019
|
-
return new Promise((
|
|
10173
|
+
return new Promise((resolve3) => {
|
|
10020
10174
|
const waiter = {
|
|
10021
10175
|
identity: identity2,
|
|
10022
10176
|
holder,
|
|
10023
|
-
resolve:
|
|
10177
|
+
resolve: resolve3,
|
|
10024
10178
|
timer: setTimeout(() => {
|
|
10025
10179
|
this.#queue = this.#queue.filter((candidate) => candidate !== waiter);
|
|
10026
|
-
|
|
10180
|
+
resolve3({ granted: false, live: first.live });
|
|
10027
10181
|
}, waitMs)
|
|
10028
10182
|
};
|
|
10029
10183
|
this.#queue.push(waiter);
|
|
@@ -10085,11 +10239,11 @@ class Admission {
|
|
|
10085
10239
|
return { granted: false, live: occupancy.live };
|
|
10086
10240
|
}
|
|
10087
10241
|
async#pump() {
|
|
10088
|
-
if (this.#queue.length === 0)
|
|
10089
|
-
return;
|
|
10090
10242
|
const { maxStacks } = resolveMaxStacks();
|
|
10091
10243
|
const occupancy = await this.ledger.occupancy();
|
|
10092
10244
|
this.#cache(occupancy);
|
|
10245
|
+
if (this.#queue.length === 0)
|
|
10246
|
+
return;
|
|
10093
10247
|
let used = occupancy.live.length + occupancy.reserved.length;
|
|
10094
10248
|
const granted = [];
|
|
10095
10249
|
for (const waiter of this.#queue) {
|
|
@@ -10716,8 +10870,86 @@ async function updateSharedHostname(name, mutate) {
|
|
|
10716
10870
|
});
|
|
10717
10871
|
}
|
|
10718
10872
|
|
|
10719
|
-
// packages/engine/src/router/
|
|
10873
|
+
// packages/engine/src/router/origin-liveness.ts
|
|
10874
|
+
import { execFile as execFile7 } from "child_process";
|
|
10875
|
+
import { promisify as promisify6 } from "util";
|
|
10720
10876
|
var pexec6 = promisify6(execFile7);
|
|
10877
|
+
async function probeDockerOrigin(target, port) {
|
|
10878
|
+
try {
|
|
10879
|
+
const service = target.service;
|
|
10880
|
+
const args = [
|
|
10881
|
+
"ps",
|
|
10882
|
+
"--no-trunc",
|
|
10883
|
+
"--format",
|
|
10884
|
+
"{{.ID}}\t{{.Ports}}",
|
|
10885
|
+
"--filter",
|
|
10886
|
+
`label=dev.hestia.stack=${target.project}`,
|
|
10887
|
+
"--filter",
|
|
10888
|
+
`label=com.docker.compose.service=${service.name}`
|
|
10889
|
+
];
|
|
10890
|
+
const { stdout } = await pexec6("docker", args, { timeout: 2000 });
|
|
10891
|
+
const matches = stdout.split(`
|
|
10892
|
+
`).some((line) => {
|
|
10893
|
+
const [id, ports = ""] = line.split("\t");
|
|
10894
|
+
const identityMatches = service.containerId === undefined || id?.startsWith(service.containerId);
|
|
10895
|
+
return identityMatches && new RegExp(`(?:127\\.0\\.0\\.1|0\\.0\\.0\\.0):${port}->`).test(ports);
|
|
10896
|
+
});
|
|
10897
|
+
return matches ? "live" : "dead";
|
|
10898
|
+
} catch {
|
|
10899
|
+
return "unknown";
|
|
10900
|
+
}
|
|
10901
|
+
}
|
|
10902
|
+
async function probeProcOrigin(service, port) {
|
|
10903
|
+
if (service.pid === undefined || service.startTime === undefined)
|
|
10904
|
+
return "dead";
|
|
10905
|
+
if (!Number.isSafeInteger(service.pid) || service.pid <= 0 || typeof service.startTime !== "string") {
|
|
10906
|
+
throw new Error("Route origin has an invalid process identity");
|
|
10907
|
+
}
|
|
10908
|
+
const identity2 = probeProcessIdentity({ pid: service.pid, startTime: service.startTime });
|
|
10909
|
+
if (identity2 !== "live")
|
|
10910
|
+
return identity2;
|
|
10911
|
+
try {
|
|
10912
|
+
return (await inspectPort(service.pid, port)).ownerIsMember ? "live" : "dead";
|
|
10913
|
+
} catch {
|
|
10914
|
+
return "unknown";
|
|
10915
|
+
}
|
|
10916
|
+
}
|
|
10917
|
+
async function probeLocalRouterTarget(target) {
|
|
10918
|
+
const service = target.service;
|
|
10919
|
+
const port = service?.publishedPort;
|
|
10920
|
+
if (service === undefined || port === undefined || service.backend === "tunnel")
|
|
10921
|
+
return "dead";
|
|
10922
|
+
return service.backend === "docker" ? await probeDockerOrigin(target, port) : await probeProcOrigin(service, port);
|
|
10923
|
+
}
|
|
10924
|
+
async function verifyLocalRouterTarget(target) {
|
|
10925
|
+
const port = target.service?.publishedPort;
|
|
10926
|
+
try {
|
|
10927
|
+
return port !== undefined && await probeLocalRouterTarget(target) === "live" ? port : null;
|
|
10928
|
+
} catch {
|
|
10929
|
+
return null;
|
|
10930
|
+
}
|
|
10931
|
+
}
|
|
10932
|
+
async function verifyStackServiceOrigin(record, service, publishedPort = service.publishedPort) {
|
|
10933
|
+
return await verifyLocalRouterTarget({
|
|
10934
|
+
project: record.project,
|
|
10935
|
+
service: { ...service, publishedPort }
|
|
10936
|
+
}) !== null;
|
|
10937
|
+
}
|
|
10938
|
+
function resolveSharedContractOrigin(mirror, contractService) {
|
|
10939
|
+
const endpoint = mirror.endpoints.find((candidate) => (candidate.alias ?? candidate.name) === contractService);
|
|
10940
|
+
const service = mirror.services.find((candidate) => candidate.name === (endpoint?.workload ?? endpoint?.name));
|
|
10941
|
+
const binding = service?.bindings?.find((candidate) => `${candidate.target}/${candidate.protocol}` === endpoint?.binding);
|
|
10942
|
+
const publishedPort = binding?.publishedPort ?? service?.publishedPort;
|
|
10943
|
+
return service === undefined || service.backend === "tunnel" || publishedPort === undefined ? undefined : { ...service, publishedPort };
|
|
10944
|
+
}
|
|
10945
|
+
async function probeSharedHolderOrigin(mirror, contractService) {
|
|
10946
|
+
const service = resolveSharedContractOrigin(mirror, contractService);
|
|
10947
|
+
if (service === undefined)
|
|
10948
|
+
return "unknown";
|
|
10949
|
+
return await probeLocalRouterTarget({ project: mirror.project, service });
|
|
10950
|
+
}
|
|
10951
|
+
|
|
10952
|
+
// packages/engine/src/router/local-http-router.ts
|
|
10721
10953
|
var ROUTE_REFRESH_MS = 1000;
|
|
10722
10954
|
var MAX_MIRROR_BYTES = 2 * 1024 * 1024;
|
|
10723
10955
|
var MAX_REQUEST_HEADER_BYTES = 64 * 1024;
|
|
@@ -10800,55 +11032,6 @@ function forwardedHeaders(request, originPort) {
|
|
|
10800
11032
|
headers["x-forwarded-for"] = remote;
|
|
10801
11033
|
return headers;
|
|
10802
11034
|
}
|
|
10803
|
-
async function verifyDockerRouteTarget(target, port) {
|
|
10804
|
-
try {
|
|
10805
|
-
const service = target.service;
|
|
10806
|
-
const args = [
|
|
10807
|
-
"ps",
|
|
10808
|
-
"--no-trunc",
|
|
10809
|
-
"--format",
|
|
10810
|
-
"{{.ID}}\t{{.Ports}}",
|
|
10811
|
-
"--filter",
|
|
10812
|
-
`label=dev.hestia.stack=${target.project}`,
|
|
10813
|
-
"--filter",
|
|
10814
|
-
`label=com.docker.compose.service=${service.name}`
|
|
10815
|
-
];
|
|
10816
|
-
const { stdout } = await pexec6("docker", args, { timeout: 2000 });
|
|
10817
|
-
return stdout.split(`
|
|
10818
|
-
`).some((line) => {
|
|
10819
|
-
const [id, ports = ""] = line.split("\t");
|
|
10820
|
-
const identityMatches = service.containerId === undefined || id?.startsWith(service.containerId);
|
|
10821
|
-
return identityMatches && new RegExp(`(?:127\\.0\\.0\\.1|0\\.0\\.0\\.0):${port}->`).test(ports);
|
|
10822
|
-
});
|
|
10823
|
-
} catch {
|
|
10824
|
-
return false;
|
|
10825
|
-
}
|
|
10826
|
-
}
|
|
10827
|
-
async function verifyLocalRouterTarget(target) {
|
|
10828
|
-
const service = target.service;
|
|
10829
|
-
const port = service?.publishedPort;
|
|
10830
|
-
if (service === undefined || port === undefined || service.backend === "tunnel")
|
|
10831
|
-
return null;
|
|
10832
|
-
if (service.backend === "docker") {
|
|
10833
|
-
return await verifyDockerRouteTarget(target, port) ? port : null;
|
|
10834
|
-
}
|
|
10835
|
-
if (service.pid === undefined || service.startTime === undefined)
|
|
10836
|
-
return null;
|
|
10837
|
-
if (!isLive({ pid: service.pid, startTime: service.startTime }))
|
|
10838
|
-
return null;
|
|
10839
|
-
try {
|
|
10840
|
-
return (await inspectPort(service.pid, port)).ownerIsMember ? port : null;
|
|
10841
|
-
} catch {
|
|
10842
|
-
return null;
|
|
10843
|
-
}
|
|
10844
|
-
}
|
|
10845
|
-
async function verifyStackServiceOrigin(record, service, publishedPort = service.publishedPort) {
|
|
10846
|
-
return await verifyLocalRouterTarget({
|
|
10847
|
-
hostname: "",
|
|
10848
|
-
project: record.project,
|
|
10849
|
-
service: { ...service, publishedPort }
|
|
10850
|
-
}) !== null;
|
|
10851
|
-
}
|
|
10852
11035
|
function targetIdentity(target) {
|
|
10853
11036
|
const service = target.service;
|
|
10854
11037
|
return [
|
|
@@ -10936,27 +11119,27 @@ class HestiaLocalHttpRouter {
|
|
|
10936
11119
|
#sharedRoutes = new Map;
|
|
10937
11120
|
#timer;
|
|
10938
11121
|
async start() {
|
|
10939
|
-
await new Promise((
|
|
11122
|
+
await new Promise((resolve3, reject) => {
|
|
10940
11123
|
this.#server.once("error", reject);
|
|
10941
11124
|
this.#server.listen(0, "127.0.0.1", () => {
|
|
10942
11125
|
this.#server.off("error", reject);
|
|
10943
|
-
|
|
11126
|
+
resolve3();
|
|
10944
11127
|
});
|
|
10945
11128
|
});
|
|
10946
11129
|
prepareGatewaySocket(this.socketPath);
|
|
10947
|
-
await new Promise((
|
|
11130
|
+
await new Promise((resolve3, reject) => {
|
|
10948
11131
|
this.#unixServer.once("error", reject);
|
|
10949
11132
|
this.#unixServer.listen(this.socketPath, () => {
|
|
10950
11133
|
this.#unixServer.off("error", reject);
|
|
10951
11134
|
chmodSync5(this.socketPath, 384);
|
|
10952
|
-
|
|
11135
|
+
resolve3();
|
|
10953
11136
|
});
|
|
10954
11137
|
});
|
|
10955
|
-
await new Promise((
|
|
11138
|
+
await new Promise((resolve3, reject) => {
|
|
10956
11139
|
this.#frontServer.once("error", reject);
|
|
10957
11140
|
this.#frontServer.listen(0, "127.0.0.1", () => {
|
|
10958
11141
|
this.#frontServer.off("error", reject);
|
|
10959
|
-
|
|
11142
|
+
resolve3();
|
|
10960
11143
|
});
|
|
10961
11144
|
});
|
|
10962
11145
|
await this.refreshRoutes();
|
|
@@ -11045,14 +11228,12 @@ class HestiaLocalHttpRouter {
|
|
|
11045
11228
|
let target;
|
|
11046
11229
|
if (holder !== undefined) {
|
|
11047
11230
|
const stack = records.find((record) => record.project === holder.project);
|
|
11048
|
-
const
|
|
11049
|
-
const service = stack?.services.find((candidate2) => candidate2.name === (endpoint?.workload ?? endpoint?.name));
|
|
11050
|
-
const binding = service?.bindings?.find((candidate2) => `${candidate2.target}/${candidate2.protocol}` === endpoint?.binding);
|
|
11051
|
-
const publishedPort = binding?.publishedPort ?? service?.publishedPort;
|
|
11231
|
+
const service = stack === undefined ? undefined : resolveSharedContractOrigin(stack, holder.service);
|
|
11052
11232
|
const candidate = {
|
|
11053
11233
|
hostname,
|
|
11054
11234
|
project: holder.project,
|
|
11055
|
-
|
|
11235
|
+
sharedHolder: holder.project,
|
|
11236
|
+
service
|
|
11056
11237
|
};
|
|
11057
11238
|
const previous = this.#sharedRoutes.get(hostname)?.find((entry) => entry.name === shared.name)?.target;
|
|
11058
11239
|
target = previous !== undefined && targetIdentity(previous) === targetIdentity(candidate) ? previous : candidate;
|
|
@@ -11154,7 +11335,10 @@ Connection: close\r
|
|
|
11154
11335
|
};
|
|
11155
11336
|
}
|
|
11156
11337
|
if (entry.target?.service?.publishedPort === undefined) {
|
|
11157
|
-
return {
|
|
11338
|
+
return {
|
|
11339
|
+
status: 503,
|
|
11340
|
+
message: `Hestia route origin unavailable \u2014 held by ${entry.target?.project ?? "unknown"}`
|
|
11341
|
+
};
|
|
11158
11342
|
}
|
|
11159
11343
|
return { target: entry.target };
|
|
11160
11344
|
}
|
|
@@ -11176,7 +11360,8 @@ Connection: close\r
|
|
|
11176
11360
|
const target = match.target;
|
|
11177
11361
|
const port = target.service.publishedPort;
|
|
11178
11362
|
if (await verifyLocalRouterTarget(target) !== port) {
|
|
11179
|
-
|
|
11363
|
+
const suffix = target.sharedHolder === undefined ? "" : ` \u2014 held by ${target.sharedHolder}`;
|
|
11364
|
+
return sendRouterResponse(response, 503, `Hestia route origin unavailable${suffix}`);
|
|
11180
11365
|
}
|
|
11181
11366
|
const proxy = httpRequest({
|
|
11182
11367
|
hostname: "127.0.0.1",
|
|
@@ -11198,7 +11383,8 @@ Connection: close\r
|
|
|
11198
11383
|
proxy.on("error", (error) => {
|
|
11199
11384
|
if (!response.headersSent) {
|
|
11200
11385
|
const ownershipFailure = error instanceof OriginOwnershipError;
|
|
11201
|
-
|
|
11386
|
+
const suffix = target.sharedHolder === undefined ? "" : ` \u2014 held by ${target.sharedHolder}`;
|
|
11387
|
+
sendRouterResponse(response, ownershipFailure ? 503 : 502, ownershipFailure ? `Hestia route origin unavailable${suffix}` : "Hestia route origin failed");
|
|
11202
11388
|
} else
|
|
11203
11389
|
response.destroy();
|
|
11204
11390
|
});
|
|
@@ -11276,16 +11462,54 @@ Connection: close\r
|
|
|
11276
11462
|
}
|
|
11277
11463
|
}
|
|
11278
11464
|
|
|
11465
|
+
// packages/engine/src/configured-workload-environment.ts
|
|
11466
|
+
import { readFileSync as readFileSync15 } from "fs";
|
|
11467
|
+
import { join as join18 } from "path";
|
|
11468
|
+
var ENDPOINT_TEMPLATE = /\$\{endpoint:([^}]+)\.(host|port|url)\}/g;
|
|
11469
|
+
function endpointTemplateValue(endpoint, field) {
|
|
11470
|
+
const value = endpoint[field];
|
|
11471
|
+
if (value === undefined) {
|
|
11472
|
+
throw new HestiaError("config-invalid", `endpoint template requests ${endpoint.name}.${field}, but that endpoint has no ${field}`);
|
|
11473
|
+
}
|
|
11474
|
+
return String(value);
|
|
11475
|
+
}
|
|
11476
|
+
function resolveEndpointTemplate(template, record) {
|
|
11477
|
+
const resolved = template.replace(ENDPOINT_TEMPLATE, (_match, endpointName, field) => {
|
|
11478
|
+
const endpoint = record.endpoints.find((candidate) => candidate.name === endpointName);
|
|
11479
|
+
if (endpoint === undefined) {
|
|
11480
|
+
throw new HestiaError("config-invalid", `endpoint template references ${JSON.stringify(endpointName)} before that endpoint is available`);
|
|
11481
|
+
}
|
|
11482
|
+
return endpointTemplateValue(endpoint, field);
|
|
11483
|
+
});
|
|
11484
|
+
if (resolved.includes("${endpoint:")) {
|
|
11485
|
+
throw new HestiaError("config-invalid", `malformed endpoint template ${JSON.stringify(template)}`);
|
|
11486
|
+
}
|
|
11487
|
+
return resolved;
|
|
11488
|
+
}
|
|
11489
|
+
function readIgnoredEnvironmentFile(worktreeRoot, file) {
|
|
11490
|
+
try {
|
|
11491
|
+
return readFileSync15(join18(worktreeRoot, file), "utf8").replace(/\r?\n$/, "");
|
|
11492
|
+
} catch (error) {
|
|
11493
|
+
throw new HestiaError("config-invalid", `cannot read configured environment file ${file}: ${error.message}`, { path: join18(worktreeRoot, file) });
|
|
11494
|
+
}
|
|
11495
|
+
}
|
|
11496
|
+
function resolveConfiguredEnvironment(worktreeRoot, configured, record) {
|
|
11497
|
+
return Object.fromEntries(Object.entries(configured).map(([name, value]) => {
|
|
11498
|
+
const raw = typeof value === "string" ? value : readIgnoredEnvironmentFile(worktreeRoot, value.file);
|
|
11499
|
+
return [name, resolveEndpointTemplate(raw, record)];
|
|
11500
|
+
}));
|
|
11501
|
+
}
|
|
11502
|
+
|
|
11279
11503
|
// packages/engine/src/wrangler/adapter.ts
|
|
11280
|
-
import { existsSync as existsSync15, readFileSync as
|
|
11504
|
+
import { existsSync as existsSync15, readFileSync as readFileSync16, statSync as statSync2 } from "fs";
|
|
11281
11505
|
import { homedir as homedir3 } from "os";
|
|
11282
|
-
import { dirname as
|
|
11506
|
+
import { dirname as dirname6, join as join19, relative as relative3 } from "path";
|
|
11283
11507
|
import { connect } from "net";
|
|
11284
11508
|
function privateRegistryDir(worktreeRoot) {
|
|
11285
|
-
return
|
|
11509
|
+
return join19(worktreeRoot, ".hestia", "wrangler-registry");
|
|
11286
11510
|
}
|
|
11287
11511
|
function globalRegistryDir() {
|
|
11288
|
-
return process.env.WRANGLER_REGISTRY_PATH ??
|
|
11512
|
+
return process.env.WRANGLER_REGISTRY_PATH ?? join19(homedir3(), ".wrangler", "config", "registry");
|
|
11289
11513
|
}
|
|
11290
11514
|
var HEARTBEAT_FRESH_MS = 60000;
|
|
11291
11515
|
function tcpConnectable(address, timeoutMs = 500) {
|
|
@@ -11293,15 +11517,15 @@ function tcpConnectable(address, timeoutMs = 500) {
|
|
|
11293
11517
|
const port = Number(portStr);
|
|
11294
11518
|
if (!host || !Number.isFinite(port))
|
|
11295
11519
|
return Promise.resolve(false);
|
|
11296
|
-
return new Promise((
|
|
11520
|
+
return new Promise((resolve3) => {
|
|
11297
11521
|
const sock = connect({ host, port, timeout: timeoutMs });
|
|
11298
11522
|
sock.once("connect", () => {
|
|
11299
11523
|
sock.destroy();
|
|
11300
|
-
|
|
11524
|
+
resolve3(true);
|
|
11301
11525
|
});
|
|
11302
11526
|
const dead = () => {
|
|
11303
11527
|
sock.destroy();
|
|
11304
|
-
|
|
11528
|
+
resolve3(false);
|
|
11305
11529
|
};
|
|
11306
11530
|
sock.once("error", dead);
|
|
11307
11531
|
sock.once("timeout", dead);
|
|
@@ -11314,14 +11538,14 @@ async function preflightForeignSessions(workers) {
|
|
|
11314
11538
|
for (const w of workers) {
|
|
11315
11539
|
if (w.name === null)
|
|
11316
11540
|
continue;
|
|
11317
|
-
const entryPath =
|
|
11541
|
+
const entryPath = join19(dir, w.name);
|
|
11318
11542
|
if (!existsSync15(entryPath))
|
|
11319
11543
|
continue;
|
|
11320
11544
|
const freshMs = Date.now() - statSync2(entryPath).mtimeMs;
|
|
11321
11545
|
let live = freshMs < HEARTBEAT_FRESH_MS;
|
|
11322
11546
|
if (!live) {
|
|
11323
11547
|
try {
|
|
11324
|
-
const entry = JSON.parse(
|
|
11548
|
+
const entry = JSON.parse(readFileSync16(entryPath, "utf8"));
|
|
11325
11549
|
live = entry.debugPortAddress ? await tcpConnectable(entry.debugPortAddress) : false;
|
|
11326
11550
|
} catch {
|
|
11327
11551
|
live = false;
|
|
@@ -11342,14 +11566,14 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
11342
11566
|
throw new HestiaError("no-workers-found", opts.filter.length > 0 ? `no wrangler configs match --workers=${opts.filter.join(",")}` : `no wrangler.{jsonc,json,toml} found in ${worktreeRoot}`);
|
|
11343
11567
|
}
|
|
11344
11568
|
const wranglerBinFor = (configPath) => {
|
|
11345
|
-
let dir =
|
|
11569
|
+
let dir = dirname6(configPath);
|
|
11346
11570
|
for (;; ) {
|
|
11347
|
-
const candidate =
|
|
11571
|
+
const candidate = join19(dir, "node_modules", ".bin", "wrangler");
|
|
11348
11572
|
if (existsSync15(candidate))
|
|
11349
11573
|
return candidate;
|
|
11350
11574
|
if (dir === worktreeRoot)
|
|
11351
11575
|
return null;
|
|
11352
|
-
const parent =
|
|
11576
|
+
const parent = dirname6(dir);
|
|
11353
11577
|
if (parent === dir)
|
|
11354
11578
|
return null;
|
|
11355
11579
|
dir = parent;
|
|
@@ -11381,6 +11605,7 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
11381
11605
|
throw new HestiaError("wrangler-missing", `no local wrangler for ${relative3(worktreeRoot, w.configPath)} ` + `(searched node_modules/.bin from the config dir up to the worktree ` + `root) \u2014 hestia never uses a global install`);
|
|
11382
11606
|
}
|
|
11383
11607
|
const inspectorPort = await allocatePort();
|
|
11608
|
+
const workerCwd = dirname6(w.configPath);
|
|
11384
11609
|
specs.push({
|
|
11385
11610
|
name: w.name,
|
|
11386
11611
|
argv: [
|
|
@@ -11398,9 +11623,10 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
11398
11623
|
WRANGLER_REGISTRY_PATH: registry,
|
|
11399
11624
|
MINIFLARE_REGISTRY_PATH: registry
|
|
11400
11625
|
},
|
|
11626
|
+
cwd: relative3(worktreeRoot, workerCwd),
|
|
11401
11627
|
port: "auto",
|
|
11402
11628
|
signal: "int",
|
|
11403
|
-
varlock: opts.varlock,
|
|
11629
|
+
varlock: opts.varlock && detectVarlock(worktreeRoot, workerCwd) !== null,
|
|
11404
11630
|
backend: "wrangler",
|
|
11405
11631
|
inspectorPort,
|
|
11406
11632
|
configPath: w.configPath
|
|
@@ -11441,14 +11667,14 @@ import { execFile as execFile8 } from "child_process";
|
|
|
11441
11667
|
import { promisify as promisify7 } from "util";
|
|
11442
11668
|
import { existsSync as existsSync17 } from "fs";
|
|
11443
11669
|
import { homedir as homedir4 } from "os";
|
|
11444
|
-
import { join as
|
|
11670
|
+
import { join as join20 } from "path";
|
|
11445
11671
|
var pexec7 = promisify7(execFile8);
|
|
11446
11672
|
var BIN = "cloudflared";
|
|
11447
11673
|
function cloudflaredHome() {
|
|
11448
|
-
return process.env.HESTIA_CLOUDFLARED_HOME ??
|
|
11674
|
+
return process.env.HESTIA_CLOUDFLARED_HOME ?? join20(homedir4(), ".cloudflared");
|
|
11449
11675
|
}
|
|
11450
11676
|
function credFilePath(uuid) {
|
|
11451
|
-
return process.env.TUNNEL_CRED_FILE ??
|
|
11677
|
+
return process.env.TUNNEL_CRED_FILE ?? join20(cloudflaredHome(), `${uuid}.json`);
|
|
11452
11678
|
}
|
|
11453
11679
|
async function run(args, timeoutMs = 30000) {
|
|
11454
11680
|
try {
|
|
@@ -11485,8 +11711,8 @@ async function adoptTunnel(name) {
|
|
|
11485
11711
|
|
|
11486
11712
|
// packages/engine/src/tunnel/ingress.ts
|
|
11487
11713
|
import { createHash as createHash6 } from "crypto";
|
|
11488
|
-
import { existsSync as existsSync18, readFileSync as
|
|
11489
|
-
import { join as
|
|
11714
|
+
import { existsSync as existsSync18, readFileSync as readFileSync17 } from "fs";
|
|
11715
|
+
import { join as join21 } from "path";
|
|
11490
11716
|
var LABEL_MAX = 63;
|
|
11491
11717
|
var HASH_LEN = 6;
|
|
11492
11718
|
function hostnameFor(tunnelName, branch, service, zone) {
|
|
@@ -11506,12 +11732,12 @@ function inferZone(baseRules) {
|
|
|
11506
11732
|
return zones.size === 1 ? [...zones][0] : undefined;
|
|
11507
11733
|
}
|
|
11508
11734
|
function importBaseRules(uuid, name) {
|
|
11509
|
-
const p =
|
|
11735
|
+
const p = join21(cloudflaredHome(), "config.yml");
|
|
11510
11736
|
if (!existsSync18(p))
|
|
11511
11737
|
return [];
|
|
11512
11738
|
let parsed;
|
|
11513
11739
|
try {
|
|
11514
|
-
parsed = $parse(
|
|
11740
|
+
parsed = $parse(readFileSync17(p, "utf8"));
|
|
11515
11741
|
} catch {
|
|
11516
11742
|
return [];
|
|
11517
11743
|
}
|
|
@@ -11576,16 +11802,16 @@ function assertDisjoint(base, dynamic, shared) {
|
|
|
11576
11802
|
import {
|
|
11577
11803
|
existsSync as existsSync19,
|
|
11578
11804
|
mkdirSync as mkdirSync8,
|
|
11579
|
-
readFileSync as
|
|
11805
|
+
readFileSync as readFileSync18,
|
|
11580
11806
|
readdirSync as readdirSync7
|
|
11581
11807
|
} from "fs";
|
|
11582
|
-
import { join as
|
|
11808
|
+
import { join as join23 } from "path";
|
|
11583
11809
|
|
|
11584
11810
|
// packages/engine/src/tunnel/orphans.ts
|
|
11585
11811
|
import { execFileSync as execFileSync4 } from "child_process";
|
|
11586
|
-
import { join as
|
|
11812
|
+
import { join as join22 } from "path";
|
|
11587
11813
|
function hestiaTunnelMarker(uuid) {
|
|
11588
|
-
return
|
|
11814
|
+
return join22(hestiaHome(), "tunnel", uuid);
|
|
11589
11815
|
}
|
|
11590
11816
|
function parseLocalHestiaConnectors(psOutput, marker) {
|
|
11591
11817
|
const rows = [];
|
|
@@ -11706,13 +11932,13 @@ async function quickTunnelUrl(metricsPort, timeoutMs) {
|
|
|
11706
11932
|
var CONNECTOR = "connector";
|
|
11707
11933
|
var READY_TIMEOUT_MS2 = 30000;
|
|
11708
11934
|
function tunnelDir(uuid) {
|
|
11709
|
-
return
|
|
11935
|
+
return join23(hestiaHome(), "tunnel", uuid);
|
|
11710
11936
|
}
|
|
11711
11937
|
function configPath(uuid) {
|
|
11712
|
-
return
|
|
11938
|
+
return join23(tunnelDir(uuid), "config.yml");
|
|
11713
11939
|
}
|
|
11714
11940
|
function adoptedMarker(uuid) {
|
|
11715
|
-
return
|
|
11941
|
+
return join23(tunnelDir(uuid), "adopted.json");
|
|
11716
11942
|
}
|
|
11717
11943
|
function isAdopted(uuid) {
|
|
11718
11944
|
return existsSync19(adoptedMarker(uuid));
|
|
@@ -11723,7 +11949,7 @@ function readAdopted(uuid) {
|
|
|
11723
11949
|
return null;
|
|
11724
11950
|
let marker;
|
|
11725
11951
|
try {
|
|
11726
|
-
marker = JSON.parse(
|
|
11952
|
+
marker = JSON.parse(readFileSync18(p, "utf8"));
|
|
11727
11953
|
} catch {
|
|
11728
11954
|
marker = {};
|
|
11729
11955
|
}
|
|
@@ -11731,14 +11957,14 @@ function readAdopted(uuid) {
|
|
|
11731
11957
|
return { uuid, name: marker.name, credFile: marker.credFile, reconstructed: false };
|
|
11732
11958
|
}
|
|
11733
11959
|
let name;
|
|
11734
|
-
const stacksDir =
|
|
11960
|
+
const stacksDir = join23(hestiaHome(), "stacks");
|
|
11735
11961
|
if (existsSync19(stacksDir)) {
|
|
11736
11962
|
for (const project of readdirSync7(stacksDir)) {
|
|
11737
|
-
const sp =
|
|
11963
|
+
const sp = join23(stacksDir, project, "stack.json");
|
|
11738
11964
|
if (!existsSync19(sp))
|
|
11739
11965
|
continue;
|
|
11740
11966
|
try {
|
|
11741
|
-
const record = parseStackRecord(
|
|
11967
|
+
const record = parseStackRecord(readFileSync18(sp, "utf8"), sp);
|
|
11742
11968
|
if (record.tunnel?.uuid === uuid) {
|
|
11743
11969
|
name = record.tunnel.name;
|
|
11744
11970
|
break;
|
|
@@ -11754,24 +11980,24 @@ function readAdopted(uuid) {
|
|
|
11754
11980
|
};
|
|
11755
11981
|
}
|
|
11756
11982
|
function listAdopted() {
|
|
11757
|
-
const root =
|
|
11983
|
+
const root = join23(hestiaHome(), "tunnel");
|
|
11758
11984
|
if (!existsSync19(root))
|
|
11759
11985
|
return [];
|
|
11760
11986
|
return readdirSync7(root).filter((uuid) => isAdopted(uuid));
|
|
11761
11987
|
}
|
|
11762
11988
|
function collectDynamicRules(uuid) {
|
|
11763
|
-
const stacksDir =
|
|
11989
|
+
const stacksDir = join23(hestiaHome(), "stacks");
|
|
11764
11990
|
const rules = [];
|
|
11765
11991
|
const dropped = [];
|
|
11766
11992
|
if (!existsSync19(stacksDir))
|
|
11767
11993
|
return { rules, dropped };
|
|
11768
11994
|
for (const project of readdirSync7(stacksDir)) {
|
|
11769
|
-
const p =
|
|
11995
|
+
const p = join23(stacksDir, project, "stack.json");
|
|
11770
11996
|
if (!existsSync19(p))
|
|
11771
11997
|
continue;
|
|
11772
11998
|
let record;
|
|
11773
11999
|
try {
|
|
11774
|
-
record = parseStackRecord(
|
|
12000
|
+
record = parseStackRecord(readFileSync18(p, "utf8"), p);
|
|
11775
12001
|
} catch {
|
|
11776
12002
|
continue;
|
|
11777
12003
|
}
|
|
@@ -11815,7 +12041,7 @@ async function reconcileTunnel(ref, opts) {
|
|
|
11815
12041
|
if (reapedGroups > 0) {
|
|
11816
12042
|
warnings.push(`reaped ${reapedGroups} orphan hestia connector process group(s) for this tunnel`);
|
|
11817
12043
|
}
|
|
11818
|
-
const currentConfig = existsSync19(cfgPath) ?
|
|
12044
|
+
const currentConfig = existsSync19(cfgPath) ? readFileSync18(cfgPath, "utf8") : null;
|
|
11819
12045
|
if (live && currentConfig === nextConfig) {
|
|
11820
12046
|
return { restarted: false, metricsPort: pf.port, ready: true, warnings };
|
|
11821
12047
|
}
|
|
@@ -11968,7 +12194,7 @@ async function* composeLogLines(project, service, options = {}) {
|
|
|
11968
12194
|
args.push("--follow");
|
|
11969
12195
|
args.push(service);
|
|
11970
12196
|
const child = spawn3("docker", args, { stdio: ["ignore", "pipe", "pipe"] });
|
|
11971
|
-
const completion = new Promise((
|
|
12197
|
+
const completion = new Promise((resolve3) => child.once("close", resolve3));
|
|
11972
12198
|
let stderr = "";
|
|
11973
12199
|
let spawnErrorMessage;
|
|
11974
12200
|
child.stderr.setEncoding("utf8");
|
|
@@ -12059,12 +12285,12 @@ function readLastLineEventsBeforeOffset(path, count, endOffset) {
|
|
|
12059
12285
|
function waitForLogPoll(signal) {
|
|
12060
12286
|
if (signal?.aborted)
|
|
12061
12287
|
return Promise.resolve();
|
|
12062
|
-
return new Promise((
|
|
12288
|
+
return new Promise((resolve3) => {
|
|
12063
12289
|
const timer = setTimeout(done, LOG_POLL_MS);
|
|
12064
12290
|
function done() {
|
|
12065
12291
|
clearTimeout(timer);
|
|
12066
12292
|
signal?.removeEventListener("abort", done);
|
|
12067
|
-
|
|
12293
|
+
resolve3();
|
|
12068
12294
|
}
|
|
12069
12295
|
signal?.addEventListener("abort", done, { once: true });
|
|
12070
12296
|
});
|
|
@@ -12167,10 +12393,10 @@ class BoundedLogMergeQueue {
|
|
|
12167
12393
|
}
|
|
12168
12394
|
async push(item, signal) {
|
|
12169
12395
|
while (!this.#closed && !signal?.aborted && this.#items.length >= this.capacity) {
|
|
12170
|
-
await new Promise((
|
|
12396
|
+
await new Promise((resolve3) => {
|
|
12171
12397
|
const done = () => {
|
|
12172
12398
|
signal?.removeEventListener("abort", done);
|
|
12173
|
-
|
|
12399
|
+
resolve3();
|
|
12174
12400
|
};
|
|
12175
12401
|
this.#capacityWaiters.push(done);
|
|
12176
12402
|
signal?.addEventListener("abort", done, { once: true });
|
|
@@ -12193,7 +12419,7 @@ class BoundedLogMergeQueue {
|
|
|
12193
12419
|
}
|
|
12194
12420
|
if (this.#closed)
|
|
12195
12421
|
return Promise.resolve({ done: true });
|
|
12196
|
-
return new Promise((
|
|
12422
|
+
return new Promise((resolve3) => this.#waiters.push(resolve3));
|
|
12197
12423
|
}
|
|
12198
12424
|
close() {
|
|
12199
12425
|
if (this.#closed)
|
|
@@ -12305,12 +12531,47 @@ async function* streamStackLogs(record, options = {}) {
|
|
|
12305
12531
|
}
|
|
12306
12532
|
}
|
|
12307
12533
|
// packages/engine/src/daemon/shared-arbiter.ts
|
|
12534
|
+
var ORIGIN_DEAD_RELEASE_STRIKES = 3;
|
|
12535
|
+
function probeMirrorProcessOccupancy(mirror) {
|
|
12536
|
+
const identities = [];
|
|
12537
|
+
let malformed = false;
|
|
12538
|
+
for (const service of [...mirror.services, ...mirror.auxiliary ?? []].filter((candidate) => candidate.backend === "proc" || candidate.backend === "wrangler")) {
|
|
12539
|
+
if (service.pid === undefined && service.startTime === undefined)
|
|
12540
|
+
continue;
|
|
12541
|
+
if (!Number.isSafeInteger(service.pid) || service.pid <= 0 || typeof service.startTime !== "string") {
|
|
12542
|
+
malformed = true;
|
|
12543
|
+
continue;
|
|
12544
|
+
}
|
|
12545
|
+
identities.push({ pid: service.pid, startTime: service.startTime });
|
|
12546
|
+
}
|
|
12547
|
+
const pidfileScan = scanPidfiles(mirrorProcsDir(mirror.project));
|
|
12548
|
+
malformed ||= pidfileScan.errors.length > 0;
|
|
12549
|
+
identities.push(...pidfileScan.pidfiles.filter((pidfile) => pidfile.backend !== "tunnel").map((pidfile) => ({ pid: pidfile.pid, startTime: pidfile.startTime })));
|
|
12550
|
+
let unknown = false;
|
|
12551
|
+
for (const identityRecord of identities) {
|
|
12552
|
+
const identity2 = probeProcessIdentity(identityRecord);
|
|
12553
|
+
if (identity2 === "live")
|
|
12554
|
+
return "live";
|
|
12555
|
+
if (identity2 === "unknown")
|
|
12556
|
+
unknown = true;
|
|
12557
|
+
}
|
|
12558
|
+
if (unknown)
|
|
12559
|
+
return "unknown";
|
|
12560
|
+
if (malformed)
|
|
12561
|
+
throw new Error("Shared holder mirror has an invalid process identity");
|
|
12562
|
+
return "dead";
|
|
12563
|
+
}
|
|
12564
|
+
|
|
12308
12565
|
class SharedArbiter {
|
|
12309
|
-
onChange;
|
|
12310
12566
|
#mutex = Promise.resolve();
|
|
12311
12567
|
#polls = new Map;
|
|
12312
|
-
|
|
12313
|
-
|
|
12568
|
+
#deadOriginStrikes = new Map;
|
|
12569
|
+
#onChange;
|
|
12570
|
+
#probeHolderOrigin;
|
|
12571
|
+
constructor(onChangeOrOptions) {
|
|
12572
|
+
const options = typeof onChangeOrOptions === "function" ? { onChange: onChangeOrOptions } : onChangeOrOptions;
|
|
12573
|
+
this.#onChange = options?.onChange;
|
|
12574
|
+
this.#probeHolderOrigin = options?.probeHolderOrigin ?? probeSharedHolderOrigin;
|
|
12314
12575
|
}
|
|
12315
12576
|
#locked(fn) {
|
|
12316
12577
|
const next = this.#mutex.then(fn, fn);
|
|
@@ -12319,7 +12580,7 @@ class SharedArbiter {
|
|
|
12319
12580
|
}
|
|
12320
12581
|
async#notify() {
|
|
12321
12582
|
try {
|
|
12322
|
-
await this
|
|
12583
|
+
await this.#onChange?.();
|
|
12323
12584
|
} catch {}
|
|
12324
12585
|
}
|
|
12325
12586
|
#result(record, granted) {
|
|
@@ -12342,7 +12603,8 @@ class SharedArbiter {
|
|
|
12342
12603
|
delete next.holder;
|
|
12343
12604
|
return next;
|
|
12344
12605
|
}
|
|
12345
|
-
|
|
12606
|
+
const mirror = readMirrorStateSafe(head.project);
|
|
12607
|
+
if (mirror.status === "ok" && mirror.record === null)
|
|
12346
12608
|
continue;
|
|
12347
12609
|
return {
|
|
12348
12610
|
...record,
|
|
@@ -12400,11 +12662,11 @@ class SharedArbiter {
|
|
|
12400
12662
|
}
|
|
12401
12663
|
if (waitMs <= 0)
|
|
12402
12664
|
return this.#result(outcome.record, false);
|
|
12403
|
-
return new Promise((
|
|
12665
|
+
return new Promise((resolve3) => {
|
|
12404
12666
|
const key = `${name}\x00${requester.project}`;
|
|
12405
12667
|
const polls = this.#polls.get(key) ?? [];
|
|
12406
12668
|
const poll = {
|
|
12407
|
-
resolve:
|
|
12669
|
+
resolve: resolve3,
|
|
12408
12670
|
timer: setTimeout(() => {
|
|
12409
12671
|
const remaining = (this.#polls.get(key) ?? []).filter((candidate) => candidate !== poll);
|
|
12410
12672
|
if (remaining.length === 0)
|
|
@@ -12412,7 +12674,7 @@ class SharedArbiter {
|
|
|
12412
12674
|
else
|
|
12413
12675
|
this.#polls.set(key, remaining);
|
|
12414
12676
|
const record = readSharedHostname(name);
|
|
12415
|
-
|
|
12677
|
+
resolve3(record === null ? { granted: false, queued: [] } : this.#result(record, record.holder?.project === requester.project));
|
|
12416
12678
|
}, waitMs)
|
|
12417
12679
|
};
|
|
12418
12680
|
polls.push(poll);
|
|
@@ -12494,35 +12756,135 @@ class SharedArbiter {
|
|
|
12494
12756
|
}
|
|
12495
12757
|
async sweep(occupied) {
|
|
12496
12758
|
let changed = false;
|
|
12759
|
+
const releases = [];
|
|
12497
12760
|
for (const record of listSharedHostnames()) {
|
|
12498
|
-
const
|
|
12499
|
-
const
|
|
12500
|
-
|
|
12761
|
+
const holder = record.holder;
|
|
12762
|
+
const holderMirror = holder === undefined ? undefined : readMirrorStateSafe(holder.project);
|
|
12763
|
+
const deadWaiterProjects = new Set((record.queue ?? []).flatMap((waiter) => {
|
|
12764
|
+
const mirror = readMirrorStateSafe(waiter.project);
|
|
12765
|
+
return mirror.status === "ok" && mirror.record === null ? [waiter.project] : [];
|
|
12766
|
+
}));
|
|
12767
|
+
const deadWaiters = (record.queue ?? []).filter((waiter) => deadWaiterProjects.has(waiter.project));
|
|
12768
|
+
let releaseReason;
|
|
12769
|
+
if (holder !== undefined && holderMirror !== undefined) {
|
|
12770
|
+
const holderKey = `${holder.project}\x00${holder.at}`;
|
|
12771
|
+
const holderOccupied = occupied.has(holder.project);
|
|
12772
|
+
if (holderMirror.status === "error") {
|
|
12773
|
+
if (this.#recordDeadStrike(record.name, holderKey))
|
|
12774
|
+
releaseReason = "stack-dead";
|
|
12775
|
+
} else {
|
|
12776
|
+
try {
|
|
12777
|
+
const mirror = holderMirror.record;
|
|
12778
|
+
if (mirror === null) {
|
|
12779
|
+
if (!holderOccupied)
|
|
12780
|
+
releaseReason = "stack-gone";
|
|
12781
|
+
else
|
|
12782
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12783
|
+
} else {
|
|
12784
|
+
const protectedState = mirror.state === "starting" || mirror.state === "queued";
|
|
12785
|
+
if (!protectedState && mirror.starter !== undefined && (typeof mirror.starter !== "object" || mirror.starter === null || !Number.isSafeInteger(mirror.starter.pid) || mirror.starter.pid <= 0 || typeof mirror.starter.startTime !== "string")) {
|
|
12786
|
+
throw new Error("Shared holder mirror has an invalid starter identity");
|
|
12787
|
+
}
|
|
12788
|
+
const starterLiveness = protectedState || mirror.starter === undefined ? "dead" : probeProcessIdentity(mirror.starter);
|
|
12789
|
+
const protectedStartup = protectedState || starterLiveness === "live";
|
|
12790
|
+
if (protectedStartup) {
|
|
12791
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12792
|
+
} else if (starterLiveness === "unknown") {
|
|
12793
|
+
this.#retainStrikeForHolder(record.name, holderKey);
|
|
12794
|
+
} else if (!holderOccupied) {
|
|
12795
|
+
const contractOrigin = resolveSharedContractOrigin(mirror, holder.service);
|
|
12796
|
+
if (contractOrigin === undefined) {
|
|
12797
|
+
const processOccupancy = probeMirrorProcessOccupancy(mirror);
|
|
12798
|
+
if (processOccupancy === "live") {
|
|
12799
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12800
|
+
} else if (processOccupancy === "unknown") {
|
|
12801
|
+
this.#retainStrikeForHolder(record.name, holderKey);
|
|
12802
|
+
} else if (this.#recordDeadStrike(record.name, holderKey)) {
|
|
12803
|
+
releaseReason = "stack-dead";
|
|
12804
|
+
}
|
|
12805
|
+
} else {
|
|
12806
|
+
const origin = await this.#probeHolderOrigin(mirror, holder.service);
|
|
12807
|
+
if (origin === "live") {
|
|
12808
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12809
|
+
} else if (origin === "dead") {
|
|
12810
|
+
if (this.#recordDeadStrike(record.name, holderKey))
|
|
12811
|
+
releaseReason = "stack-dead";
|
|
12812
|
+
} else {
|
|
12813
|
+
this.#retainStrikeForHolder(record.name, holderKey);
|
|
12814
|
+
}
|
|
12815
|
+
}
|
|
12816
|
+
} else {
|
|
12817
|
+
const origin = await this.#probeHolderOrigin(mirror, holder.service);
|
|
12818
|
+
if (origin === "live") {
|
|
12819
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12820
|
+
} else if (origin === "dead") {
|
|
12821
|
+
if (this.#recordDeadStrike(record.name, holderKey))
|
|
12822
|
+
releaseReason = "origin-dead";
|
|
12823
|
+
} else {
|
|
12824
|
+
this.#retainStrikeForHolder(record.name, holderKey);
|
|
12825
|
+
}
|
|
12826
|
+
}
|
|
12827
|
+
}
|
|
12828
|
+
} catch {
|
|
12829
|
+
if (this.#recordDeadStrike(record.name, holderKey))
|
|
12830
|
+
releaseReason = "stack-dead";
|
|
12831
|
+
}
|
|
12832
|
+
}
|
|
12833
|
+
} else {
|
|
12834
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12835
|
+
}
|
|
12836
|
+
if (releaseReason === undefined && deadWaiters.length === 0)
|
|
12501
12837
|
continue;
|
|
12838
|
+
let released = false;
|
|
12502
12839
|
const updated = await this.#locked(() => updateSharedHostname(record.name, (candidate) => {
|
|
12503
12840
|
const pruned = {
|
|
12504
12841
|
...candidate,
|
|
12505
|
-
queue: (candidate.queue ?? []).filter((waiter) =>
|
|
12842
|
+
queue: (candidate.queue ?? []).filter((waiter) => {
|
|
12843
|
+
if (!deadWaiterProjects.has(waiter.project))
|
|
12844
|
+
return true;
|
|
12845
|
+
const freshMirror = readMirrorStateSafe(waiter.project);
|
|
12846
|
+
return freshMirror.status === "error" || freshMirror.record !== null;
|
|
12847
|
+
})
|
|
12506
12848
|
};
|
|
12507
|
-
const
|
|
12508
|
-
|
|
12849
|
+
const holderMatches = releaseReason !== undefined && pruned.holder?.project === holder?.project && pruned.holder?.at === holder?.at;
|
|
12850
|
+
if (!holderMatches)
|
|
12851
|
+
return pruned;
|
|
12852
|
+
released = true;
|
|
12853
|
+
return this.#grantNext(pruned);
|
|
12509
12854
|
}));
|
|
12510
12855
|
if (updated === null)
|
|
12511
12856
|
continue;
|
|
12512
|
-
changed
|
|
12857
|
+
changed ||= released || deadWaiters.length > 0;
|
|
12858
|
+
if (released && holder !== undefined && releaseReason !== undefined) {
|
|
12859
|
+
releases.push({ name: record.name, project: holder.project, reason: releaseReason });
|
|
12860
|
+
this.#deadOriginStrikes.delete(record.name);
|
|
12861
|
+
}
|
|
12513
12862
|
if (updated.holder !== undefined && updated.holder.project !== record.holder?.project) {
|
|
12514
12863
|
this.#wake(updated.name, updated.holder.project, this.#result(updated, true));
|
|
12515
12864
|
}
|
|
12516
12865
|
}
|
|
12517
12866
|
if (changed)
|
|
12518
12867
|
await this.#notify();
|
|
12868
|
+
return releases;
|
|
12869
|
+
}
|
|
12870
|
+
#recordDeadStrike(name, holderKey) {
|
|
12871
|
+
const previous = this.#deadOriginStrikes.get(name);
|
|
12872
|
+
const strikes = previous?.holderKey === holderKey ? previous.strikes + 1 : 1;
|
|
12873
|
+
this.#deadOriginStrikes.set(name, { holderKey, strikes });
|
|
12874
|
+
return strikes >= ORIGIN_DEAD_RELEASE_STRIKES;
|
|
12875
|
+
}
|
|
12876
|
+
#retainStrikeForHolder(name, holderKey) {
|
|
12877
|
+
const previous = this.#deadOriginStrikes.get(name);
|
|
12878
|
+
if (previous?.holderKey !== holderKey) {
|
|
12879
|
+
this.#deadOriginStrikes.set(name, { holderKey, strikes: 0 });
|
|
12880
|
+
}
|
|
12519
12881
|
}
|
|
12520
12882
|
}
|
|
12521
12883
|
// packages/engine/src/daemon/fleet-monitor.ts
|
|
12522
12884
|
import { execFile as execFile9 } from "child_process";
|
|
12523
12885
|
import { promisify as promisify8 } from "util";
|
|
12524
|
-
import { existsSync as existsSync20, readFileSync as
|
|
12525
|
-
import { join as
|
|
12886
|
+
import { existsSync as existsSync20, readFileSync as readFileSync19, readdirSync as readdirSync8 } from "fs";
|
|
12887
|
+
import { join as join24 } from "path";
|
|
12526
12888
|
var pexec8 = promisify8(execFile9);
|
|
12527
12889
|
var DEFAULT_REFRESH_MS = 1000;
|
|
12528
12890
|
var DEFAULT_HEARTBEAT_MS = 15000;
|
|
@@ -12553,8 +12915,8 @@ class LatestFleetChannel {
|
|
|
12553
12915
|
}
|
|
12554
12916
|
if (this.#closed)
|
|
12555
12917
|
return Promise.resolve({ value: undefined, done: true });
|
|
12556
|
-
return new Promise((
|
|
12557
|
-
this.#waiter =
|
|
12918
|
+
return new Promise((resolve3) => {
|
|
12919
|
+
this.#waiter = resolve3;
|
|
12558
12920
|
});
|
|
12559
12921
|
}
|
|
12560
12922
|
close() {
|
|
@@ -12569,13 +12931,13 @@ class LatestFleetChannel {
|
|
|
12569
12931
|
function readManagedMirrors() {
|
|
12570
12932
|
const records = [];
|
|
12571
12933
|
const warnings = [];
|
|
12572
|
-
const stacksDirectory =
|
|
12934
|
+
const stacksDirectory = join24(hestiaHome(), "stacks");
|
|
12573
12935
|
if (!existsSync20(stacksDirectory))
|
|
12574
12936
|
return { records, warnings };
|
|
12575
12937
|
for (const project of readdirSync8(stacksDirectory).sort()) {
|
|
12576
|
-
const path =
|
|
12938
|
+
const path = join24(stacksDirectory, project, "stack.json");
|
|
12577
12939
|
try {
|
|
12578
|
-
const source =
|
|
12940
|
+
const source = readFileSync19(path);
|
|
12579
12941
|
if (source.byteLength > MAX_MIRROR_BYTES2) {
|
|
12580
12942
|
warnings.push(`Fleet mirror too large for ${project}`);
|
|
12581
12943
|
continue;
|
|
@@ -13051,7 +13413,7 @@ async function assertHestiaStateIgnored(worktreeRoot) {
|
|
|
13051
13413
|
try {
|
|
13052
13414
|
await pexec9("git", ["-C", worktreeRoot, "check-ignore", "-q", ".hestia/"], { timeout: 5000 });
|
|
13053
13415
|
} catch {
|
|
13054
|
-
throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${
|
|
13416
|
+
throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${join25(worktreeRoot, ".gitignore")}`, { remedy: `.hestia/`, path: join25(worktreeRoot, ".gitignore") });
|
|
13055
13417
|
}
|
|
13056
13418
|
}
|
|
13057
13419
|
async function prepareCompose(worktreeRoot, project, repo, branch, opts, configuredBaseFile) {
|
|
@@ -13068,7 +13430,7 @@ async function prepareCompose(worktreeRoot, project, repo, branch, opts, configu
|
|
|
13068
13430
|
services
|
|
13069
13431
|
});
|
|
13070
13432
|
ensureDir(hestiaDir(worktreeRoot));
|
|
13071
|
-
const overridePath =
|
|
13433
|
+
const overridePath = join25(hestiaDir(worktreeRoot), OVERRIDE_FILE);
|
|
13072
13434
|
writeAtomicTextFile(overridePath, yaml);
|
|
13073
13435
|
return {
|
|
13074
13436
|
ctx: {
|
|
@@ -13101,7 +13463,7 @@ function writeDockerfileComposeModel(worktreeRoot, workloads, conventionalCompos
|
|
|
13101
13463
|
...conventionalComposeFile === undefined ? {} : { include: [{ path: conventionalComposeFile }] },
|
|
13102
13464
|
services
|
|
13103
13465
|
};
|
|
13104
|
-
const path =
|
|
13466
|
+
const path = join25(hestiaDir(worktreeRoot), DOCKERFILE_COMPOSE_FILE);
|
|
13105
13467
|
writeAtomicTextFile(path, $stringify(model));
|
|
13106
13468
|
return path;
|
|
13107
13469
|
}
|
|
@@ -13121,9 +13483,9 @@ function freshRecord(project, repoId, repo, branch, worktree) {
|
|
|
13121
13483
|
};
|
|
13122
13484
|
}
|
|
13123
13485
|
function assertCurrentStackIdentity(record, current) {
|
|
13124
|
-
const path =
|
|
13486
|
+
const path = join25(hestiaDir(current.worktreeRoot), "stack.json");
|
|
13125
13487
|
assertMutableStackRecord(record, path);
|
|
13126
|
-
const matches = record.repoId === current.repoId && record.repo === current.repo && record.branch === current.branch &&
|
|
13488
|
+
const matches = record.repoId === current.repoId && record.repo === current.repo && record.branch === current.branch && resolve3(record.worktree) === resolve3(current.worktreeRoot) && record.project === projectName(current.repoId, current.repo, current.branch, current.worktreeRoot);
|
|
13127
13489
|
if (!matches) {
|
|
13128
13490
|
throw new HestiaError("stack-identity-changed", `this checkout no longer matches stack ${record.project}; run hestia down before starting or changing it`, {
|
|
13129
13491
|
recorded: {
|
|
@@ -13301,7 +13663,7 @@ function matchesExpectedStack(record, expected) {
|
|
|
13301
13663
|
return record !== null && (record.repoId === undefined || record.repoId === expected.repoId) && record.worktree === expected.worktree && record.createdAt === expected.createdAt;
|
|
13302
13664
|
}
|
|
13303
13665
|
function projectMutationRoot(project) {
|
|
13304
|
-
return
|
|
13666
|
+
return join25(hestiaHome(), "project-locks", project);
|
|
13305
13667
|
}
|
|
13306
13668
|
async function assertNamedTunnelDns(hostname, tunnelUuid) {
|
|
13307
13669
|
const expected = `${tunnelUuid}.cfargotunnel.com`;
|
|
@@ -13558,12 +13920,25 @@ class ComposeEngine {
|
|
|
13558
13920
|
}
|
|
13559
13921
|
}
|
|
13560
13922
|
}
|
|
13923
|
+
applyConfiguredEndpoints(record, configuredWorkloads);
|
|
13924
|
+
if (opts?.workers || configuredWorkers.length > 0) {
|
|
13925
|
+
const configuredFilters = configuredWorkers.map(([name, workload]) => workload.wranglerConfig ?? name);
|
|
13926
|
+
const explicitFilters = Array.isArray(opts?.workers) ? opts.workers : [];
|
|
13927
|
+
await this.#upWorkers(worktreeRoot, record, configuredWorkloads, {
|
|
13928
|
+
...opts,
|
|
13929
|
+
workers: opts?.workers === true ? true : [...new Set([...explicitFilters, ...configuredFilters])]
|
|
13930
|
+
});
|
|
13931
|
+
}
|
|
13932
|
+
applyConfiguredEndpoints(record, configuredWorkloads);
|
|
13561
13933
|
for (const [name, workload] of configuredProcs) {
|
|
13562
|
-
const command = workload.command;
|
|
13563
13934
|
const configuredSpec = {
|
|
13564
13935
|
name,
|
|
13565
|
-
argv: command,
|
|
13936
|
+
argv: workload.command,
|
|
13937
|
+
cwd: workload.cwd,
|
|
13938
|
+
env: resolveConfiguredEnvironment(worktreeRoot, workload.env, record),
|
|
13566
13939
|
port: workload.port ?? "auto",
|
|
13940
|
+
varlock: workload.varlock,
|
|
13941
|
+
healthPath: workload.healthPath,
|
|
13567
13942
|
backend: "proc"
|
|
13568
13943
|
};
|
|
13569
13944
|
const clash = record.services.find((service) => service.name === name && service.backend === "docker");
|
|
@@ -13584,19 +13959,11 @@ class ComposeEngine {
|
|
|
13584
13959
|
}
|
|
13585
13960
|
const result = await startProc(worktreeRoot, configuredSpec, record.env, (pidfile2) => mirrorPidfile(record.project, pidfile2));
|
|
13586
13961
|
recordProc(record, result.record);
|
|
13962
|
+
applyConfiguredEndpoints(record, configuredWorkloads);
|
|
13587
13963
|
writeState(worktreeRoot, record);
|
|
13588
13964
|
if (result.error !== undefined)
|
|
13589
13965
|
throw result.error;
|
|
13590
13966
|
}
|
|
13591
|
-
if (opts?.workers || configuredWorkers.length > 0) {
|
|
13592
|
-
const configuredFilters = configuredWorkers.map(([name, workload]) => workload.wranglerConfig ?? name);
|
|
13593
|
-
const explicitFilters = Array.isArray(opts?.workers) ? opts.workers : [];
|
|
13594
|
-
await this.#upWorkers(worktreeRoot, record, {
|
|
13595
|
-
...opts,
|
|
13596
|
-
workers: opts?.workers === true ? true : [...new Set([...explicitFilters, ...configuredFilters])]
|
|
13597
|
-
});
|
|
13598
|
-
}
|
|
13599
|
-
applyConfiguredEndpoints(record, configuredWorkloads);
|
|
13600
13967
|
record.state = "up";
|
|
13601
13968
|
delete record.starter;
|
|
13602
13969
|
setTunnelDirty(syncExposures(record));
|
|
@@ -13604,17 +13971,27 @@ class ComposeEngine {
|
|
|
13604
13971
|
return record;
|
|
13605
13972
|
}));
|
|
13606
13973
|
}
|
|
13607
|
-
async#upWorkers(worktreeRoot, record, opts) {
|
|
13974
|
+
async#upWorkers(worktreeRoot, record, configuredWorkloads, opts) {
|
|
13608
13975
|
const plan = await planWorkers(worktreeRoot, {
|
|
13609
13976
|
filter: Array.isArray(opts.workers) ? opts.workers : [],
|
|
13610
13977
|
allowRemote: opts.allowRemote ?? false,
|
|
13611
13978
|
force: opts.force ?? false,
|
|
13612
|
-
varlock: !opts.noVarlock
|
|
13979
|
+
varlock: !opts.noVarlock
|
|
13613
13980
|
});
|
|
13614
13981
|
for (const w of plan.warnings)
|
|
13615
13982
|
process.stderr.write(`warning: ${w}
|
|
13616
13983
|
`);
|
|
13617
13984
|
for (const spec of plan.specs) {
|
|
13985
|
+
const configured = Object.entries(configuredWorkloads).find(([name, workload]) => workload.source === "wrangler" && (name === spec.name || workload.wranglerConfig !== undefined && resolve3(worktreeRoot, workload.wranglerConfig) === spec.configPath))?.[1];
|
|
13986
|
+
if (configured !== undefined && configured.source === "wrangler") {
|
|
13987
|
+
spec.cwd = configured.cwd ?? spec.cwd;
|
|
13988
|
+
spec.varlock = configured.varlock ?? spec.varlock;
|
|
13989
|
+
spec.healthPath = configured.healthPath;
|
|
13990
|
+
spec.env = {
|
|
13991
|
+
...spec.env,
|
|
13992
|
+
...resolveConfiguredEnvironment(worktreeRoot, configured.env, record)
|
|
13993
|
+
};
|
|
13994
|
+
}
|
|
13618
13995
|
const clash = record.services.find((s) => s.name === spec.name && s.backend === "docker");
|
|
13619
13996
|
if (clash !== undefined) {
|
|
13620
13997
|
throw new HestiaError("name-conflict", `worker "${spec.name}" collides with a compose service name`);
|
|
@@ -13908,13 +14285,13 @@ class ComposeEngine {
|
|
|
13908
14285
|
const composeFile = record?.composeFile ?? tryLoadConfig(worktreeRoot)?.composeFile;
|
|
13909
14286
|
if (composeFile !== undefined) {
|
|
13910
14287
|
const project2 = record?.project ?? projectName(repoId, repo, branch, worktreeRoot);
|
|
13911
|
-
const overrideFile = record?.overrideFile ??
|
|
14288
|
+
const overrideFile = record?.overrideFile ?? join25(hestiaDir(worktreeRoot), OVERRIDE_FILE);
|
|
13912
14289
|
if (existsSync21(overrideFile)) {
|
|
13913
14290
|
await composeDown({ project: project2, baseFile: composeFile, overrideFile, cwd: worktreeRoot }, opts?.destroy ?? false);
|
|
13914
14291
|
} else if (record?.composeFile !== undefined) {
|
|
13915
14292
|
const rest = ["compose", "-p", project2, "down", "--remove-orphans"];
|
|
13916
14293
|
if (opts?.destroy)
|
|
13917
|
-
rest.push("-v");
|
|
14294
|
+
rest.push("-v", "--rmi", "local");
|
|
13918
14295
|
await pexec9("docker", rest, { timeout: 180000 });
|
|
13919
14296
|
}
|
|
13920
14297
|
}
|
|
@@ -13971,7 +14348,7 @@ class ComposeEngine {
|
|
|
13971
14348
|
try {
|
|
13972
14349
|
const rest = ["compose", "-p", project, "down", "--remove-orphans"];
|
|
13973
14350
|
if (opts?.destroy)
|
|
13974
|
-
rest.push("-v");
|
|
14351
|
+
rest.push("-v", "--rmi", "local");
|
|
13975
14352
|
await pexec9("docker", rest, { timeout: 180000 });
|
|
13976
14353
|
} catch (err) {
|
|
13977
14354
|
if (fresh?.composeFile !== undefined) {
|
|
@@ -13982,7 +14359,7 @@ class ComposeEngine {
|
|
|
13982
14359
|
};
|
|
13983
14360
|
if (record !== null && existsSync21(record.worktree)) {
|
|
13984
14361
|
const info = await getRepoInfo(record.worktree);
|
|
13985
|
-
const identityMatches =
|
|
14362
|
+
const identityMatches = resolve3(info.worktreeRoot) === resolve3(record.worktree) && (record.repoId === undefined ? record.repo === info.repo : record.repoId === info.repoId);
|
|
13986
14363
|
const localRecord = readState(record.worktree);
|
|
13987
14364
|
if (identityMatches && localRecord?.project === record.project) {
|
|
13988
14365
|
await this.down(record.worktree, opts);
|
|
@@ -14141,7 +14518,7 @@ class ComposeEngine {
|
|
|
14141
14518
|
const alias = selection.endpoint.alias ?? selection.endpoint.name;
|
|
14142
14519
|
const authority = internalEndpointAuthority(record.project, alias);
|
|
14143
14520
|
const name = `aux-quick-${createHash7("sha256").update(alias).digest("hex").slice(0, 10)}`;
|
|
14144
|
-
const quickCfg =
|
|
14521
|
+
const quickCfg = join25(hestiaDir(worktreeRoot), `quick-${name}.yml`);
|
|
14145
14522
|
writeAtomicTextFile(quickCfg, `ingress:
|
|
14146
14523
|
- service: ${JSON.stringify(`unix:${publicGatewaySocketPath()}`)}
|
|
14147
14524
|
` + ` originRequest:
|
|
@@ -14512,9 +14889,28 @@ class ComposeEngine {
|
|
|
14512
14889
|
var engine = new ComposeEngine;
|
|
14513
14890
|
|
|
14514
14891
|
// packages/engine/src/daemon/duties.ts
|
|
14515
|
-
var
|
|
14892
|
+
var DEFAULT_SWEEP_INTERVAL_MS = 15000;
|
|
14893
|
+
var MAX_TIMER_INTERVAL_MS = 2147483647;
|
|
14894
|
+
function resolveSweepIntervalMs() {
|
|
14895
|
+
const value = process.env.HESTIA_SWEEP_INTERVAL_MS;
|
|
14896
|
+
if (value === undefined)
|
|
14897
|
+
return { intervalMs: DEFAULT_SWEEP_INTERVAL_MS, warnings: [] };
|
|
14898
|
+
const parsed = Number(value);
|
|
14899
|
+
if (Number.isInteger(parsed) && parsed >= 100 && parsed <= MAX_TIMER_INTERVAL_MS) {
|
|
14900
|
+
return { intervalMs: parsed, warnings: [] };
|
|
14901
|
+
}
|
|
14902
|
+
return {
|
|
14903
|
+
intervalMs: DEFAULT_SWEEP_INTERVAL_MS,
|
|
14904
|
+
warnings: [
|
|
14905
|
+
`invalid HESTIA_SWEEP_INTERVAL_MS=${JSON.stringify(value)} \u2014 using default ${DEFAULT_SWEEP_INTERVAL_MS}`
|
|
14906
|
+
]
|
|
14907
|
+
};
|
|
14908
|
+
}
|
|
14516
14909
|
function startDuties(admission, opts) {
|
|
14517
14910
|
const log = opts?.log ?? ((line) => console.error(line));
|
|
14911
|
+
const cadence = opts?.intervalMs === undefined ? resolveSweepIntervalMs() : { intervalMs: opts.intervalMs, warnings: [] };
|
|
14912
|
+
for (const warning of cadence.warnings)
|
|
14913
|
+
log(`sweep: ${warning}`);
|
|
14518
14914
|
let running = false;
|
|
14519
14915
|
const tick = async () => {
|
|
14520
14916
|
if (running)
|
|
@@ -14529,7 +14925,10 @@ function startDuties(admission, opts) {
|
|
|
14529
14925
|
if (opts?.shared !== undefined) {
|
|
14530
14926
|
try {
|
|
14531
14927
|
const state = admission.healthSnapshot();
|
|
14532
|
-
await opts.shared.sweep(new Set([...state.live, ...state.reserved]));
|
|
14928
|
+
const releases = await opts.shared.sweep(new Set([...state.live, ...state.reserved]));
|
|
14929
|
+
for (const release of releases) {
|
|
14930
|
+
log(`sweep: auto-released shared hostname "${release.name}" \u2014 holder ` + `${release.project} ${release.reason}, granting queue head`);
|
|
14931
|
+
}
|
|
14533
14932
|
} catch (err) {
|
|
14534
14933
|
log(`sweep: shared-hostname sweep failed: ${err.message}`);
|
|
14535
14934
|
}
|
|
@@ -14567,7 +14966,7 @@ function startDuties(admission, opts) {
|
|
|
14567
14966
|
running = false;
|
|
14568
14967
|
}
|
|
14569
14968
|
};
|
|
14570
|
-
const timer = setInterval(tick,
|
|
14969
|
+
const timer = setInterval(tick, cadence.intervalMs);
|
|
14571
14970
|
tick();
|
|
14572
14971
|
return () => clearInterval(timer);
|
|
14573
14972
|
}
|
|
@@ -14629,11 +15028,11 @@ async function main() {
|
|
|
14629
15028
|
port: "none",
|
|
14630
15029
|
backend: "proc"
|
|
14631
15030
|
}),
|
|
14632
|
-
logPath:
|
|
15031
|
+
logPath: join26(root, "daemon.log"),
|
|
14633
15032
|
signal: "term",
|
|
14634
15033
|
backend: "proc"
|
|
14635
15034
|
});
|
|
14636
|
-
writeAtomicJsonFile(
|
|
15035
|
+
writeAtomicJsonFile(join26(root, "daemon.json"), {
|
|
14637
15036
|
schemaVersion: STATE_SCHEMA_VERSION,
|
|
14638
15037
|
pid: process.pid,
|
|
14639
15038
|
port: server.port,
|
|
@@ -14649,7 +15048,7 @@ async function main() {
|
|
|
14649
15048
|
fleet.stop();
|
|
14650
15049
|
localRouter.stop();
|
|
14651
15050
|
removePidfile(root, PIDFILE_NAME2);
|
|
14652
|
-
rmSync11(
|
|
15051
|
+
rmSync11(join26(root, "daemon.json"), { force: true });
|
|
14653
15052
|
server.stop(true);
|
|
14654
15053
|
process.exit(0);
|
|
14655
15054
|
};
|
|
@@ -14664,4 +15063,4 @@ async function main() {
|
|
|
14664
15063
|
}
|
|
14665
15064
|
main();
|
|
14666
15065
|
|
|
14667
|
-
//# debugId=
|
|
15066
|
+
//# debugId=928DC10BC59C1FF964756E2164756E21
|