@tridha643/hestia 1.0.1 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1247 -135
- package/dist/cli.js.map +19 -17
- package/dist/daemon.js +889 -108
- package/dist/daemon.js.map +16 -14
- package/package.json +1 -1
- package/skills/hestia/SKILL.md +61 -0
package/dist/daemon.js
CHANGED
|
@@ -6945,9 +6945,9 @@ var require_public_api = __commonJS((exports) => {
|
|
|
6945
6945
|
});
|
|
6946
6946
|
|
|
6947
6947
|
// packages/engine/src/daemon/main.ts
|
|
6948
|
-
import { chmodSync as chmodSync6, rmSync as
|
|
6948
|
+
import { chmodSync as chmodSync6, rmSync as rmSync11 } from "fs";
|
|
6949
6949
|
import { randomBytes } from "crypto";
|
|
6950
|
-
import { join as
|
|
6950
|
+
import { join as join24 } from "path";
|
|
6951
6951
|
|
|
6952
6952
|
// packages/session-broker-core/src/limits.ts
|
|
6953
6953
|
var MAX_HTTP_BODY_BYTES = 4 * 1024 * 1024;
|
|
@@ -7807,6 +7807,19 @@ import {
|
|
|
7807
7807
|
} from "fs";
|
|
7808
7808
|
import { dirname } from "path";
|
|
7809
7809
|
import { randomUUID as randomUUID2 } from "crypto";
|
|
7810
|
+
function fsyncDir(path) {
|
|
7811
|
+
let fd;
|
|
7812
|
+
try {
|
|
7813
|
+
fd = openSync(dirname(path), "r");
|
|
7814
|
+
} catch {
|
|
7815
|
+
return;
|
|
7816
|
+
}
|
|
7817
|
+
try {
|
|
7818
|
+
fsyncSync(fd);
|
|
7819
|
+
} catch {} finally {
|
|
7820
|
+
closeSync(fd);
|
|
7821
|
+
}
|
|
7822
|
+
}
|
|
7810
7823
|
function writeAtomicJsonFile(path, value, options = {}) {
|
|
7811
7824
|
mkdirSync(dirname(path), { recursive: true });
|
|
7812
7825
|
const temporaryPath = `${path}.${process.pid}.${randomUUID2()}.tmp`;
|
|
@@ -7828,6 +7841,7 @@ function writeAtomicJsonFile(path, value, options = {}) {
|
|
|
7828
7841
|
rmSync(temporaryPath, { force: true });
|
|
7829
7842
|
throw error;
|
|
7830
7843
|
}
|
|
7844
|
+
fsyncDir(path);
|
|
7831
7845
|
}
|
|
7832
7846
|
function writeAtomicTextFile(path, source, mode = 384) {
|
|
7833
7847
|
mkdirSync(dirname(path), { recursive: true });
|
|
@@ -7849,6 +7863,7 @@ function writeAtomicTextFile(path, source, mode = 384) {
|
|
|
7849
7863
|
rmSync(temporaryPath, { force: true });
|
|
7850
7864
|
throw error;
|
|
7851
7865
|
}
|
|
7866
|
+
fsyncDir(path);
|
|
7852
7867
|
}
|
|
7853
7868
|
|
|
7854
7869
|
// packages/engine/src/proc/pidfile.ts
|
|
@@ -8203,10 +8218,10 @@ function clearState(worktreeRoot, project) {
|
|
|
8203
8218
|
}
|
|
8204
8219
|
|
|
8205
8220
|
// packages/engine/src/index.ts
|
|
8206
|
-
import { existsSync as
|
|
8221
|
+
import { existsSync as existsSync21, rmSync as rmSync10 } from "fs";
|
|
8207
8222
|
import { execFile as execFile10 } from "child_process";
|
|
8208
8223
|
import { promisify as promisify9 } from "util";
|
|
8209
|
-
import { join as
|
|
8224
|
+
import { join as join23, resolve as resolve2 } from "path";
|
|
8210
8225
|
import { createHash as createHash7 } from "crypto";
|
|
8211
8226
|
import { resolve4, resolve6, resolveCname } from "dns/promises";
|
|
8212
8227
|
|
|
@@ -9402,10 +9417,10 @@ import { execFile as execFile7 } from "child_process";
|
|
|
9402
9417
|
import { Agent, createServer as createServer2, request as httpRequest } from "http";
|
|
9403
9418
|
import { createConnection, createServer as createNetServer } from "net";
|
|
9404
9419
|
import { promisify as promisify6 } from "util";
|
|
9405
|
-
import { existsSync as
|
|
9406
|
-
import { chmodSync as chmodSync5, lstatSync, mkdirSync as mkdirSync7, rmSync as
|
|
9420
|
+
import { existsSync as existsSync14, readFileSync as readFileSync14, readdirSync as readdirSync5 } from "fs";
|
|
9421
|
+
import { chmodSync as chmodSync5, lstatSync, mkdirSync as mkdirSync7, rmSync as rmSync9 } from "fs";
|
|
9407
9422
|
import { tmpdir } from "os";
|
|
9408
|
-
import { join as
|
|
9423
|
+
import { join as join17 } from "path";
|
|
9409
9424
|
import { createHash as createHash5 } from "crypto";
|
|
9410
9425
|
|
|
9411
9426
|
// packages/engine/src/router/router-config.ts
|
|
@@ -9876,6 +9891,39 @@ async function releaseSlot(port, project) {
|
|
|
9876
9891
|
});
|
|
9877
9892
|
} catch {}
|
|
9878
9893
|
}
|
|
9894
|
+
async function sharedVerb(port, verb, body) {
|
|
9895
|
+
const timeoutMs = (body.waitMs ?? 0) + 5000;
|
|
9896
|
+
let response;
|
|
9897
|
+
try {
|
|
9898
|
+
response = await fetch(`http://127.0.0.1:${port}/hestia/shared/${verb}`, {
|
|
9899
|
+
method: "POST",
|
|
9900
|
+
headers: { "content-type": "application/json", ...daemonAuthHeaders(port) },
|
|
9901
|
+
body: JSON.stringify(body),
|
|
9902
|
+
signal: AbortSignal.timeout(timeoutMs)
|
|
9903
|
+
});
|
|
9904
|
+
} catch (error) {
|
|
9905
|
+
throw new HestiaError("daemon-unreachable", `could not reach hestiad on 127.0.0.1:${port}: ${error.message}`);
|
|
9906
|
+
}
|
|
9907
|
+
const payload = await response.json().catch(() => ({}));
|
|
9908
|
+
if (!response.ok) {
|
|
9909
|
+
throw new HestiaError(payload.code ?? "daemon-unreachable", payload.error ?? `daemon rejected shared ${verb}: HTTP ${response.status}`);
|
|
9910
|
+
}
|
|
9911
|
+
return {
|
|
9912
|
+
granted: payload.granted === true,
|
|
9913
|
+
holder: payload.holder,
|
|
9914
|
+
queued: payload.queued ?? []
|
|
9915
|
+
};
|
|
9916
|
+
}
|
|
9917
|
+
async function releaseSharedForProject(port, project, service) {
|
|
9918
|
+
try {
|
|
9919
|
+
await fetch(`http://127.0.0.1:${port}/hestia/shared/release-project`, {
|
|
9920
|
+
method: "POST",
|
|
9921
|
+
headers: { "content-type": "application/json", ...daemonAuthHeaders(port) },
|
|
9922
|
+
body: JSON.stringify({ project, service }),
|
|
9923
|
+
signal: AbortSignal.timeout(2000)
|
|
9924
|
+
});
|
|
9925
|
+
} catch {}
|
|
9926
|
+
}
|
|
9879
9927
|
|
|
9880
9928
|
// packages/engine/src/daemon/launchd.ts
|
|
9881
9929
|
import { existsSync as existsSync11, mkdirSync as mkdirSync6, readFileSync as readFileSync11, rmSync as rmSync6 } from "fs";
|
|
@@ -10258,9 +10306,57 @@ function createRoutes(admission, startedAt, dependencies) {
|
|
|
10258
10306
|
await admission.release(body.project);
|
|
10259
10307
|
return json({ ok: true });
|
|
10260
10308
|
}
|
|
10309
|
+
if (url.pathname.startsWith("/hestia/shared/") && request.method === "POST") {
|
|
10310
|
+
let body;
|
|
10311
|
+
try {
|
|
10312
|
+
body = await parseJsonBody(request);
|
|
10313
|
+
} catch (error) {
|
|
10314
|
+
return json({ error: `invalid JSON body: ${error.message}` }, 400);
|
|
10315
|
+
}
|
|
10316
|
+
const verb = url.pathname.slice("/hestia/shared/".length);
|
|
10317
|
+
if (!isProject(body.project))
|
|
10318
|
+
return json({ error: "project is invalid" }, 400);
|
|
10319
|
+
const project = body.project;
|
|
10320
|
+
try {
|
|
10321
|
+
if (verb === "release-project") {
|
|
10322
|
+
const service = body.service === undefined ? undefined : isService(body.service) ? body.service : null;
|
|
10323
|
+
if (service === null)
|
|
10324
|
+
return json({ error: "service is invalid" }, 400);
|
|
10325
|
+
await dependencies.shared.releaseProject(project, service);
|
|
10326
|
+
return json({ ok: true });
|
|
10327
|
+
}
|
|
10328
|
+
if (!isSharedName(body.name))
|
|
10329
|
+
return json({ error: "name is invalid" }, 400);
|
|
10330
|
+
const name = body.name;
|
|
10331
|
+
if (verb === "claim") {
|
|
10332
|
+
if (typeof body.worktree !== "string" || !body.worktree.startsWith("/") || body.worktree.length > 4096) {
|
|
10333
|
+
return json({ error: "worktree is invalid" }, 400);
|
|
10334
|
+
}
|
|
10335
|
+
const waitMs = typeof body.waitMs === "number" && Number.isFinite(body.waitMs) && body.waitMs > 0 ? Math.min(body.waitMs, 24 * 60 * 60 * 1000) : 0;
|
|
10336
|
+
return json(await dependencies.shared.request(name, { project, worktree: body.worktree }, waitMs));
|
|
10337
|
+
}
|
|
10338
|
+
if (verb === "allow")
|
|
10339
|
+
return json(await dependencies.shared.allow(name, project));
|
|
10340
|
+
if (verb === "deny")
|
|
10341
|
+
return json(await dependencies.shared.deny(name, project));
|
|
10342
|
+
if (verb === "release")
|
|
10343
|
+
return json(await dependencies.shared.release(name, project));
|
|
10344
|
+
if (verb === "cancel")
|
|
10345
|
+
return json(await dependencies.shared.cancel(name, project));
|
|
10346
|
+
} catch (error) {
|
|
10347
|
+
if (error instanceof HestiaError) {
|
|
10348
|
+
return json({ error: error.message, code: error.code }, error.code === "shared-not-found" ? 404 : 409);
|
|
10349
|
+
}
|
|
10350
|
+
return json({ error: error.message }, 500);
|
|
10351
|
+
}
|
|
10352
|
+
return json({ error: `unknown route ${url.pathname}` }, 404);
|
|
10353
|
+
}
|
|
10261
10354
|
return json({ error: `unknown route ${url.pathname}` }, 404);
|
|
10262
10355
|
};
|
|
10263
10356
|
}
|
|
10357
|
+
function isSharedName(value) {
|
|
10358
|
+
return typeof value === "string" && /^[a-z0-9][a-z0-9-]{0,62}$/.test(value);
|
|
10359
|
+
}
|
|
10264
10360
|
|
|
10265
10361
|
// packages/engine/src/daemon/ensure.ts
|
|
10266
10362
|
var START_TIMEOUT_MS = 5000;
|
|
@@ -10477,6 +10573,149 @@ async function keychainHasFingerprint(keychain, fingerprint) {
|
|
|
10477
10573
|
}
|
|
10478
10574
|
}
|
|
10479
10575
|
|
|
10576
|
+
// packages/engine/src/tunnel/shared.ts
|
|
10577
|
+
import { existsSync as existsSync13, readFileSync as readFileSync13, readdirSync as readdirSync4, rmSync as rmSync8 } from "fs";
|
|
10578
|
+
import { join as join16 } from "path";
|
|
10579
|
+
var NAME_RE = /^[a-z0-9][a-z0-9-]{0,62}$/;
|
|
10580
|
+
var LABEL_RE = /^[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?$/;
|
|
10581
|
+
var PATH_SEGMENT_RE = /^[A-Za-z0-9._~!$&'()*+,;=:@%-]+$/;
|
|
10582
|
+
function sharedRoot() {
|
|
10583
|
+
return join16(hestiaHome(), "shared");
|
|
10584
|
+
}
|
|
10585
|
+
function sharedPath(name) {
|
|
10586
|
+
return join16(sharedRoot(), `${name}.json`);
|
|
10587
|
+
}
|
|
10588
|
+
function assertSharedName(name) {
|
|
10589
|
+
if (!NAME_RE.test(name)) {
|
|
10590
|
+
throw new HestiaError("usage", `shared hostname handle ${JSON.stringify(name)} must be a DNS label: ` + `lowercase alphanumerics and hyphens, at most 63 characters`);
|
|
10591
|
+
}
|
|
10592
|
+
}
|
|
10593
|
+
function assertSharedHostnameFqdn(hostname) {
|
|
10594
|
+
const labels = hostname.split(".");
|
|
10595
|
+
const valid = hostname.length <= 253 && hostname === hostname.toLowerCase() && labels.length >= 2 && labels.every((label) => LABEL_RE.test(label));
|
|
10596
|
+
if (!valid) {
|
|
10597
|
+
throw new HestiaError("usage", `shared hostname ${JSON.stringify(hostname)} must be a fully-qualified ` + `lowercase domain (e.g. slack.acme.com)`);
|
|
10598
|
+
}
|
|
10599
|
+
}
|
|
10600
|
+
function normalizeSharedPath(input) {
|
|
10601
|
+
if (input === undefined)
|
|
10602
|
+
return;
|
|
10603
|
+
const trimmed = input.trim();
|
|
10604
|
+
const segments = trimmed.split("/").filter((segment) => segment !== "");
|
|
10605
|
+
for (const segment of segments) {
|
|
10606
|
+
if (segment === "." || segment === ".." || !PATH_SEGMENT_RE.test(segment)) {
|
|
10607
|
+
throw new HestiaError("usage", `shared hostname path ${JSON.stringify(input)} must be a URL path prefix ` + `like /webhooks/slack (no query, fragment, or ".." segments)`);
|
|
10608
|
+
}
|
|
10609
|
+
}
|
|
10610
|
+
if (segments.length === 0)
|
|
10611
|
+
return;
|
|
10612
|
+
return `/${segments.join("/")}`;
|
|
10613
|
+
}
|
|
10614
|
+
function sharedPathMatches(prefix, requestPath) {
|
|
10615
|
+
if (prefix === undefined)
|
|
10616
|
+
return true;
|
|
10617
|
+
return requestPath === prefix || requestPath.startsWith(`${prefix}/`);
|
|
10618
|
+
}
|
|
10619
|
+
function withSharedLock(fn) {
|
|
10620
|
+
return withLock(sharedRoot(), fn);
|
|
10621
|
+
}
|
|
10622
|
+
function parseShared(source) {
|
|
10623
|
+
let value;
|
|
10624
|
+
try {
|
|
10625
|
+
value = JSON.parse(source);
|
|
10626
|
+
} catch {
|
|
10627
|
+
return null;
|
|
10628
|
+
}
|
|
10629
|
+
if (value.schemaVersion !== STATE_SCHEMA_VERSION || typeof value.name !== "string" || !NAME_RE.test(value.name) || typeof value.hostname !== "string" || value.hostname.length === 0 || typeof value.tunnelUuid !== "string" || value.tunnelUuid.length === 0 || typeof value.zone !== "string" || value.zone.length === 0 || typeof value.service !== "string" || value.service.length === 0 || typeof value.createdAt !== "string")
|
|
10630
|
+
return null;
|
|
10631
|
+
if (value.path !== undefined && (typeof value.path !== "string" || !value.path.startsWith("/"))) {
|
|
10632
|
+
return null;
|
|
10633
|
+
}
|
|
10634
|
+
if (value.holder !== undefined) {
|
|
10635
|
+
const holder = value.holder;
|
|
10636
|
+
if (typeof holder.project !== "string" || typeof holder.worktree !== "string" || typeof holder.service !== "string" || typeof holder.at !== "string")
|
|
10637
|
+
return null;
|
|
10638
|
+
}
|
|
10639
|
+
if (value.queue !== undefined) {
|
|
10640
|
+
if (!Array.isArray(value.queue))
|
|
10641
|
+
return null;
|
|
10642
|
+
for (const entry of value.queue) {
|
|
10643
|
+
if (typeof entry !== "object" || entry === null || typeof entry.project !== "string" || typeof entry.worktree !== "string" || typeof entry.at !== "string")
|
|
10644
|
+
return null;
|
|
10645
|
+
}
|
|
10646
|
+
}
|
|
10647
|
+
return value;
|
|
10648
|
+
}
|
|
10649
|
+
function readSharedHostname(name) {
|
|
10650
|
+
const p = sharedPath(name);
|
|
10651
|
+
if (!existsSync13(p))
|
|
10652
|
+
return null;
|
|
10653
|
+
try {
|
|
10654
|
+
return parseShared(readFileSync13(p, "utf8"));
|
|
10655
|
+
} catch {
|
|
10656
|
+
return null;
|
|
10657
|
+
}
|
|
10658
|
+
}
|
|
10659
|
+
function listSharedHostnames() {
|
|
10660
|
+
const root = sharedRoot();
|
|
10661
|
+
if (!existsSync13(root))
|
|
10662
|
+
return [];
|
|
10663
|
+
const records = [];
|
|
10664
|
+
for (const entry of readdirSync4(root).sort()) {
|
|
10665
|
+
if (!entry.endsWith(".json"))
|
|
10666
|
+
continue;
|
|
10667
|
+
const record = readSharedHostname(entry.slice(0, -".json".length));
|
|
10668
|
+
if (record !== null && `${record.name}.json` === entry)
|
|
10669
|
+
records.push(record);
|
|
10670
|
+
}
|
|
10671
|
+
return records;
|
|
10672
|
+
}
|
|
10673
|
+
async function declareSharedHostname(record) {
|
|
10674
|
+
assertSharedName(record.name);
|
|
10675
|
+
assertSharedHostnameFqdn(record.hostname);
|
|
10676
|
+
const path = normalizeSharedPath(record.path);
|
|
10677
|
+
return withSharedLock(async () => {
|
|
10678
|
+
const collision = listSharedHostnames().find((candidate) => candidate.name !== record.name && candidate.hostname === record.hostname && (candidate.path ?? undefined) === (path ?? undefined));
|
|
10679
|
+
if (collision !== undefined) {
|
|
10680
|
+
throw new HestiaError("shared-conflict", `${record.hostname}${path ?? ""} is already declared as shared hostname ` + `"${collision.name}" \u2014 pick a distinct path or handle`);
|
|
10681
|
+
}
|
|
10682
|
+
const existing = readSharedHostname(record.name);
|
|
10683
|
+
if (existing !== null) {
|
|
10684
|
+
if (existing.hostname !== record.hostname || existing.tunnelUuid !== record.tunnelUuid || (existing.path ?? undefined) !== (path ?? undefined)) {
|
|
10685
|
+
throw new HestiaError("shared-conflict", `shared hostname "${record.name}" already declared as ${existing.hostname}` + `${existing.path ?? ""} on tunnel ${existing.tunnelUuid} \u2014 release and remove it before re-pointing`);
|
|
10686
|
+
}
|
|
10687
|
+
const merged = { ...existing, service: record.service, zone: record.zone };
|
|
10688
|
+
writeAtomicJsonFile(sharedPath(record.name), merged);
|
|
10689
|
+
return merged;
|
|
10690
|
+
}
|
|
10691
|
+
const fresh = {
|
|
10692
|
+
schemaVersion: STATE_SCHEMA_VERSION,
|
|
10693
|
+
...record,
|
|
10694
|
+
createdAt: new Date().toISOString()
|
|
10695
|
+
};
|
|
10696
|
+
if (path === undefined)
|
|
10697
|
+
delete fresh.path;
|
|
10698
|
+
else
|
|
10699
|
+
fresh.path = path;
|
|
10700
|
+
writeAtomicJsonFile(sharedPath(record.name), fresh);
|
|
10701
|
+
return fresh;
|
|
10702
|
+
});
|
|
10703
|
+
}
|
|
10704
|
+
async function updateSharedHostname(name, mutate) {
|
|
10705
|
+
return withSharedLock(async () => {
|
|
10706
|
+
const existing = readSharedHostname(name);
|
|
10707
|
+
if (existing === null)
|
|
10708
|
+
return null;
|
|
10709
|
+
const next = mutate(structuredClone(existing));
|
|
10710
|
+
if (next.queue !== undefined && next.queue.length === 0)
|
|
10711
|
+
delete next.queue;
|
|
10712
|
+
if (next.holder === undefined)
|
|
10713
|
+
delete next.holder;
|
|
10714
|
+
writeAtomicJsonFile(sharedPath(name), next);
|
|
10715
|
+
return next;
|
|
10716
|
+
});
|
|
10717
|
+
}
|
|
10718
|
+
|
|
10480
10719
|
// packages/engine/src/router/local-http-router.ts
|
|
10481
10720
|
var pexec6 = promisify6(execFile7);
|
|
10482
10721
|
var ROUTE_REFRESH_MS = 1000;
|
|
@@ -10492,18 +10731,25 @@ var HOP_BY_HOP_HEADERS = new Set([
|
|
|
10492
10731
|
"transfer-encoding",
|
|
10493
10732
|
"upgrade"
|
|
10494
10733
|
]);
|
|
10734
|
+
function pathnameOf(target) {
|
|
10735
|
+
if (target === undefined || target.length === 0)
|
|
10736
|
+
return "/";
|
|
10737
|
+
const cut = target.search(/[?#]/);
|
|
10738
|
+
const path = cut < 0 ? target : target.slice(0, cut);
|
|
10739
|
+
return path.startsWith("/") ? path : "/";
|
|
10740
|
+
}
|
|
10495
10741
|
|
|
10496
10742
|
class OriginOwnershipError extends Error {
|
|
10497
10743
|
}
|
|
10498
10744
|
function readRouterStackRecords() {
|
|
10499
|
-
const stacksDir =
|
|
10500
|
-
if (!
|
|
10745
|
+
const stacksDir = join17(hestiaHome(), "stacks");
|
|
10746
|
+
if (!existsSync14(stacksDir))
|
|
10501
10747
|
return [];
|
|
10502
10748
|
const records = [];
|
|
10503
|
-
for (const project of
|
|
10504
|
-
const path =
|
|
10749
|
+
for (const project of readdirSync5(stacksDir).sort()) {
|
|
10750
|
+
const path = join17(stacksDir, project, "stack.json");
|
|
10505
10751
|
try {
|
|
10506
|
-
const source =
|
|
10752
|
+
const source = readFileSync14(path);
|
|
10507
10753
|
if (source.byteLength > MAX_MIRROR_BYTES)
|
|
10508
10754
|
continue;
|
|
10509
10755
|
const record = parseStackRecord(source.toString("utf8"), path);
|
|
@@ -10655,11 +10901,11 @@ function internalEndpointAuthority(project, service) {
|
|
|
10655
10901
|
function publicGatewaySocketPath() {
|
|
10656
10902
|
const uid = process.getuid?.() ?? 501;
|
|
10657
10903
|
const homeHash = createHash5("sha256").update(hestiaHome()).digest("hex").slice(0, 12);
|
|
10658
|
-
return
|
|
10904
|
+
return join17(tmpdir(), `hestia-${uid}`, homeHash, "origin.sock");
|
|
10659
10905
|
}
|
|
10660
10906
|
function prepareGatewaySocket(path) {
|
|
10661
10907
|
const uid = process.getuid?.();
|
|
10662
|
-
const directories = [
|
|
10908
|
+
const directories = [join17(tmpdir(), `hestia-${uid ?? 501}`), join17(tmpdir(), `hestia-${uid ?? 501}`, createHash5("sha256").update(hestiaHome()).digest("hex").slice(0, 12))];
|
|
10663
10909
|
for (const directory of directories) {
|
|
10664
10910
|
mkdirSync7(directory, { recursive: true, mode: 448 });
|
|
10665
10911
|
const stat2 = lstatSync(directory);
|
|
@@ -10668,13 +10914,13 @@ function prepareGatewaySocket(path) {
|
|
|
10668
10914
|
}
|
|
10669
10915
|
chmodSync5(directory, 448);
|
|
10670
10916
|
}
|
|
10671
|
-
if (!
|
|
10917
|
+
if (!existsSync14(path))
|
|
10672
10918
|
return;
|
|
10673
10919
|
const stat = lstatSync(path);
|
|
10674
10920
|
if (stat.isSymbolicLink() || !stat.isSocket() || uid !== undefined && stat.uid !== uid) {
|
|
10675
10921
|
throw new Error(`refusing unsafe existing Hestia gateway socket ${path}`);
|
|
10676
10922
|
}
|
|
10677
|
-
|
|
10923
|
+
rmSync9(path);
|
|
10678
10924
|
}
|
|
10679
10925
|
function sendRouterResponse(response, status, message) {
|
|
10680
10926
|
response.writeHead(status, { "content-type": "text/plain; charset=utf-8", "x-hestia-router": "1" });
|
|
@@ -10687,6 +10933,7 @@ class HestiaLocalHttpRouter {
|
|
|
10687
10933
|
#frontServer = createNetServer((socket) => this.#acceptFrontSocket(socket));
|
|
10688
10934
|
#unixServer = createNetServer((socket) => this.#acceptFrontSocket(socket));
|
|
10689
10935
|
#targets = new Map;
|
|
10936
|
+
#sharedRoutes = new Map;
|
|
10690
10937
|
#timer;
|
|
10691
10938
|
async start() {
|
|
10692
10939
|
await new Promise((resolve2, reject) => {
|
|
@@ -10790,12 +11037,52 @@ class HestiaLocalHttpRouter {
|
|
|
10790
11037
|
}
|
|
10791
11038
|
}
|
|
10792
11039
|
}
|
|
11040
|
+
const sharedRoutes = new Map;
|
|
11041
|
+
for (const shared of listSharedHostnames()) {
|
|
11042
|
+
const hostname = shared.hostname.toLowerCase();
|
|
11043
|
+
targets.delete(hostname);
|
|
11044
|
+
const holder = shared.holder;
|
|
11045
|
+
let target;
|
|
11046
|
+
if (holder !== undefined) {
|
|
11047
|
+
const stack = records.find((record) => record.project === holder.project);
|
|
11048
|
+
const endpoint = stack?.endpoints.find((candidate2) => (candidate2.alias ?? candidate2.name) === holder.service);
|
|
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;
|
|
11052
|
+
const candidate = {
|
|
11053
|
+
hostname,
|
|
11054
|
+
project: holder.project,
|
|
11055
|
+
service: service === undefined || service.backend === "tunnel" || publishedPort === undefined ? undefined : { ...service, publishedPort }
|
|
11056
|
+
};
|
|
11057
|
+
const previous = this.#sharedRoutes.get(hostname)?.find((entry) => entry.name === shared.name)?.target;
|
|
11058
|
+
target = previous !== undefined && targetIdentity(previous) === targetIdentity(candidate) ? previous : candidate;
|
|
11059
|
+
}
|
|
11060
|
+
const list = sharedRoutes.get(hostname) ?? [];
|
|
11061
|
+
list.push({ path: shared.path, name: shared.name, claimed: holder !== undefined, target });
|
|
11062
|
+
sharedRoutes.set(hostname, list);
|
|
11063
|
+
}
|
|
11064
|
+
for (const list of sharedRoutes.values()) {
|
|
11065
|
+
list.sort((left, right) => (right.path?.length ?? -1) - (left.path?.length ?? -1));
|
|
11066
|
+
}
|
|
10793
11067
|
reconcilePortlessAliases([...targets.keys()].filter((hostname) => hostname.endsWith(".localhost")), this.port);
|
|
10794
|
-
|
|
10795
|
-
|
|
11068
|
+
const retained = new Set(targets.values());
|
|
11069
|
+
for (const list of sharedRoutes.values()) {
|
|
11070
|
+
for (const entry of list)
|
|
11071
|
+
if (entry.target !== undefined)
|
|
11072
|
+
retained.add(entry.target);
|
|
11073
|
+
}
|
|
11074
|
+
const priorTargets = [...this.#targets.values()];
|
|
11075
|
+
for (const list of this.#sharedRoutes.values()) {
|
|
11076
|
+
for (const entry of list)
|
|
11077
|
+
if (entry.target !== undefined)
|
|
11078
|
+
priorTargets.push(entry.target);
|
|
11079
|
+
}
|
|
11080
|
+
for (const target of priorTargets) {
|
|
11081
|
+
if (!retained.has(target))
|
|
10796
11082
|
target.agent?.destroy();
|
|
10797
11083
|
}
|
|
10798
11084
|
this.#targets = targets;
|
|
11085
|
+
this.#sharedRoutes = sharedRoutes;
|
|
10799
11086
|
}
|
|
10800
11087
|
stop() {
|
|
10801
11088
|
if (this.#timer)
|
|
@@ -10809,7 +11096,7 @@ class HestiaLocalHttpRouter {
|
|
|
10809
11096
|
target.agent?.destroy();
|
|
10810
11097
|
this.#frontServer.close();
|
|
10811
11098
|
this.#unixServer.close();
|
|
10812
|
-
|
|
11099
|
+
rmSync9(this.socketPath, { force: true });
|
|
10813
11100
|
this.#server.close();
|
|
10814
11101
|
}
|
|
10815
11102
|
#acceptFrontSocket(socket) {
|
|
@@ -10854,16 +11141,40 @@ Connection: close\r
|
|
|
10854
11141
|
`));
|
|
10855
11142
|
socket.once("error", () => internal.destroy());
|
|
10856
11143
|
}
|
|
11144
|
+
#matchTarget(hostname, requestPath) {
|
|
11145
|
+
const routes = this.#sharedRoutes.get(hostname);
|
|
11146
|
+
if (routes !== undefined) {
|
|
11147
|
+
const entry = routes.find((candidate) => sharedPathMatches(candidate.path, requestPath));
|
|
11148
|
+
if (entry === undefined)
|
|
11149
|
+
return { status: 404, message: "Hestia route not found" };
|
|
11150
|
+
if (!entry.claimed) {
|
|
11151
|
+
return {
|
|
11152
|
+
status: 503,
|
|
11153
|
+
message: `Shared hostname unclaimed \u2014 run \`hestia claim ${entry.name}\` in the worktree that should receive this traffic`
|
|
11154
|
+
};
|
|
11155
|
+
}
|
|
11156
|
+
if (entry.target?.service?.publishedPort === undefined) {
|
|
11157
|
+
return { status: 503, message: "Hestia route origin unavailable" };
|
|
11158
|
+
}
|
|
11159
|
+
return { target: entry.target };
|
|
11160
|
+
}
|
|
11161
|
+
const target = this.#targets.get(hostname);
|
|
11162
|
+
if (target === undefined)
|
|
11163
|
+
return { status: 404, message: "Hestia route not found" };
|
|
11164
|
+
if (target.service?.publishedPort === undefined) {
|
|
11165
|
+
return { status: 503, message: "Hestia route origin unavailable" };
|
|
11166
|
+
}
|
|
11167
|
+
return { target };
|
|
11168
|
+
}
|
|
10857
11169
|
async#proxyHttp(request, response) {
|
|
10858
11170
|
const hostname = parseRouteAuthority(request.headers.host);
|
|
10859
11171
|
if (hostname === null)
|
|
10860
11172
|
return sendRouterResponse(response, 400, "Malformed Host authority");
|
|
10861
|
-
const
|
|
10862
|
-
if (
|
|
10863
|
-
return sendRouterResponse(response,
|
|
10864
|
-
const
|
|
10865
|
-
|
|
10866
|
-
return sendRouterResponse(response, 503, "Hestia route origin unavailable");
|
|
11173
|
+
const match = this.#matchTarget(hostname, pathnameOf(request.url));
|
|
11174
|
+
if ("status" in match)
|
|
11175
|
+
return sendRouterResponse(response, match.status, match.message);
|
|
11176
|
+
const target = match.target;
|
|
11177
|
+
const port = target.service.publishedPort;
|
|
10867
11178
|
if (await verifyLocalRouterTarget(target) !== port) {
|
|
10868
11179
|
return sendRouterResponse(response, 503, "Hestia route origin unavailable");
|
|
10869
11180
|
}
|
|
@@ -10904,16 +11215,19 @@ Connection: close\r
|
|
|
10904
11215
|
`);
|
|
10905
11216
|
return;
|
|
10906
11217
|
}
|
|
10907
|
-
const
|
|
10908
|
-
|
|
10909
|
-
|
|
10910
|
-
|
|
11218
|
+
const rawHeaderLines = header.slice(0, -4).split(`\r
|
|
11219
|
+
`);
|
|
11220
|
+
const requestPath = pathnameOf(rawHeaderLines[0]?.split(" ")[1]);
|
|
11221
|
+
const match = this.#matchTarget(host, requestPath);
|
|
11222
|
+
if ("status" in match) {
|
|
11223
|
+
const reason = match.status === 404 ? "404 Not Found" : "503 Service Unavailable";
|
|
11224
|
+
socket.end(`HTTP/1.1 ${reason}\r
|
|
10911
11225
|
\r
|
|
10912
11226
|
`);
|
|
10913
11227
|
return;
|
|
10914
11228
|
}
|
|
10915
|
-
const
|
|
10916
|
-
|
|
11229
|
+
const target = match.target;
|
|
11230
|
+
const port = target.service.publishedPort;
|
|
10917
11231
|
const connectionValues = rawHeaderLines.slice(1).filter((line) => /^connection:/i.test(line)).map((line) => line.slice(line.indexOf(":") + 1));
|
|
10918
11232
|
const connectionNames = connectionHeaderTokens(connectionValues);
|
|
10919
11233
|
const retainedHeaders = rawHeaderLines.slice(1).filter((line) => {
|
|
@@ -10963,15 +11277,15 @@ Connection: close\r
|
|
|
10963
11277
|
}
|
|
10964
11278
|
|
|
10965
11279
|
// packages/engine/src/wrangler/adapter.ts
|
|
10966
|
-
import { existsSync as
|
|
11280
|
+
import { existsSync as existsSync15, readFileSync as readFileSync15, statSync as statSync2 } from "fs";
|
|
10967
11281
|
import { homedir as homedir3 } from "os";
|
|
10968
|
-
import { dirname as dirname5, join as
|
|
11282
|
+
import { dirname as dirname5, join as join18, relative as relative3 } from "path";
|
|
10969
11283
|
import { connect } from "net";
|
|
10970
11284
|
function privateRegistryDir(worktreeRoot) {
|
|
10971
|
-
return
|
|
11285
|
+
return join18(worktreeRoot, ".hestia", "wrangler-registry");
|
|
10972
11286
|
}
|
|
10973
11287
|
function globalRegistryDir() {
|
|
10974
|
-
return process.env.WRANGLER_REGISTRY_PATH ??
|
|
11288
|
+
return process.env.WRANGLER_REGISTRY_PATH ?? join18(homedir3(), ".wrangler", "config", "registry");
|
|
10975
11289
|
}
|
|
10976
11290
|
var HEARTBEAT_FRESH_MS = 60000;
|
|
10977
11291
|
function tcpConnectable(address, timeoutMs = 500) {
|
|
@@ -10995,19 +11309,19 @@ function tcpConnectable(address, timeoutMs = 500) {
|
|
|
10995
11309
|
}
|
|
10996
11310
|
async function preflightForeignSessions(workers) {
|
|
10997
11311
|
const dir = globalRegistryDir();
|
|
10998
|
-
if (!
|
|
11312
|
+
if (!existsSync15(dir))
|
|
10999
11313
|
return;
|
|
11000
11314
|
for (const w of workers) {
|
|
11001
11315
|
if (w.name === null)
|
|
11002
11316
|
continue;
|
|
11003
|
-
const entryPath =
|
|
11004
|
-
if (!
|
|
11317
|
+
const entryPath = join18(dir, w.name);
|
|
11318
|
+
if (!existsSync15(entryPath))
|
|
11005
11319
|
continue;
|
|
11006
11320
|
const freshMs = Date.now() - statSync2(entryPath).mtimeMs;
|
|
11007
11321
|
let live = freshMs < HEARTBEAT_FRESH_MS;
|
|
11008
11322
|
if (!live) {
|
|
11009
11323
|
try {
|
|
11010
|
-
const entry = JSON.parse(
|
|
11324
|
+
const entry = JSON.parse(readFileSync15(entryPath, "utf8"));
|
|
11011
11325
|
live = entry.debugPortAddress ? await tcpConnectable(entry.debugPortAddress) : false;
|
|
11012
11326
|
} catch {
|
|
11013
11327
|
live = false;
|
|
@@ -11030,8 +11344,8 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
11030
11344
|
const wranglerBinFor = (configPath) => {
|
|
11031
11345
|
let dir = dirname5(configPath);
|
|
11032
11346
|
for (;; ) {
|
|
11033
|
-
const candidate =
|
|
11034
|
-
if (
|
|
11347
|
+
const candidate = join18(dir, "node_modules", ".bin", "wrangler");
|
|
11348
|
+
if (existsSync15(candidate))
|
|
11035
11349
|
return candidate;
|
|
11036
11350
|
if (dir === worktreeRoot)
|
|
11037
11351
|
return null;
|
|
@@ -11096,11 +11410,11 @@ async function planWorkers(worktreeRoot, opts) {
|
|
|
11096
11410
|
}
|
|
11097
11411
|
|
|
11098
11412
|
// packages/engine/src/wrangler/verify.ts
|
|
11099
|
-
import { existsSync as
|
|
11413
|
+
import { existsSync as existsSync16, readdirSync as readdirSync6 } from "fs";
|
|
11100
11414
|
function namesIn(dir) {
|
|
11101
|
-
if (!
|
|
11415
|
+
if (!existsSync16(dir))
|
|
11102
11416
|
return new Set;
|
|
11103
|
-
return new Set(
|
|
11417
|
+
return new Set(readdirSync6(dir));
|
|
11104
11418
|
}
|
|
11105
11419
|
function snapshotGlobalRegistry() {
|
|
11106
11420
|
return namesIn(globalRegistryDir());
|
|
@@ -11125,16 +11439,16 @@ function globalGainWarnings(before, ourNames) {
|
|
|
11125
11439
|
// packages/engine/src/tunnel/cloudflared.ts
|
|
11126
11440
|
import { execFile as execFile8 } from "child_process";
|
|
11127
11441
|
import { promisify as promisify7 } from "util";
|
|
11128
|
-
import { existsSync as
|
|
11442
|
+
import { existsSync as existsSync17 } from "fs";
|
|
11129
11443
|
import { homedir as homedir4 } from "os";
|
|
11130
|
-
import { join as
|
|
11444
|
+
import { join as join19 } from "path";
|
|
11131
11445
|
var pexec7 = promisify7(execFile8);
|
|
11132
11446
|
var BIN = "cloudflared";
|
|
11133
11447
|
function cloudflaredHome() {
|
|
11134
|
-
return process.env.HESTIA_CLOUDFLARED_HOME ??
|
|
11448
|
+
return process.env.HESTIA_CLOUDFLARED_HOME ?? join19(homedir4(), ".cloudflared");
|
|
11135
11449
|
}
|
|
11136
11450
|
function credFilePath(uuid) {
|
|
11137
|
-
return process.env.TUNNEL_CRED_FILE ??
|
|
11451
|
+
return process.env.TUNNEL_CRED_FILE ?? join19(cloudflaredHome(), `${uuid}.json`);
|
|
11138
11452
|
}
|
|
11139
11453
|
async function run(args, timeoutMs = 30000) {
|
|
11140
11454
|
try {
|
|
@@ -11163,7 +11477,7 @@ async function adoptTunnel(name) {
|
|
|
11163
11477
|
throw new HestiaError("tunnel-not-found", `no tunnel named "${name}" on this account \u2014 create it once with ` + `\`cloudflared tunnel create ${name}\``);
|
|
11164
11478
|
}
|
|
11165
11479
|
const credFile = credFilePath(entry.id);
|
|
11166
|
-
if (!
|
|
11480
|
+
if (!existsSync17(credFile)) {
|
|
11167
11481
|
throw new HestiaError("tunnel-auth-missing", `credentials for tunnel "${name}" not found at ${credFile} \u2014 ` + `run \`cloudflared tunnel create\` on this machine or copy the JSON`);
|
|
11168
11482
|
}
|
|
11169
11483
|
return { uuid: entry.id, credFile, connections: entry.connections.length };
|
|
@@ -11171,8 +11485,8 @@ async function adoptTunnel(name) {
|
|
|
11171
11485
|
|
|
11172
11486
|
// packages/engine/src/tunnel/ingress.ts
|
|
11173
11487
|
import { createHash as createHash6 } from "crypto";
|
|
11174
|
-
import { existsSync as
|
|
11175
|
-
import { join as
|
|
11488
|
+
import { existsSync as existsSync18, readFileSync as readFileSync16 } from "fs";
|
|
11489
|
+
import { join as join20 } from "path";
|
|
11176
11490
|
var LABEL_MAX = 63;
|
|
11177
11491
|
var HASH_LEN = 6;
|
|
11178
11492
|
function hostnameFor(tunnelName, branch, service, zone) {
|
|
@@ -11192,12 +11506,12 @@ function inferZone(baseRules) {
|
|
|
11192
11506
|
return zones.size === 1 ? [...zones][0] : undefined;
|
|
11193
11507
|
}
|
|
11194
11508
|
function importBaseRules(uuid, name) {
|
|
11195
|
-
const p =
|
|
11196
|
-
if (!
|
|
11509
|
+
const p = join20(cloudflaredHome(), "config.yml");
|
|
11510
|
+
if (!existsSync18(p))
|
|
11197
11511
|
return [];
|
|
11198
11512
|
let parsed;
|
|
11199
11513
|
try {
|
|
11200
|
-
parsed = $parse(
|
|
11514
|
+
parsed = $parse(readFileSync16(p, "utf8"));
|
|
11201
11515
|
} catch {
|
|
11202
11516
|
return [];
|
|
11203
11517
|
}
|
|
@@ -11209,7 +11523,12 @@ function importBaseRules(uuid, name) {
|
|
|
11209
11523
|
return parsed.ingress.filter((r) => r.hostname !== undefined || r.path !== undefined);
|
|
11210
11524
|
}
|
|
11211
11525
|
function generateMergedConfig(opts) {
|
|
11212
|
-
assertDisjoint(opts.baseRules, opts.dynamicRules);
|
|
11526
|
+
assertDisjoint(opts.baseRules, opts.dynamicRules, opts.sharedRules ?? []);
|
|
11527
|
+
const sharedHostnames = [...new Set((opts.sharedRules ?? []).map((s) => s.hostname))];
|
|
11528
|
+
const shared = sharedHostnames.map((hostname) => ({
|
|
11529
|
+
hostname,
|
|
11530
|
+
service: `unix:${publicGatewaySocketPath()}`
|
|
11531
|
+
}));
|
|
11213
11532
|
const dynamic = opts.dynamicRules.map((d) => ({
|
|
11214
11533
|
hostname: d.hostname,
|
|
11215
11534
|
service: `unix:${publicGatewaySocketPath()}`,
|
|
@@ -11220,17 +11539,30 @@ function generateMergedConfig(opts) {
|
|
|
11220
11539
|
"credentials-file": opts.credFile,
|
|
11221
11540
|
ingress: [
|
|
11222
11541
|
...opts.baseRules,
|
|
11542
|
+
...shared,
|
|
11223
11543
|
...dynamic,
|
|
11224
11544
|
{ service: "http_status:404" }
|
|
11225
11545
|
]
|
|
11226
11546
|
});
|
|
11227
11547
|
}
|
|
11228
|
-
function assertDisjoint(base, dynamic) {
|
|
11548
|
+
function assertDisjoint(base, dynamic, shared) {
|
|
11229
11549
|
const seen = new Map;
|
|
11230
11550
|
for (const r of base) {
|
|
11231
11551
|
if (r.hostname !== undefined)
|
|
11232
11552
|
seen.set(r.hostname, "the user's static rules");
|
|
11233
11553
|
}
|
|
11554
|
+
const sharedByHost = new Map;
|
|
11555
|
+
for (const s of shared) {
|
|
11556
|
+
if (!sharedByHost.has(s.hostname))
|
|
11557
|
+
sharedByHost.set(s.hostname, s.name);
|
|
11558
|
+
}
|
|
11559
|
+
for (const [hostname, name] of sharedByHost) {
|
|
11560
|
+
const holder = seen.get(hostname);
|
|
11561
|
+
if (holder !== undefined) {
|
|
11562
|
+
throw new HestiaError("hostname-conflict", `shared hostname ${hostname} ("${name}") is already claimed by ${holder}`);
|
|
11563
|
+
}
|
|
11564
|
+
seen.set(hostname, `shared hostname "${name}"`);
|
|
11565
|
+
}
|
|
11234
11566
|
for (const d of dynamic) {
|
|
11235
11567
|
const holder = seen.get(d.hostname);
|
|
11236
11568
|
if (holder !== undefined) {
|
|
@@ -11242,12 +11574,12 @@ function assertDisjoint(base, dynamic) {
|
|
|
11242
11574
|
|
|
11243
11575
|
// packages/engine/src/tunnel/registry.ts
|
|
11244
11576
|
import {
|
|
11245
|
-
existsSync as
|
|
11577
|
+
existsSync as existsSync19,
|
|
11246
11578
|
mkdirSync as mkdirSync8,
|
|
11247
|
-
readFileSync as
|
|
11248
|
-
readdirSync as
|
|
11579
|
+
readFileSync as readFileSync17,
|
|
11580
|
+
readdirSync as readdirSync7
|
|
11249
11581
|
} from "fs";
|
|
11250
|
-
import { join as
|
|
11582
|
+
import { join as join21 } from "path";
|
|
11251
11583
|
|
|
11252
11584
|
// packages/engine/src/tunnel/verify.ts
|
|
11253
11585
|
var POLL_MS4 = 300;
|
|
@@ -11295,24 +11627,24 @@ async function quickTunnelUrl(metricsPort, timeoutMs) {
|
|
|
11295
11627
|
var CONNECTOR = "connector";
|
|
11296
11628
|
var READY_TIMEOUT_MS2 = 30000;
|
|
11297
11629
|
function tunnelDir(uuid) {
|
|
11298
|
-
return
|
|
11630
|
+
return join21(hestiaHome(), "tunnel", uuid);
|
|
11299
11631
|
}
|
|
11300
11632
|
function configPath(uuid) {
|
|
11301
|
-
return
|
|
11633
|
+
return join21(tunnelDir(uuid), "config.yml");
|
|
11302
11634
|
}
|
|
11303
11635
|
function adoptedMarker(uuid) {
|
|
11304
|
-
return
|
|
11636
|
+
return join21(tunnelDir(uuid), "adopted.json");
|
|
11305
11637
|
}
|
|
11306
11638
|
function isAdopted(uuid) {
|
|
11307
|
-
return
|
|
11639
|
+
return existsSync19(adoptedMarker(uuid));
|
|
11308
11640
|
}
|
|
11309
11641
|
function readAdopted(uuid) {
|
|
11310
11642
|
const p = adoptedMarker(uuid);
|
|
11311
|
-
if (!
|
|
11643
|
+
if (!existsSync19(p))
|
|
11312
11644
|
return null;
|
|
11313
11645
|
let marker;
|
|
11314
11646
|
try {
|
|
11315
|
-
marker = JSON.parse(
|
|
11647
|
+
marker = JSON.parse(readFileSync17(p, "utf8"));
|
|
11316
11648
|
} catch {
|
|
11317
11649
|
marker = {};
|
|
11318
11650
|
}
|
|
@@ -11320,14 +11652,14 @@ function readAdopted(uuid) {
|
|
|
11320
11652
|
return { uuid, name: marker.name, credFile: marker.credFile, reconstructed: false };
|
|
11321
11653
|
}
|
|
11322
11654
|
let name;
|
|
11323
|
-
const stacksDir =
|
|
11324
|
-
if (
|
|
11325
|
-
for (const project of
|
|
11326
|
-
const sp =
|
|
11327
|
-
if (!
|
|
11655
|
+
const stacksDir = join21(hestiaHome(), "stacks");
|
|
11656
|
+
if (existsSync19(stacksDir)) {
|
|
11657
|
+
for (const project of readdirSync7(stacksDir)) {
|
|
11658
|
+
const sp = join21(stacksDir, project, "stack.json");
|
|
11659
|
+
if (!existsSync19(sp))
|
|
11328
11660
|
continue;
|
|
11329
11661
|
try {
|
|
11330
|
-
const record = parseStackRecord(
|
|
11662
|
+
const record = parseStackRecord(readFileSync17(sp, "utf8"), sp);
|
|
11331
11663
|
if (record.tunnel?.uuid === uuid) {
|
|
11332
11664
|
name = record.tunnel.name;
|
|
11333
11665
|
break;
|
|
@@ -11343,24 +11675,24 @@ function readAdopted(uuid) {
|
|
|
11343
11675
|
};
|
|
11344
11676
|
}
|
|
11345
11677
|
function listAdopted() {
|
|
11346
|
-
const root =
|
|
11347
|
-
if (!
|
|
11678
|
+
const root = join21(hestiaHome(), "tunnel");
|
|
11679
|
+
if (!existsSync19(root))
|
|
11348
11680
|
return [];
|
|
11349
|
-
return
|
|
11681
|
+
return readdirSync7(root).filter((uuid) => isAdopted(uuid));
|
|
11350
11682
|
}
|
|
11351
11683
|
function collectDynamicRules(uuid) {
|
|
11352
|
-
const stacksDir =
|
|
11684
|
+
const stacksDir = join21(hestiaHome(), "stacks");
|
|
11353
11685
|
const rules = [];
|
|
11354
11686
|
const dropped = [];
|
|
11355
|
-
if (!
|
|
11687
|
+
if (!existsSync19(stacksDir))
|
|
11356
11688
|
return { rules, dropped };
|
|
11357
|
-
for (const project of
|
|
11358
|
-
const p =
|
|
11359
|
-
if (!
|
|
11689
|
+
for (const project of readdirSync7(stacksDir)) {
|
|
11690
|
+
const p = join21(stacksDir, project, "stack.json");
|
|
11691
|
+
if (!existsSync19(p))
|
|
11360
11692
|
continue;
|
|
11361
11693
|
let record;
|
|
11362
11694
|
try {
|
|
11363
|
-
record = parseStackRecord(
|
|
11695
|
+
record = parseStackRecord(readFileSync17(p, "utf8"), p);
|
|
11364
11696
|
} catch {
|
|
11365
11697
|
continue;
|
|
11366
11698
|
}
|
|
@@ -11389,20 +11721,22 @@ async function reconcileTunnel(ref, opts) {
|
|
|
11389
11721
|
for (const d of dropped) {
|
|
11390
11722
|
warnings.push(`exposure ${d.hostname} dropped \u2014 service "${d.service}" of ` + `${d.project} is not running (hostname now 404s)`);
|
|
11391
11723
|
}
|
|
11724
|
+
const sharedRules = listSharedHostnames().filter((record) => record.tunnelUuid === ref.uuid).map((record) => ({ name: record.name, hostname: record.hostname }));
|
|
11392
11725
|
const cfgPath = configPath(ref.uuid);
|
|
11393
11726
|
const nextConfig = generateMergedConfig({
|
|
11394
11727
|
uuid: ref.uuid,
|
|
11395
11728
|
credFile: ref.credFile,
|
|
11396
11729
|
baseRules,
|
|
11397
|
-
dynamicRules: rules
|
|
11730
|
+
dynamicRules: rules,
|
|
11731
|
+
sharedRules
|
|
11398
11732
|
});
|
|
11399
11733
|
const pf = readPidfile(dir, CONNECTOR);
|
|
11400
11734
|
const live = pf !== null && isLive(pf);
|
|
11401
|
-
const currentConfig =
|
|
11735
|
+
const currentConfig = existsSync19(cfgPath) ? readFileSync17(cfgPath, "utf8") : null;
|
|
11402
11736
|
if (live && currentConfig === nextConfig) {
|
|
11403
11737
|
return { restarted: false, metricsPort: pf.port, ready: true, warnings };
|
|
11404
11738
|
}
|
|
11405
|
-
if (baseRules.length === 0 && rules.length === 0) {
|
|
11739
|
+
if (baseRules.length === 0 && rules.length === 0 && sharedRules.length === 0) {
|
|
11406
11740
|
if (pf !== null) {
|
|
11407
11741
|
await stopProcTree(pf);
|
|
11408
11742
|
removePidfile(dir, CONNECTOR);
|
|
@@ -11887,11 +12221,225 @@ async function* streamStackLogs(record, options = {}) {
|
|
|
11887
12221
|
await Promise.allSettled(pumps);
|
|
11888
12222
|
}
|
|
11889
12223
|
}
|
|
12224
|
+
// packages/engine/src/daemon/shared-arbiter.ts
|
|
12225
|
+
class SharedArbiter {
|
|
12226
|
+
onChange;
|
|
12227
|
+
#mutex = Promise.resolve();
|
|
12228
|
+
#polls = new Map;
|
|
12229
|
+
constructor(onChange) {
|
|
12230
|
+
this.onChange = onChange;
|
|
12231
|
+
}
|
|
12232
|
+
#locked(fn) {
|
|
12233
|
+
const next = this.#mutex.then(fn, fn);
|
|
12234
|
+
this.#mutex = next.catch(() => {});
|
|
12235
|
+
return next;
|
|
12236
|
+
}
|
|
12237
|
+
async#notify() {
|
|
12238
|
+
try {
|
|
12239
|
+
await this.onChange?.();
|
|
12240
|
+
} catch {}
|
|
12241
|
+
}
|
|
12242
|
+
#result(record, granted) {
|
|
12243
|
+
return { granted, holder: record.holder, queued: [...record.queue ?? []] };
|
|
12244
|
+
}
|
|
12245
|
+
#wake(name, project, result) {
|
|
12246
|
+
const key = `${name}\x00${project}`;
|
|
12247
|
+
for (const poll of this.#polls.get(key) ?? []) {
|
|
12248
|
+
clearTimeout(poll.timer);
|
|
12249
|
+
poll.resolve(result);
|
|
12250
|
+
}
|
|
12251
|
+
this.#polls.delete(key);
|
|
12252
|
+
}
|
|
12253
|
+
#grantNext(record) {
|
|
12254
|
+
const queue = [...record.queue ?? []];
|
|
12255
|
+
for (;; ) {
|
|
12256
|
+
const head = queue.shift();
|
|
12257
|
+
if (head === undefined) {
|
|
12258
|
+
const next = { ...record, queue: [] };
|
|
12259
|
+
delete next.holder;
|
|
12260
|
+
return next;
|
|
12261
|
+
}
|
|
12262
|
+
if (readMirrorState(head.project) === null)
|
|
12263
|
+
continue;
|
|
12264
|
+
return {
|
|
12265
|
+
...record,
|
|
12266
|
+
holder: {
|
|
12267
|
+
project: head.project,
|
|
12268
|
+
worktree: head.worktree,
|
|
12269
|
+
service: record.service,
|
|
12270
|
+
at: new Date().toISOString()
|
|
12271
|
+
},
|
|
12272
|
+
queue
|
|
12273
|
+
};
|
|
12274
|
+
}
|
|
12275
|
+
}
|
|
12276
|
+
async request(name, requester, waitMs) {
|
|
12277
|
+
const outcome = await this.#locked(async () => {
|
|
12278
|
+
let granted = false;
|
|
12279
|
+
const updated = await updateSharedHostname(name, (record) => {
|
|
12280
|
+
if (record.holder?.project === requester.project) {
|
|
12281
|
+
granted = true;
|
|
12282
|
+
return {
|
|
12283
|
+
...record,
|
|
12284
|
+
holder: { ...record.holder, worktree: requester.worktree },
|
|
12285
|
+
queue: (record.queue ?? []).filter((waiter) => waiter.project !== requester.project)
|
|
12286
|
+
};
|
|
12287
|
+
}
|
|
12288
|
+
if (record.holder === undefined) {
|
|
12289
|
+
granted = true;
|
|
12290
|
+
return {
|
|
12291
|
+
...record,
|
|
12292
|
+
holder: {
|
|
12293
|
+
project: requester.project,
|
|
12294
|
+
worktree: requester.worktree,
|
|
12295
|
+
service: record.service,
|
|
12296
|
+
at: new Date().toISOString()
|
|
12297
|
+
},
|
|
12298
|
+
queue: (record.queue ?? []).filter((waiter) => waiter.project !== requester.project)
|
|
12299
|
+
};
|
|
12300
|
+
}
|
|
12301
|
+
const queue = [...record.queue ?? []];
|
|
12302
|
+
const existing = queue.find((waiter) => waiter.project === requester.project);
|
|
12303
|
+
if (existing !== undefined)
|
|
12304
|
+
existing.worktree = requester.worktree;
|
|
12305
|
+
else
|
|
12306
|
+
queue.push({ project: requester.project, worktree: requester.worktree, at: new Date().toISOString() });
|
|
12307
|
+
return { ...record, queue };
|
|
12308
|
+
});
|
|
12309
|
+
if (updated === null) {
|
|
12310
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared`);
|
|
12311
|
+
}
|
|
12312
|
+
return { granted, record: updated };
|
|
12313
|
+
});
|
|
12314
|
+
if (outcome.granted) {
|
|
12315
|
+
await this.#notify();
|
|
12316
|
+
return this.#result(outcome.record, true);
|
|
12317
|
+
}
|
|
12318
|
+
if (waitMs <= 0)
|
|
12319
|
+
return this.#result(outcome.record, false);
|
|
12320
|
+
return new Promise((resolve2) => {
|
|
12321
|
+
const key = `${name}\x00${requester.project}`;
|
|
12322
|
+
const polls = this.#polls.get(key) ?? [];
|
|
12323
|
+
const poll = {
|
|
12324
|
+
resolve: resolve2,
|
|
12325
|
+
timer: setTimeout(() => {
|
|
12326
|
+
const remaining = (this.#polls.get(key) ?? []).filter((candidate) => candidate !== poll);
|
|
12327
|
+
if (remaining.length === 0)
|
|
12328
|
+
this.#polls.delete(key);
|
|
12329
|
+
else
|
|
12330
|
+
this.#polls.set(key, remaining);
|
|
12331
|
+
const record = readSharedHostname(name);
|
|
12332
|
+
resolve2(record === null ? { granted: false, queued: [] } : this.#result(record, record.holder?.project === requester.project));
|
|
12333
|
+
}, waitMs)
|
|
12334
|
+
};
|
|
12335
|
+
polls.push(poll);
|
|
12336
|
+
this.#polls.set(key, polls);
|
|
12337
|
+
});
|
|
12338
|
+
}
|
|
12339
|
+
async cancel(name, project) {
|
|
12340
|
+
const record = await this.#locked(() => updateSharedHostname(name, (current) => ({
|
|
12341
|
+
...current,
|
|
12342
|
+
queue: (current.queue ?? []).filter((waiter) => waiter.project !== project)
|
|
12343
|
+
})));
|
|
12344
|
+
if (record === null)
|
|
12345
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared`);
|
|
12346
|
+
this.#wake(name, project, this.#result(record, false));
|
|
12347
|
+
return this.#result(record, false);
|
|
12348
|
+
}
|
|
12349
|
+
#assertHolder(record, project, verb) {
|
|
12350
|
+
if (record.holder?.project !== project) {
|
|
12351
|
+
throw new HestiaError("shared-not-holder", record.holder === undefined ? `cannot ${verb} "${record.name}" \u2014 it is unclaimed` : `cannot ${verb} "${record.name}" \u2014 it is held by ${record.holder.project}, not ${project}`);
|
|
12352
|
+
}
|
|
12353
|
+
}
|
|
12354
|
+
async allow(name, callerProject) {
|
|
12355
|
+
const record = await this.#transfer(name, callerProject, "allow");
|
|
12356
|
+
return this.#result(record, false);
|
|
12357
|
+
}
|
|
12358
|
+
async deny(name, callerProject) {
|
|
12359
|
+
const record = await this.#locked(async () => {
|
|
12360
|
+
const current = readSharedHostname(name);
|
|
12361
|
+
if (current === null)
|
|
12362
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared`);
|
|
12363
|
+
this.#assertHolder(current, callerProject, "deny");
|
|
12364
|
+
const updated = await updateSharedHostname(name, (candidate) => {
|
|
12365
|
+
const queue = [...candidate.queue ?? []];
|
|
12366
|
+
if (queue.length > 0)
|
|
12367
|
+
queue[0] = { ...queue[0], denied: true };
|
|
12368
|
+
return { ...candidate, queue };
|
|
12369
|
+
});
|
|
12370
|
+
return updated;
|
|
12371
|
+
});
|
|
12372
|
+
return this.#result(record, false);
|
|
12373
|
+
}
|
|
12374
|
+
async release(name, callerProject) {
|
|
12375
|
+
const record = await this.#transfer(name, callerProject, "release");
|
|
12376
|
+
return this.#result(record, false);
|
|
12377
|
+
}
|
|
12378
|
+
async#transfer(name, callerProject, verb) {
|
|
12379
|
+
const record = await this.#locked(async () => {
|
|
12380
|
+
const current = readSharedHostname(name);
|
|
12381
|
+
if (current === null)
|
|
12382
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared`);
|
|
12383
|
+
this.#assertHolder(current, callerProject, verb);
|
|
12384
|
+
return await updateSharedHostname(name, (candidate) => this.#grantNext(candidate));
|
|
12385
|
+
});
|
|
12386
|
+
if (record.holder !== undefined) {
|
|
12387
|
+
this.#wake(record.name, record.holder.project, this.#result(record, true));
|
|
12388
|
+
}
|
|
12389
|
+
await this.#notify();
|
|
12390
|
+
return record;
|
|
12391
|
+
}
|
|
12392
|
+
async releaseProject(project, service) {
|
|
12393
|
+
const held = listSharedHostnames().filter((record) => record.holder?.project === project && (service === undefined || record.service === service));
|
|
12394
|
+
let changed = false;
|
|
12395
|
+
for (const record of held) {
|
|
12396
|
+
const updated = await this.#locked(async () => {
|
|
12397
|
+
const current = readSharedHostname(record.name);
|
|
12398
|
+
if (current?.holder?.project !== project)
|
|
12399
|
+
return null;
|
|
12400
|
+
return await updateSharedHostname(record.name, (candidate) => this.#grantNext(candidate));
|
|
12401
|
+
});
|
|
12402
|
+
if (updated === null || updated === undefined)
|
|
12403
|
+
continue;
|
|
12404
|
+
changed = true;
|
|
12405
|
+
if (updated.holder !== undefined) {
|
|
12406
|
+
this.#wake(updated.name, updated.holder.project, this.#result(updated, true));
|
|
12407
|
+
}
|
|
12408
|
+
}
|
|
12409
|
+
if (changed)
|
|
12410
|
+
await this.#notify();
|
|
12411
|
+
}
|
|
12412
|
+
async sweep(occupied) {
|
|
12413
|
+
let changed = false;
|
|
12414
|
+
for (const record of listSharedHostnames()) {
|
|
12415
|
+
const holderDead = record.holder !== undefined && !occupied.has(record.holder.project) && readMirrorState(record.holder.project) === null;
|
|
12416
|
+
const deadWaiters = (record.queue ?? []).filter((waiter) => readMirrorState(waiter.project) === null);
|
|
12417
|
+
if (!holderDead && deadWaiters.length === 0)
|
|
12418
|
+
continue;
|
|
12419
|
+
const updated = await this.#locked(() => updateSharedHostname(record.name, (candidate) => {
|
|
12420
|
+
const pruned = {
|
|
12421
|
+
...candidate,
|
|
12422
|
+
queue: (candidate.queue ?? []).filter((waiter) => readMirrorState(waiter.project) !== null)
|
|
12423
|
+
};
|
|
12424
|
+
const currentHolderDead = pruned.holder !== undefined && !occupied.has(pruned.holder.project) && readMirrorState(pruned.holder.project) === null;
|
|
12425
|
+
return currentHolderDead ? this.#grantNext(pruned) : pruned;
|
|
12426
|
+
}));
|
|
12427
|
+
if (updated === null)
|
|
12428
|
+
continue;
|
|
12429
|
+
changed = true;
|
|
12430
|
+
if (updated.holder !== undefined && updated.holder.project !== record.holder?.project) {
|
|
12431
|
+
this.#wake(updated.name, updated.holder.project, this.#result(updated, true));
|
|
12432
|
+
}
|
|
12433
|
+
}
|
|
12434
|
+
if (changed)
|
|
12435
|
+
await this.#notify();
|
|
12436
|
+
}
|
|
12437
|
+
}
|
|
11890
12438
|
// packages/engine/src/daemon/fleet-monitor.ts
|
|
11891
12439
|
import { execFile as execFile9 } from "child_process";
|
|
11892
12440
|
import { promisify as promisify8 } from "util";
|
|
11893
|
-
import { existsSync as
|
|
11894
|
-
import { join as
|
|
12441
|
+
import { existsSync as existsSync20, readFileSync as readFileSync18, readdirSync as readdirSync8 } from "fs";
|
|
12442
|
+
import { join as join22 } from "path";
|
|
11895
12443
|
var pexec8 = promisify8(execFile9);
|
|
11896
12444
|
var DEFAULT_REFRESH_MS = 1000;
|
|
11897
12445
|
var DEFAULT_HEARTBEAT_MS = 15000;
|
|
@@ -11938,13 +12486,13 @@ class LatestFleetChannel {
|
|
|
11938
12486
|
function readManagedMirrors() {
|
|
11939
12487
|
const records = [];
|
|
11940
12488
|
const warnings = [];
|
|
11941
|
-
const stacksDirectory =
|
|
11942
|
-
if (!
|
|
12489
|
+
const stacksDirectory = join22(hestiaHome(), "stacks");
|
|
12490
|
+
if (!existsSync20(stacksDirectory))
|
|
11943
12491
|
return { records, warnings };
|
|
11944
|
-
for (const project of
|
|
11945
|
-
const path =
|
|
12492
|
+
for (const project of readdirSync8(stacksDirectory).sort()) {
|
|
12493
|
+
const path = join22(stacksDirectory, project, "stack.json");
|
|
11946
12494
|
try {
|
|
11947
|
-
const source =
|
|
12495
|
+
const source = readFileSync18(path);
|
|
11948
12496
|
if (source.byteLength > MAX_MIRROR_BYTES2) {
|
|
11949
12497
|
warnings.push(`Fleet mirror too large for ${project}`);
|
|
11950
12498
|
continue;
|
|
@@ -11964,7 +12512,7 @@ function readManagedMirrors() {
|
|
|
11964
12512
|
async function attributeLegacyRepoId(record) {
|
|
11965
12513
|
if (record.repoId !== undefined)
|
|
11966
12514
|
return record.repoId;
|
|
11967
|
-
if (!
|
|
12515
|
+
if (!existsSync20(record.worktree))
|
|
11968
12516
|
return null;
|
|
11969
12517
|
const info = await getRepoInfo(record.worktree);
|
|
11970
12518
|
if (projectName(info.repoId, info.repo, info.branch, info.worktreeRoot) !== record.project)
|
|
@@ -12070,6 +12618,25 @@ function reservationIdentity(reservation, records) {
|
|
|
12070
12618
|
worktree: record.worktree
|
|
12071
12619
|
};
|
|
12072
12620
|
}
|
|
12621
|
+
function collectFleetShared(repoProjects) {
|
|
12622
|
+
return listSharedHostnames().map((record) => ({
|
|
12623
|
+
name: record.name,
|
|
12624
|
+
hostname: record.hostname,
|
|
12625
|
+
...record.path === undefined ? {} : { path: record.path },
|
|
12626
|
+
url: `https://${record.hostname}${record.path ?? ""}`,
|
|
12627
|
+
holder: record.holder === undefined ? undefined : {
|
|
12628
|
+
project: record.holder.project,
|
|
12629
|
+
worktree: record.holder.worktree,
|
|
12630
|
+
mine: repoProjects.has(record.holder.project)
|
|
12631
|
+
},
|
|
12632
|
+
queue: (record.queue ?? []).map((waiter) => ({
|
|
12633
|
+
project: waiter.project,
|
|
12634
|
+
worktree: waiter.worktree,
|
|
12635
|
+
...waiter.denied === true ? { denied: true } : {},
|
|
12636
|
+
mine: repoProjects.has(waiter.project)
|
|
12637
|
+
}))
|
|
12638
|
+
})).sort((left, right) => left.name.localeCompare(right.name));
|
|
12639
|
+
}
|
|
12073
12640
|
async function collectFleetSnapshot(repoId, admission) {
|
|
12074
12641
|
const mirrorResult = readManagedMirrors();
|
|
12075
12642
|
const machineConfig = readHestiaMachineConfig();
|
|
@@ -12165,6 +12732,7 @@ async function collectFleetSnapshot(repoId, admission) {
|
|
|
12165
12732
|
const allRecordedProjects = new Set(attributed.map((record) => record.project));
|
|
12166
12733
|
const unbackedReservations = reservations.filter((reservation) => !allRecordedProjects.has(reservation.project));
|
|
12167
12734
|
const { maxStacks, warnings: capWarnings } = resolveMaxStacks();
|
|
12735
|
+
const repoProjects = new Set(attributed.filter((record) => record.repoId === repoId).map((record) => record.project));
|
|
12168
12736
|
return {
|
|
12169
12737
|
repoId,
|
|
12170
12738
|
observedAt: new Date().toISOString(),
|
|
@@ -12175,6 +12743,7 @@ async function collectFleetSnapshot(repoId, admission) {
|
|
|
12175
12743
|
queued: queued.length
|
|
12176
12744
|
},
|
|
12177
12745
|
stacks: stackViews,
|
|
12746
|
+
shared: collectFleetShared(repoProjects),
|
|
12178
12747
|
warnings: [...capWarnings, ...warnings].sort()
|
|
12179
12748
|
};
|
|
12180
12749
|
}
|
|
@@ -12183,6 +12752,7 @@ function semanticFleetSnapshot(snapshot) {
|
|
|
12183
12752
|
repoId: snapshot.repoId,
|
|
12184
12753
|
capacity: snapshot.capacity,
|
|
12185
12754
|
stacks: snapshot.stacks,
|
|
12755
|
+
shared: snapshot.shared,
|
|
12186
12756
|
warnings: snapshot.warnings
|
|
12187
12757
|
});
|
|
12188
12758
|
}
|
|
@@ -12398,7 +12968,7 @@ async function assertHestiaStateIgnored(worktreeRoot) {
|
|
|
12398
12968
|
try {
|
|
12399
12969
|
await pexec9("git", ["-C", worktreeRoot, "check-ignore", "-q", ".hestia/"], { timeout: 5000 });
|
|
12400
12970
|
} catch {
|
|
12401
|
-
throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${
|
|
12971
|
+
throw new HestiaError("state-not-ignored", `.hestia is not ignored in ${worktreeRoot}; add the line ".hestia/" to ${join23(worktreeRoot, ".gitignore")}`, { remedy: `.hestia/`, path: join23(worktreeRoot, ".gitignore") });
|
|
12402
12972
|
}
|
|
12403
12973
|
}
|
|
12404
12974
|
async function prepareCompose(worktreeRoot, project, repo, branch, opts, configuredBaseFile) {
|
|
@@ -12415,7 +12985,7 @@ async function prepareCompose(worktreeRoot, project, repo, branch, opts, configu
|
|
|
12415
12985
|
services
|
|
12416
12986
|
});
|
|
12417
12987
|
ensureDir(hestiaDir(worktreeRoot));
|
|
12418
|
-
const overridePath =
|
|
12988
|
+
const overridePath = join23(hestiaDir(worktreeRoot), OVERRIDE_FILE);
|
|
12419
12989
|
writeAtomicTextFile(overridePath, yaml);
|
|
12420
12990
|
return {
|
|
12421
12991
|
ctx: {
|
|
@@ -12448,7 +13018,7 @@ function writeDockerfileComposeModel(worktreeRoot, workloads, conventionalCompos
|
|
|
12448
13018
|
...conventionalComposeFile === undefined ? {} : { include: [{ path: conventionalComposeFile }] },
|
|
12449
13019
|
services
|
|
12450
13020
|
};
|
|
12451
|
-
const path =
|
|
13021
|
+
const path = join23(hestiaDir(worktreeRoot), DOCKERFILE_COMPOSE_FILE);
|
|
12452
13022
|
writeAtomicTextFile(path, $stringify(model));
|
|
12453
13023
|
return path;
|
|
12454
13024
|
}
|
|
@@ -12468,7 +13038,7 @@ function freshRecord(project, repoId, repo, branch, worktree) {
|
|
|
12468
13038
|
};
|
|
12469
13039
|
}
|
|
12470
13040
|
function assertCurrentStackIdentity(record, current) {
|
|
12471
|
-
const path =
|
|
13041
|
+
const path = join23(hestiaDir(current.worktreeRoot), "stack.json");
|
|
12472
13042
|
assertMutableStackRecord(record, path);
|
|
12473
13043
|
const matches = record.repoId === current.repoId && record.repo === current.repo && record.branch === current.branch && resolve2(record.worktree) === resolve2(current.worktreeRoot) && record.project === projectName(current.repoId, current.repo, current.branch, current.worktreeRoot);
|
|
12474
13044
|
if (!matches) {
|
|
@@ -12603,6 +13173,27 @@ function applyLocalRouteProjection(record) {
|
|
|
12603
13173
|
record.env[directUrlKey(endpoint.name)] = endpoint.url;
|
|
12604
13174
|
record.env[localUrlKey(endpoint.name)] = endpoint.localUrl;
|
|
12605
13175
|
}
|
|
13176
|
+
syncSharedProjection(record);
|
|
13177
|
+
}
|
|
13178
|
+
function syncSharedProjection(record) {
|
|
13179
|
+
for (const shared of listSharedHostnames()) {
|
|
13180
|
+
const url = `https://${shared.hostname}${shared.path ?? ""}`;
|
|
13181
|
+
if (shared.holder?.project === record.project) {
|
|
13182
|
+
const endpoint = record.endpoints.find((candidate) => (candidate.alias ?? candidate.name) === shared.service);
|
|
13183
|
+
if (endpoint !== undefined) {
|
|
13184
|
+
endpoint.publicUrl = url;
|
|
13185
|
+
record.env[urlKey(shared.service)] = url;
|
|
13186
|
+
}
|
|
13187
|
+
continue;
|
|
13188
|
+
}
|
|
13189
|
+
for (const endpoint of record.endpoints) {
|
|
13190
|
+
if (endpoint.publicUrl === url)
|
|
13191
|
+
delete endpoint.publicUrl;
|
|
13192
|
+
}
|
|
13193
|
+
if (record.env[urlKey(shared.service)] === url) {
|
|
13194
|
+
delete record.env[urlKey(shared.service)];
|
|
13195
|
+
}
|
|
13196
|
+
}
|
|
12606
13197
|
}
|
|
12607
13198
|
function syncExposures(record) {
|
|
12608
13199
|
const t = record.tunnel;
|
|
@@ -12627,7 +13218,7 @@ function matchesExpectedStack(record, expected) {
|
|
|
12627
13218
|
return record !== null && (record.repoId === undefined || record.repoId === expected.repoId) && record.worktree === expected.worktree && record.createdAt === expected.createdAt;
|
|
12628
13219
|
}
|
|
12629
13220
|
function projectMutationRoot(project) {
|
|
12630
|
-
return
|
|
13221
|
+
return join23(hestiaHome(), "project-locks", project);
|
|
12631
13222
|
}
|
|
12632
13223
|
async function assertNamedTunnelDns(hostname, tunnelUuid) {
|
|
12633
13224
|
const expected = `${tunnelUuid}.cfargotunnel.com`;
|
|
@@ -13033,6 +13624,7 @@ class ComposeEngine {
|
|
|
13033
13624
|
const { repo, repoId, branch, worktreeRoot } = currentIdentity;
|
|
13034
13625
|
let tunnel;
|
|
13035
13626
|
let tunnelDirty = false;
|
|
13627
|
+
const stoppedAliases = [];
|
|
13036
13628
|
const project = readState(worktreeRoot)?.project ?? projectName(repoId, repo, branch, worktreeRoot);
|
|
13037
13629
|
await withLock(worktreeRoot, () => withLock(projectMutationRoot(project), async () => {
|
|
13038
13630
|
const record = readState(worktreeRoot);
|
|
@@ -13041,6 +13633,11 @@ class ComposeEngine {
|
|
|
13041
13633
|
if (record.services.some((service) => service.name === name && service.backend === "docker")) {
|
|
13042
13634
|
throw new HestiaError("backend-not-stoppable", `Docker workload ${name} cannot be stopped individually; use hestia down`);
|
|
13043
13635
|
}
|
|
13636
|
+
for (const endpoint of record.endpoints) {
|
|
13637
|
+
if ((endpoint.workload ?? endpoint.name) === name) {
|
|
13638
|
+
stoppedAliases.push(endpoint.alias ?? endpoint.name);
|
|
13639
|
+
}
|
|
13640
|
+
}
|
|
13044
13641
|
}
|
|
13045
13642
|
const pf = readPidfile(worktreeRoot, name);
|
|
13046
13643
|
if (pf !== null) {
|
|
@@ -13086,6 +13683,9 @@ class ComposeEngine {
|
|
|
13086
13683
|
}
|
|
13087
13684
|
}
|
|
13088
13685
|
}));
|
|
13686
|
+
for (const alias of new Set(stoppedAliases)) {
|
|
13687
|
+
await this.#releaseSharedHoldings(project, alias);
|
|
13688
|
+
}
|
|
13089
13689
|
if (tunnelDirty)
|
|
13090
13690
|
await this.#reconcileAdopted(tunnel);
|
|
13091
13691
|
await this.#refreshLocalRoutes();
|
|
@@ -13221,12 +13821,12 @@ class ComposeEngine {
|
|
|
13221
13821
|
await stopProcTree(pf);
|
|
13222
13822
|
removePidfile(worktreeRoot, pf.name);
|
|
13223
13823
|
}
|
|
13224
|
-
|
|
13824
|
+
rmSync10(privateRegistryDir(worktreeRoot), { recursive: true, force: true });
|
|
13225
13825
|
const composeFile = record?.composeFile ?? tryLoadConfig(worktreeRoot)?.composeFile;
|
|
13226
13826
|
if (composeFile !== undefined) {
|
|
13227
13827
|
const project2 = record?.project ?? projectName(repoId, repo, branch, worktreeRoot);
|
|
13228
|
-
const overrideFile = record?.overrideFile ??
|
|
13229
|
-
if (
|
|
13828
|
+
const overrideFile = record?.overrideFile ?? join23(hestiaDir(worktreeRoot), OVERRIDE_FILE);
|
|
13829
|
+
if (existsSync21(overrideFile)) {
|
|
13230
13830
|
await composeDown({ project: project2, baseFile: composeFile, overrideFile, cwd: worktreeRoot }, opts?.destroy ?? false);
|
|
13231
13831
|
} else if (record?.composeFile !== undefined) {
|
|
13232
13832
|
const rest = ["compose", "-p", project2, "down", "--remove-orphans"];
|
|
@@ -13238,6 +13838,7 @@ class ComposeEngine {
|
|
|
13238
13838
|
const project = record?.project ?? projectName(repoId, repo, branch, worktreeRoot);
|
|
13239
13839
|
clearState(worktreeRoot, project);
|
|
13240
13840
|
await this.#releaseAdmission(project);
|
|
13841
|
+
await this.#releaseSharedHoldings(project);
|
|
13241
13842
|
}));
|
|
13242
13843
|
await this.#reconcileAdopted(tunnel);
|
|
13243
13844
|
await this.#refreshLocalRoutes();
|
|
@@ -13247,6 +13848,11 @@ class ComposeEngine {
|
|
|
13247
13848
|
if (j !== null)
|
|
13248
13849
|
await releaseSlot(j.port, project);
|
|
13249
13850
|
}
|
|
13851
|
+
async#releaseSharedHoldings(project, service) {
|
|
13852
|
+
const j = readDaemonJson();
|
|
13853
|
+
if (j !== null)
|
|
13854
|
+
await releaseSharedForProject(j.port, project, service);
|
|
13855
|
+
}
|
|
13250
13856
|
async downProject(project, opts) {
|
|
13251
13857
|
if (!/^[a-z0-9][a-z0-9-]{0,99}$/.test(project)) {
|
|
13252
13858
|
throw new HestiaError("usage", `invalid project name ${JSON.stringify(project)}`);
|
|
@@ -13289,9 +13895,9 @@ class ComposeEngine {
|
|
|
13289
13895
|
throw new HestiaError("compose-failed", `docker compose -p ${project} down failed: ${err.message}`);
|
|
13290
13896
|
}
|
|
13291
13897
|
}
|
|
13292
|
-
|
|
13898
|
+
rmSync10(mirrorDir(project), { recursive: true, force: true });
|
|
13293
13899
|
};
|
|
13294
|
-
if (record !== null &&
|
|
13900
|
+
if (record !== null && existsSync21(record.worktree)) {
|
|
13295
13901
|
const info = await getRepoInfo(record.worktree);
|
|
13296
13902
|
const identityMatches = resolve2(info.worktreeRoot) === resolve2(record.worktree) && (record.repoId === undefined ? record.repo === info.repo : record.repoId === info.repoId);
|
|
13297
13903
|
const localRecord = readState(record.worktree);
|
|
@@ -13312,6 +13918,7 @@ class ComposeEngine {
|
|
|
13312
13918
|
return;
|
|
13313
13919
|
}
|
|
13314
13920
|
await this.#releaseAdmission(project);
|
|
13921
|
+
await this.#releaseSharedHoldings(project);
|
|
13315
13922
|
await this.#reconcileAdopted(record.tunnel);
|
|
13316
13923
|
await this.#refreshLocalRoutes();
|
|
13317
13924
|
return;
|
|
@@ -13320,6 +13927,7 @@ class ComposeEngine {
|
|
|
13320
13927
|
const projectLockRoot = projectMutationRoot(project);
|
|
13321
13928
|
await withLock(projectLockRoot, teardownFromMirror);
|
|
13322
13929
|
await this.#releaseAdmission(project);
|
|
13930
|
+
await this.#releaseSharedHoldings(project);
|
|
13323
13931
|
await this.#reconcileAdopted(record?.tunnel);
|
|
13324
13932
|
await this.#refreshLocalRoutes();
|
|
13325
13933
|
}
|
|
@@ -13338,11 +13946,100 @@ class ComposeEngine {
|
|
|
13338
13946
|
}
|
|
13339
13947
|
await ensureDaemon();
|
|
13340
13948
|
await this.#refreshLocalRoutes(true);
|
|
13949
|
+
if (opts?.shared !== undefined) {
|
|
13950
|
+
if (tunnelName === undefined) {
|
|
13951
|
+
throw new HestiaError("shared-requires-named-tunnel", "shared hostnames need an adopted named tunnel (stable DNS is the point) \u2014 pass --tunnel <name>");
|
|
13952
|
+
}
|
|
13953
|
+
return this.#exposeShared(worktreeRoot, services, tunnelName, opts);
|
|
13954
|
+
}
|
|
13341
13955
|
if (tunnelName === undefined) {
|
|
13342
13956
|
return this.#exposeQuick(worktreeRoot, services, opts);
|
|
13343
13957
|
}
|
|
13344
13958
|
return this.#exposeNamed(worktreeRoot, services, tunnelName, opts);
|
|
13345
13959
|
}
|
|
13960
|
+
async#exposeShared(worktreeRoot, services, tunnelName, opts) {
|
|
13961
|
+
if (services.length !== 1) {
|
|
13962
|
+
throw new HestiaError("usage", "--shared declares exactly one service per shared hostname");
|
|
13963
|
+
}
|
|
13964
|
+
const name = opts.shared;
|
|
13965
|
+
assertSharedName(name);
|
|
13966
|
+
const project = readState(worktreeRoot)?.project;
|
|
13967
|
+
if (project === undefined) {
|
|
13968
|
+
throw new HestiaError("service-not-found", "no stack in this worktree \u2014 `hestia up`/`run` something first");
|
|
13969
|
+
}
|
|
13970
|
+
const adopted = await adoptTunnel(tunnelName);
|
|
13971
|
+
if (adopted.connections > 0 && !isAdopted(adopted.uuid) && !opts?.force) {
|
|
13972
|
+
throw new HestiaError("tunnel-busy", `tunnel "${tunnelName}" already has ${adopted.connections} live edge ` + `connection(s) from a foreign connector \u2014 stop the other cloudflared first (hestia's ` + `connector serves your static hostnames too), or pass --force to ` + `accept nondeterministic routing`);
|
|
13973
|
+
}
|
|
13974
|
+
const preflightRecord = readState(worktreeRoot);
|
|
13975
|
+
if (preflightRecord === null) {
|
|
13976
|
+
throw new HestiaError("service-not-found", "stack disappeared while preparing the shared hostname");
|
|
13977
|
+
}
|
|
13978
|
+
const baseRules = importBaseRules(adopted.uuid, tunnelName);
|
|
13979
|
+
const zoneHint = opts?.zone ?? preflightRecord.tunnel?.zone ?? inferZone(baseRules);
|
|
13980
|
+
let hostname;
|
|
13981
|
+
let zone;
|
|
13982
|
+
if (opts?.hostname !== undefined) {
|
|
13983
|
+
hostname = opts.hostname.toLowerCase();
|
|
13984
|
+
assertSharedHostnameFqdn(hostname);
|
|
13985
|
+
zone = zoneOf(hostname) ?? zoneHint ?? hostname;
|
|
13986
|
+
} else {
|
|
13987
|
+
if (zoneHint === undefined) {
|
|
13988
|
+
throw new HestiaError("usage", "cannot infer a zone from the tunnel's existing rules \u2014 pass --zone or an explicit --hostname");
|
|
13989
|
+
}
|
|
13990
|
+
zone = zoneHint;
|
|
13991
|
+
hostname = `${name}.${zone}`;
|
|
13992
|
+
}
|
|
13993
|
+
const path = normalizeSharedPath(opts?.path);
|
|
13994
|
+
const selection = resolveEndpointSelection(preflightRecord, services[0]);
|
|
13995
|
+
if (selection.endpoint.kind !== undefined && selection.endpoint.kind !== "http") {
|
|
13996
|
+
throw new HestiaError("usage", `public tunnels require an HTTP endpoint; ${services[0]} is ${selection.endpoint.kind}`);
|
|
13997
|
+
}
|
|
13998
|
+
const alias = selection.endpoint.alias ?? selection.endpoint.name;
|
|
13999
|
+
if (baseRules.some((rule) => rule.hostname === hostname)) {
|
|
14000
|
+
throw new HestiaError("hostname-conflict", `shared hostname ${hostname} is already claimed by the user's static rules`);
|
|
14001
|
+
}
|
|
14002
|
+
const dynamicConflict = collectDynamicRules(adopted.uuid).rules.find((rule) => rule.hostname === hostname);
|
|
14003
|
+
if (dynamicConflict !== undefined) {
|
|
14004
|
+
throw new HestiaError("hostname-conflict", `shared hostname ${hostname} is already claimed by project ${dynamicConflict.project}`);
|
|
14005
|
+
}
|
|
14006
|
+
await assertNamedTunnelDns(hostname, adopted.uuid);
|
|
14007
|
+
await declareSharedHostname({
|
|
14008
|
+
name,
|
|
14009
|
+
hostname,
|
|
14010
|
+
path,
|
|
14011
|
+
tunnelUuid: adopted.uuid,
|
|
14012
|
+
zone,
|
|
14013
|
+
service: alias
|
|
14014
|
+
});
|
|
14015
|
+
const handle = await ensureDaemon();
|
|
14016
|
+
const claim = await sharedVerb(handle.port, "claim", {
|
|
14017
|
+
name,
|
|
14018
|
+
project,
|
|
14019
|
+
worktree: worktreeRoot,
|
|
14020
|
+
waitMs: 0
|
|
14021
|
+
});
|
|
14022
|
+
if (!claim.granted) {
|
|
14023
|
+
throw new HestiaError("shared-held", `shared hostname "${name}" is held by ${claim.holder?.project ?? "another stack"} ` + `(${claim.queued.length} queued) \u2014 \`hestia claim ${name} --wait\` to queue for it`);
|
|
14024
|
+
}
|
|
14025
|
+
const outcome = await reconcileTunnel({ name: tunnelName, uuid: adopted.uuid, credFile: adopted.credFile }, { force: opts?.force, readyTimeoutMs: opts?.readyTimeoutMs });
|
|
14026
|
+
for (const w of outcome.warnings)
|
|
14027
|
+
process.stderr.write(`warning: ${w}
|
|
14028
|
+
`);
|
|
14029
|
+
const final = await withLock(worktreeRoot, () => withLock(projectMutationRoot(project), async () => {
|
|
14030
|
+
const record = readState(worktreeRoot);
|
|
14031
|
+
if (record === null) {
|
|
14032
|
+
throw new HestiaError("service-not-found", "stack disappeared while exposing (concurrent down?)");
|
|
14033
|
+
}
|
|
14034
|
+
applyLocalRouteProjection(record);
|
|
14035
|
+
writeState(worktreeRoot, record);
|
|
14036
|
+
return record;
|
|
14037
|
+
}));
|
|
14038
|
+
if (outcome.error !== undefined)
|
|
14039
|
+
throw outcome.error;
|
|
14040
|
+
await this.#refreshLocalRoutes(true);
|
|
14041
|
+
return final;
|
|
14042
|
+
}
|
|
13346
14043
|
async#exposeQuick(worktreeRoot, services, opts) {
|
|
13347
14044
|
const project = readState(worktreeRoot)?.project;
|
|
13348
14045
|
if (project === undefined) {
|
|
@@ -13361,7 +14058,7 @@ class ComposeEngine {
|
|
|
13361
14058
|
const alias = selection.endpoint.alias ?? selection.endpoint.name;
|
|
13362
14059
|
const authority = internalEndpointAuthority(record.project, alias);
|
|
13363
14060
|
const name = `aux-quick-${createHash7("sha256").update(alias).digest("hex").slice(0, 10)}`;
|
|
13364
|
-
const quickCfg =
|
|
14061
|
+
const quickCfg = join23(hestiaDir(worktreeRoot), `quick-${name}.yml`);
|
|
13365
14062
|
writeAtomicTextFile(quickCfg, `ingress:
|
|
13366
14063
|
- service: ${JSON.stringify(`unix:${publicGatewaySocketPath()}`)}
|
|
13367
14064
|
` + ` originRequest:
|
|
@@ -13553,6 +14250,80 @@ class ComposeEngine {
|
|
|
13553
14250
|
await this.#refreshLocalRoutes(true);
|
|
13554
14251
|
return final;
|
|
13555
14252
|
}
|
|
14253
|
+
async#sharedContext(cwd, name) {
|
|
14254
|
+
assertSharedName(name);
|
|
14255
|
+
const { worktreeRoot } = await getRepoInfo(cwd);
|
|
14256
|
+
const record = readState(worktreeRoot);
|
|
14257
|
+
if (record === null) {
|
|
14258
|
+
throw new HestiaError("service-not-found", "no stack in this worktree \u2014 `hestia up`/`run` something first");
|
|
14259
|
+
}
|
|
14260
|
+
const shared = readSharedHostname(name);
|
|
14261
|
+
if (shared === null) {
|
|
14262
|
+
throw new HestiaError("shared-not-found", `no shared hostname "${name}" is declared \u2014 \`hestia expose <svc> --shared ${name}\` creates one`);
|
|
14263
|
+
}
|
|
14264
|
+
return { worktreeRoot, record, shared };
|
|
14265
|
+
}
|
|
14266
|
+
async#applySharedProjection(worktreeRoot, project) {
|
|
14267
|
+
return withLock(worktreeRoot, () => withLock(projectMutationRoot(project), async () => {
|
|
14268
|
+
const record = readState(worktreeRoot);
|
|
14269
|
+
if (record === null) {
|
|
14270
|
+
throw new HestiaError("service-not-found", "stack disappeared during the shared-hostname transition");
|
|
14271
|
+
}
|
|
14272
|
+
applyLocalRouteProjection(record);
|
|
14273
|
+
writeState(worktreeRoot, record);
|
|
14274
|
+
return record;
|
|
14275
|
+
}));
|
|
14276
|
+
}
|
|
14277
|
+
async claimShared(cwd, name, opts) {
|
|
14278
|
+
const { worktreeRoot, record, shared } = await this.#sharedContext(cwd, name);
|
|
14279
|
+
const endpoint = record.endpoints.find((candidate) => (candidate.alias ?? candidate.name) === shared.service);
|
|
14280
|
+
if (endpoint === undefined) {
|
|
14281
|
+
throw new HestiaError("service-not-found", `shared hostname "${name}" routes to endpoint alias "${shared.service}", which this ` + `stack does not expose \u2014 \`hestia up\`/\`run\` it first`);
|
|
14282
|
+
}
|
|
14283
|
+
if (endpoint.kind !== undefined && endpoint.kind !== "http") {
|
|
14284
|
+
throw new HestiaError("usage", `shared hostnames require an HTTP endpoint; ${shared.service} is ${endpoint.kind}`);
|
|
14285
|
+
}
|
|
14286
|
+
const handle = await ensureDaemon();
|
|
14287
|
+
const result = await sharedVerb(handle.port, "claim", {
|
|
14288
|
+
name,
|
|
14289
|
+
project: record.project,
|
|
14290
|
+
worktree: worktreeRoot,
|
|
14291
|
+
waitMs: opts?.waitMs ?? 0
|
|
14292
|
+
});
|
|
14293
|
+
if (!result.granted) {
|
|
14294
|
+
const position = result.queued.findIndex((waiter) => waiter.project === record.project) + 1;
|
|
14295
|
+
throw new HestiaError("shared-held", `shared hostname "${name}" is held by ${result.holder?.project ?? "another stack"}` + (position > 0 ? ` \u2014 queued durably at position ${position}/${result.queued.length}; the holder can ` + `\`hestia share allow ${name}\`, or re-run \`hestia claim ${name} --wait\` to keep waiting` : ""), { holder: result.holder, queued: result.queued });
|
|
14296
|
+
}
|
|
14297
|
+
const updated = await this.#applySharedProjection(worktreeRoot, record.project);
|
|
14298
|
+
await this.#refreshLocalRoutes();
|
|
14299
|
+
return { record: updated, result };
|
|
14300
|
+
}
|
|
14301
|
+
async releaseShared(cwd, name) {
|
|
14302
|
+
const { worktreeRoot, record } = await this.#sharedContext(cwd, name);
|
|
14303
|
+
const handle = await ensureDaemon();
|
|
14304
|
+
await sharedVerb(handle.port, "release", { name, project: record.project });
|
|
14305
|
+
const updated = await this.#applySharedProjection(worktreeRoot, record.project);
|
|
14306
|
+
await this.#refreshLocalRoutes();
|
|
14307
|
+
return updated;
|
|
14308
|
+
}
|
|
14309
|
+
async cancelSharedClaim(cwd, name) {
|
|
14310
|
+
const { record } = await this.#sharedContext(cwd, name);
|
|
14311
|
+
const handle = await ensureDaemon();
|
|
14312
|
+
await sharedVerb(handle.port, "cancel", { name, project: record.project });
|
|
14313
|
+
}
|
|
14314
|
+
async allowShared(cwd, name) {
|
|
14315
|
+
const { worktreeRoot, record } = await this.#sharedContext(cwd, name);
|
|
14316
|
+
const handle = await ensureDaemon();
|
|
14317
|
+
await sharedVerb(handle.port, "allow", { name, project: record.project });
|
|
14318
|
+
const updated = await this.#applySharedProjection(worktreeRoot, record.project);
|
|
14319
|
+
await this.#refreshLocalRoutes();
|
|
14320
|
+
return updated;
|
|
14321
|
+
}
|
|
14322
|
+
async denyShared(cwd, name) {
|
|
14323
|
+
const { record } = await this.#sharedContext(cwd, name);
|
|
14324
|
+
const handle = await ensureDaemon();
|
|
14325
|
+
await sharedVerb(handle.port, "deny", { name, project: record.project });
|
|
14326
|
+
}
|
|
13556
14327
|
async status(cwd) {
|
|
13557
14328
|
const { worktreeRoot } = await getRepoInfo(cwd);
|
|
13558
14329
|
const record = readState(worktreeRoot);
|
|
@@ -13672,6 +14443,14 @@ function startDuties(admission, opts) {
|
|
|
13672
14443
|
} catch (err) {
|
|
13673
14444
|
log(`sweep: pump failed: ${err.message}`);
|
|
13674
14445
|
}
|
|
14446
|
+
if (opts?.shared !== undefined) {
|
|
14447
|
+
try {
|
|
14448
|
+
const state = admission.healthSnapshot();
|
|
14449
|
+
await opts.shared.sweep(new Set([...state.live, ...state.reserved]));
|
|
14450
|
+
} catch (err) {
|
|
14451
|
+
log(`sweep: shared-hostname sweep failed: ${err.message}`);
|
|
14452
|
+
}
|
|
14453
|
+
}
|
|
13675
14454
|
for (const uuid of listAdopted()) {
|
|
13676
14455
|
try {
|
|
13677
14456
|
const pf = connectorPidfile(uuid);
|
|
@@ -13729,6 +14508,7 @@ async function main() {
|
|
|
13729
14508
|
const engine2 = new ComposeEngine;
|
|
13730
14509
|
const localRouter = new HestiaLocalHttpRouter;
|
|
13731
14510
|
const routerPort = await localRouter.start();
|
|
14511
|
+
const shared = new SharedArbiter(() => localRouter.refreshRoutes());
|
|
13732
14512
|
const token = randomBytes(32).toString("hex");
|
|
13733
14513
|
const server = serveSessionBrokerDaemon({
|
|
13734
14514
|
daemon,
|
|
@@ -13739,6 +14519,7 @@ async function main() {
|
|
|
13739
14519
|
fleet,
|
|
13740
14520
|
routerPort,
|
|
13741
14521
|
gatewaySocket: localRouter.socketPath,
|
|
14522
|
+
shared,
|
|
13742
14523
|
refreshLocalRoutes: () => localRouter.refreshRoutes(),
|
|
13743
14524
|
logsProject: (project, options) => engine2.logsProject(project, options)
|
|
13744
14525
|
})
|
|
@@ -13755,11 +14536,11 @@ async function main() {
|
|
|
13755
14536
|
port: "none",
|
|
13756
14537
|
backend: "proc"
|
|
13757
14538
|
}),
|
|
13758
|
-
logPath:
|
|
14539
|
+
logPath: join24(root, "daemon.log"),
|
|
13759
14540
|
signal: "term",
|
|
13760
14541
|
backend: "proc"
|
|
13761
14542
|
});
|
|
13762
|
-
writeAtomicJsonFile(
|
|
14543
|
+
writeAtomicJsonFile(join24(root, "daemon.json"), {
|
|
13763
14544
|
schemaVersion: STATE_SCHEMA_VERSION,
|
|
13764
14545
|
pid: process.pid,
|
|
13765
14546
|
port: server.port,
|
|
@@ -13769,13 +14550,13 @@ async function main() {
|
|
|
13769
14550
|
routerPort,
|
|
13770
14551
|
gatewaySocket: localRouter.socketPath
|
|
13771
14552
|
}, { mode: 384 });
|
|
13772
|
-
const stopDuties = startDuties(admission);
|
|
14553
|
+
const stopDuties = startDuties(admission, { shared });
|
|
13773
14554
|
const shutdown = () => {
|
|
13774
14555
|
stopDuties();
|
|
13775
14556
|
fleet.stop();
|
|
13776
14557
|
localRouter.stop();
|
|
13777
14558
|
removePidfile(root, PIDFILE_NAME2);
|
|
13778
|
-
|
|
14559
|
+
rmSync11(join24(root, "daemon.json"), { force: true });
|
|
13779
14560
|
server.stop(true);
|
|
13780
14561
|
process.exit(0);
|
|
13781
14562
|
};
|
|
@@ -13790,4 +14571,4 @@ async function main() {
|
|
|
13790
14571
|
}
|
|
13791
14572
|
main();
|
|
13792
14573
|
|
|
13793
|
-
//# debugId=
|
|
14574
|
+
//# debugId=9176C6E8002FF49E64756E2164756E21
|