jishushell 0.4.30 → 0.5.22
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/Dockerfile.hermes-slim +2 -5
- package/apps/anythingllm-container.yaml +287 -0
- package/apps/browserless-chromium-container.yaml +18 -6
- package/apps/filebrowser-container.yaml +164 -0
- package/apps/ollama-binary.yaml +44 -0
- package/apps/ollama-with-hollama-binary.yaml +45 -1
- package/apps/openclaw-binary.yaml +8 -0
- package/apps/openclaw-container.yaml +9 -1
- package/apps/openclaw-with-searxng-container.yaml +4 -0
- package/apps/searxng-container.yaml +5 -4
- package/apps/weknora-container.yaml +471 -0
- package/dist/cli/doctor.js +144 -16
- package/dist/cli/doctor.js.map +1 -1
- package/dist/cli/panel.js.map +1 -1
- package/dist/config.d.ts +19 -0
- package/dist/config.js +99 -1
- package/dist/config.js.map +1 -1
- package/dist/install.js +4 -4
- package/dist/install.js.map +1 -1
- package/dist/routes/auth.js +2 -2
- package/dist/routes/auth.js.map +1 -1
- package/dist/routes/backup.js +64 -11
- package/dist/routes/backup.js.map +1 -1
- package/dist/routes/external-mounts.d.ts +17 -0
- package/dist/routes/external-mounts.js +73 -0
- package/dist/routes/external-mounts.js.map +1 -0
- package/dist/routes/file-mounts.d.ts +13 -0
- package/dist/routes/file-mounts.js +90 -0
- package/dist/routes/file-mounts.js.map +1 -0
- package/dist/routes/files-organize.d.ts +28 -0
- package/dist/routes/files-organize.js +167 -0
- package/dist/routes/files-organize.js.map +1 -0
- package/dist/routes/files.d.ts +31 -0
- package/dist/routes/files.js +321 -0
- package/dist/routes/files.js.map +1 -0
- package/dist/routes/instances.js +87 -12
- package/dist/routes/instances.js.map +1 -1
- package/dist/routes/internal.d.ts +2 -0
- package/dist/routes/internal.js +59 -0
- package/dist/routes/internal.js.map +1 -0
- package/dist/routes/llm.js +29 -0
- package/dist/routes/llm.js.map +1 -1
- package/dist/routes/setup.js +9 -9
- package/dist/routes/setup.js.map +1 -1
- package/dist/routes/system.js +1 -1
- package/dist/routes/system.js.map +1 -1
- package/dist/routes/webdav.d.ts +17 -0
- package/dist/routes/webdav.js +114 -0
- package/dist/routes/webdav.js.map +1 -0
- package/dist/server.js +358 -6
- package/dist/server.js.map +1 -1
- package/dist/services/agent-apps/catalog.d.ts +3 -0
- package/dist/services/agent-apps/catalog.js +40 -13
- package/dist/services/agent-apps/catalog.js.map +1 -1
- package/dist/services/agent-apps/installers/shell-script.d.ts +1 -1
- package/dist/services/agent-apps/installers/shell-script.js +19 -2
- package/dist/services/agent-apps/installers/shell-script.js.map +1 -1
- package/dist/services/agent-apps/types.d.ts +3 -0
- package/dist/services/app/app-compiler.d.ts +1 -1
- package/dist/services/app/app-compiler.js +5 -5
- package/dist/services/app/app-compiler.js.map +1 -1
- package/dist/services/app/app-manager.d.ts +9 -0
- package/dist/services/app/app-manager.js +248 -43
- package/dist/services/app/app-manager.js.map +1 -1
- package/dist/services/app/custom-manager.js.map +1 -1
- package/dist/services/app/hermes-agent-manager.js +1 -0
- package/dist/services/app/hermes-agent-manager.js.map +1 -1
- package/dist/services/app/ollama-manager.js +1 -1
- package/dist/services/app/ollama-manager.js.map +1 -1
- package/dist/services/app/openclaw-manager.js +37 -5
- package/dist/services/app/openclaw-manager.js.map +1 -1
- package/dist/services/app/platform-transform.d.ts +32 -0
- package/dist/services/app/platform-transform.js +65 -0
- package/dist/services/app/platform-transform.js.map +1 -0
- package/dist/services/app-passwords.d.ts +61 -0
- package/dist/services/app-passwords.js +173 -0
- package/dist/services/app-passwords.js.map +1 -0
- package/dist/services/backup-manager.d.ts +11 -0
- package/dist/services/backup-manager.js +220 -8
- package/dist/services/backup-manager.js.map +1 -1
- package/dist/services/capability-endpoint-validator.js +26 -7
- package/dist/services/capability-endpoint-validator.js.map +1 -1
- package/dist/services/connection-apply.d.ts +2 -0
- package/dist/services/connection-apply.js +55 -1
- package/dist/services/connection-apply.js.map +1 -1
- package/dist/services/connection-resolver.js +1 -1
- package/dist/services/connection-resolver.js.map +1 -1
- package/dist/services/connection-transactor.d.ts +2 -0
- package/dist/services/connection-transactor.js +12 -2
- package/dist/services/connection-transactor.js.map +1 -1
- package/dist/services/external-mounts.d.ts +40 -0
- package/dist/services/external-mounts.js +187 -0
- package/dist/services/external-mounts.js.map +1 -0
- package/dist/services/files-manager.d.ts +252 -0
- package/dist/services/files-manager.js +1075 -0
- package/dist/services/files-manager.js.map +1 -0
- package/dist/services/files-mounts.d.ts +42 -0
- package/dist/services/files-mounts.js +207 -0
- package/dist/services/files-mounts.js.map +1 -0
- package/dist/services/instance-manager.js +90 -32
- package/dist/services/instance-manager.js.map +1 -1
- package/dist/services/llm-proxy/index.d.ts +28 -0
- package/dist/services/llm-proxy/index.js +76 -3
- package/dist/services/llm-proxy/index.js.map +1 -1
- package/dist/services/llm-proxy/ssrf.js +6 -2
- package/dist/services/llm-proxy/ssrf.js.map +1 -1
- package/dist/services/llm-proxy/validate-key.d.ts +41 -0
- package/dist/services/llm-proxy/validate-key.js +672 -0
- package/dist/services/llm-proxy/validate-key.js.map +1 -0
- package/dist/services/macos-launchd.d.ts +89 -0
- package/dist/services/macos-launchd.js +273 -0
- package/dist/services/macos-launchd.js.map +1 -0
- package/dist/services/nomad-manager.d.ts +11 -0
- package/dist/services/nomad-manager.js +343 -98
- package/dist/services/nomad-manager.js.map +1 -1
- package/dist/services/organize/applier.d.ts +46 -0
- package/dist/services/organize/applier.js +218 -0
- package/dist/services/organize/applier.js.map +1 -0
- package/dist/services/organize/rules.d.ts +57 -0
- package/dist/services/organize/rules.js +286 -0
- package/dist/services/organize/rules.js.map +1 -0
- package/dist/services/organize/scanner.d.ts +50 -0
- package/dist/services/organize/scanner.js +366 -0
- package/dist/services/organize/scanner.js.map +1 -0
- package/dist/services/organize/store.d.ts +14 -0
- package/dist/services/organize/store.js +82 -0
- package/dist/services/organize/store.js.map +1 -0
- package/dist/services/panel-manager.js +40 -11
- package/dist/services/panel-manager.js.map +1 -1
- package/dist/services/process-manager.js +3 -2
- package/dist/services/process-manager.js.map +1 -1
- package/dist/services/runtime/adapters/custom.js +56 -0
- package/dist/services/runtime/adapters/custom.js.map +1 -1
- package/dist/services/runtime/adapters/hermes.d.ts +4 -3
- package/dist/services/runtime/adapters/hermes.js +166 -64
- package/dist/services/runtime/adapters/hermes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw-routes.d.ts +8 -2
- package/dist/services/runtime/adapters/openclaw-routes.js +68 -0
- package/dist/services/runtime/adapters/openclaw-routes.js.map +1 -1
- package/dist/services/runtime/adapters/openclaw.d.ts +118 -0
- package/dist/services/runtime/adapters/openclaw.js +1459 -49
- package/dist/services/runtime/adapters/openclaw.js.map +1 -1
- package/dist/services/runtime/instance.d.ts +1 -1
- package/dist/services/runtime/instance.js +1 -1
- package/dist/services/runtime/instance.js.map +1 -1
- package/dist/services/runtime/mcp-shims/anythingllm-shim.d.ts +46 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js +281 -0
- package/dist/services/runtime/mcp-shims/anythingllm-shim.js.map +1 -0
- package/dist/services/runtime/mcp-shims/drive-shim.d.ts +54 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js +489 -0
- package/dist/services/runtime/mcp-shims/drive-shim.js.map +1 -0
- package/dist/services/runtime/types.d.ts +31 -0
- package/dist/services/setup-manager.js +190 -68
- package/dist/services/setup-manager.js.map +1 -1
- package/dist/services/suggestions.js.map +1 -1
- package/dist/services/update-manager.js +32 -14
- package/dist/services/update-manager.js.map +1 -1
- package/dist/services/webdav/server.d.ts +24 -0
- package/dist/services/webdav/server.js +420 -0
- package/dist/services/webdav/server.js.map +1 -0
- package/dist/services/webdav/xml-builder.d.ts +73 -0
- package/dist/services/webdav/xml-builder.js +156 -0
- package/dist/services/webdav/xml-builder.js.map +1 -0
- package/dist/services/workspace-builder.d.ts +29 -0
- package/dist/services/workspace-builder.js +188 -0
- package/dist/services/workspace-builder.js.map +1 -0
- package/dist/types.d.ts +61 -0
- package/dist/utils/path-locks.d.ts +30 -0
- package/dist/utils/path-locks.js +63 -0
- package/dist/utils/path-locks.js.map +1 -0
- package/dist/utils/path-safety.d.ts +41 -0
- package/dist/utils/path-safety.js +119 -0
- package/dist/utils/path-safety.js.map +1 -0
- package/dist/utils/safe-write.d.ts +24 -0
- package/dist/utils/safe-write.js +82 -0
- package/dist/utils/safe-write.js.map +1 -0
- package/install/jishu-install.sh +247 -35
- package/install/jishu-uninstall.sh +45 -5
- package/package.json +20 -2
- package/public/assets/ApiKeyField-CvyAOcJS.js +1 -0
- package/public/assets/Dashboard-AuJESBlJ.js +1 -0
- package/public/assets/{HermesChatPanel-_GHoklgo.js → HermesChatPanel-CByPREwb.js} +1 -1
- package/public/assets/HermesConfigForm-DRda8FKX.js +4 -0
- package/public/assets/InitPassword-ka4wNpM5.js +1 -0
- package/public/assets/InstanceDetail-Cg1nS8HX.js +92 -0
- package/public/assets/Login-aPajuQzf.js +1 -0
- package/public/assets/NewInstance-Dd1ebNIx.js +1 -0
- package/public/assets/ProviderRecommendations-DFmADQ7V.js +1 -0
- package/public/assets/Settings-BYQnbLYL.js +1 -0
- package/public/assets/Setup-D05lwDOV.js +1 -0
- package/public/assets/WeixinLoginPanel-D89kdhP4.js +9 -0
- package/public/assets/index-HSXCsceK.css +1 -0
- package/public/assets/index-bnBu0nlQ.js +19 -0
- package/public/assets/registry-C_qeFTkZ.js +2 -0
- package/public/assets/usePolling-Bn93fe7M.js +1 -0
- package/public/assets/{vendor-i18n-ucpM0OR0.js → vendor-i18n-flxcMVeP.js} +2 -2
- package/public/assets/{vendor-react-Bk1hRGiY.js → vendor-react-ZC5T_huj.js} +7 -7
- package/public/index.html +4 -4
- package/scripts/check-app-spec.mjs +18 -4
- package/scripts/check-colima-launchd.mjs +230 -0
- package/scripts/check-new-file-tests.mjs +230 -0
- package/scripts/check-quarantine-expiry.mjs +105 -0
- package/scripts/perf/README.md +49 -0
- package/scripts/perf/auth.js +99 -0
- package/scripts/perf/config.js +63 -0
- package/scripts/perf/instances.js +143 -0
- package/scripts/perf/proxy.js +96 -0
- package/scripts/smoke/files-w1.sh +142 -0
- package/scripts/smoke-backend.mjs +122 -0
- package/scripts/smoke-post-publish.mjs +346 -0
- package/public/assets/Dashboard-rkWp-CXd.js +0 -1
- package/public/assets/HermesConfigForm-anDnwUp_.js +0 -4
- package/public/assets/InitPassword-ZU9_-hDr.js +0 -1
- package/public/assets/InstanceDetail-CN0FH1aw.js +0 -92
- package/public/assets/Login-BItXqYAJ.js +0 -1
- package/public/assets/NewInstance-BousE6kY.js +0 -1
- package/public/assets/ProviderRecommendations-DFYj7Fb6.js +0 -1
- package/public/assets/Settings-Bttc6QmM.js +0 -1
- package/public/assets/Setup-Bsxx1zgj.js +0 -1
- package/public/assets/WeixinLoginPanel-DPZpAKgO.js +0 -9
- package/public/assets/index-8xZy1z5k.css +0 -1
- package/public/assets/index-Dw3HhUYE.js +0 -19
- package/public/assets/input-paste-CrNVAyOy.js +0 -1
- package/public/assets/providers-DtNXh9JD.js +0 -1
- package/public/assets/registry-5s2UB6is.js +0 -2
- package/public/assets/usePolling-Do5Erqm_.js +0 -1
|
@@ -20,7 +20,7 @@ import { StringDecoder } from "string_decoder";
|
|
|
20
20
|
import { promisify } from "util";
|
|
21
21
|
import { parse } from "yaml";
|
|
22
22
|
import * as config from "../config.js";
|
|
23
|
-
import { getGatewayPort, getInstance, getInstanceRuntime, instanceMetaPath, getRuntimeEnv, isPortInUse, reallocateGatewayPort, } from "./instance-manager.js";
|
|
23
|
+
import { extractGatewayPort, getGatewayPort, getInstance, getInstanceRuntime, instanceMetaPath, getRuntimeEnv, isPortInUse, reallocateGatewayPort, } from "./instance-manager.js";
|
|
24
24
|
import { getAdapter, resolveAgentType } from "./runtime/index.js";
|
|
25
25
|
function getConfigValue(name) {
|
|
26
26
|
return name in config ? config[name] : undefined;
|
|
@@ -30,7 +30,7 @@ function resolveConfigPath(value, fallback) {
|
|
|
30
30
|
}
|
|
31
31
|
const JISHUSHELL_HOME = resolveConfigPath(getConfigValue("JISHUSHELL_HOME"), join(process.env.HOME ?? homedir(), ".jishushell"));
|
|
32
32
|
const APPS_DIR = resolveConfigPath(getConfigValue("APPS_DIR"), join(JISHUSHELL_HOME, "apps"));
|
|
33
|
-
const
|
|
33
|
+
const _INSTANCES_DIR = resolveConfigPath(getConfigValue("INSTANCES_DIR"), join(JISHUSHELL_HOME, "instances"));
|
|
34
34
|
const getNomadAddrValue = getConfigValue("getNomadAddr");
|
|
35
35
|
const getNomadDriverValue = getConfigValue("getNomadDriver");
|
|
36
36
|
const getNomadTokenValue = getConfigValue("getNomadToken");
|
|
@@ -441,13 +441,22 @@ function wrapNomadJob(jid, groupName, task) {
|
|
|
441
441
|
Count: 1,
|
|
442
442
|
...(groupNetworks ? { Networks: groupNetworks } : {}),
|
|
443
443
|
RestartPolicy: {
|
|
444
|
-
|
|
445
|
-
|
|
444
|
+
// 10 attempts × 15s delay = ~2.5min recovery window. Multi-task
|
|
445
|
+
// groups (e.g. weknora-app racing paradedb cold-init) commonly
|
|
446
|
+
// need 3-5 restarts before the dependency's external port-publish
|
|
447
|
+
// settles. Previous 3-attempt cap caused alloc-fail cascades on
|
|
448
|
+
// first-boot of stacks with DB sidecars.
|
|
449
|
+
Attempts: 10,
|
|
450
|
+
Interval: 600000000000,
|
|
446
451
|
Delay: 15000000000,
|
|
447
452
|
Mode: "fail",
|
|
448
453
|
},
|
|
449
454
|
Reschedule: {
|
|
450
|
-
|
|
455
|
+
// Allow Nomad to reschedule the whole alloc up to 3 times if all
|
|
456
|
+
// restarts fail (e.g. transient image pull or host reboot).
|
|
457
|
+
// Unlimited stays false to keep alloc churn bounded.
|
|
458
|
+
Attempts: 3,
|
|
459
|
+
Interval: 3600000000000,
|
|
451
460
|
Unlimited: false,
|
|
452
461
|
},
|
|
453
462
|
Update: {
|
|
@@ -531,24 +540,35 @@ async function injectConnectionsRuntimeEnv(instanceId, task) {
|
|
|
531
540
|
}
|
|
532
541
|
}
|
|
533
542
|
async function getRunningAlloc(instanceId) {
|
|
543
|
+
const allocs = await getAllocs(instanceId);
|
|
544
|
+
if (!allocs)
|
|
545
|
+
return null;
|
|
546
|
+
for (const status of ["running", "pending"]) {
|
|
547
|
+
for (const alloc of allocs) {
|
|
548
|
+
if (alloc.ClientStatus === status)
|
|
549
|
+
return alloc;
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
async function getAllocs(instanceId) {
|
|
534
555
|
const jid = jobId(instanceId);
|
|
535
556
|
try {
|
|
536
557
|
const resp = await nomadGet(`/v1/job/${jid}/allocations`);
|
|
537
558
|
if (resp.status === 404)
|
|
538
|
-
return
|
|
559
|
+
return [];
|
|
539
560
|
const allocs = await resp.json();
|
|
540
|
-
|
|
541
|
-
for (const alloc of allocs) {
|
|
542
|
-
if (alloc.ClientStatus === status)
|
|
543
|
-
return alloc;
|
|
544
|
-
}
|
|
545
|
-
}
|
|
546
|
-
return null;
|
|
561
|
+
return Array.isArray(allocs) ? allocs : [];
|
|
547
562
|
}
|
|
548
563
|
catch {
|
|
549
564
|
return null;
|
|
550
565
|
}
|
|
551
566
|
}
|
|
567
|
+
function latestAlloc(allocs) {
|
|
568
|
+
if (!allocs.length)
|
|
569
|
+
return null;
|
|
570
|
+
return [...allocs].sort((a, b) => ((b.ModifyIndex ?? b.CreateIndex ?? 0) - (a.ModifyIndex ?? a.CreateIndex ?? 0)))[0] ?? null;
|
|
571
|
+
}
|
|
552
572
|
// Returns true if the Nomad job exists and was NOT explicitly stopped by the user (Stop=false).
|
|
553
573
|
// Used on jishushell startup to auto-restart instances that were running before a reboot.
|
|
554
574
|
export async function shouldAutoStart(instanceId) {
|
|
@@ -595,7 +615,12 @@ export async function getStatus(instanceId) {
|
|
|
595
615
|
catch {
|
|
596
616
|
return { ...stopped, status: "unknown", error: "Nomad unreachable" };
|
|
597
617
|
}
|
|
598
|
-
const
|
|
618
|
+
const allocs = await getAllocs(instanceId);
|
|
619
|
+
if (allocs == null || allocs.length === 0)
|
|
620
|
+
return { ...stopped, status: "pending" };
|
|
621
|
+
const alloc = allocs.find((entry) => entry.ClientStatus === "running")
|
|
622
|
+
?? allocs.find((entry) => entry.ClientStatus === "pending")
|
|
623
|
+
?? latestAlloc(allocs);
|
|
599
624
|
if (!alloc)
|
|
600
625
|
return { ...stopped, status: "pending" };
|
|
601
626
|
const allocId = alloc.ID;
|
|
@@ -633,32 +658,19 @@ export async function getStatus(instanceId) {
|
|
|
633
658
|
}
|
|
634
659
|
}
|
|
635
660
|
catch { /* ignore */ }
|
|
636
|
-
// Fallback: Nomad cgroup stats are often zero on cgroup v2 (e.g. Raspberry
|
|
637
|
-
//
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
const raw = stdout.trim();
|
|
649
|
-
// Format: "499.6MiB / 3GiB" or "123.4MB / 2GB"
|
|
650
|
-
const match = raw.match(/^([\d.]+)\s*(MiB|GiB|MB|GB|KiB|KB)/i);
|
|
651
|
-
if (match) {
|
|
652
|
-
let mb = parseFloat(match[1]);
|
|
653
|
-
const unit = match[2].toLowerCase();
|
|
654
|
-
if (unit === "gib" || unit === "gb")
|
|
655
|
-
mb *= 1024;
|
|
656
|
-
else if (unit === "kib" || unit === "kb")
|
|
657
|
-
mb /= 1024;
|
|
658
|
-
result.memory_mb = Math.round(mb * 10) / 10;
|
|
659
|
-
}
|
|
661
|
+
// Fallback: Nomad cgroup stats are often zero on cgroup v2 (e.g. Raspberry
|
|
662
|
+
// Pi / CIX). Read from the shared, cached, single-flight `docker stats`
|
|
663
|
+
// snapshot instead of forking one `docker stats` per instance — see
|
|
664
|
+
// getDockerMemSnapshot for why per-instance forking was the cold-path cost.
|
|
665
|
+
if (!result.memory_mb && allocId && /^[a-f0-9-]+$/i.test(allocId)) {
|
|
666
|
+
const containerName = `${resolveTaskName(instanceId)}-${allocId}`;
|
|
667
|
+
const stat = (await getDockerMemSnapshot()).get(containerName);
|
|
668
|
+
if (stat) {
|
|
669
|
+
if (stat.memory_mb)
|
|
670
|
+
result.memory_mb = stat.memory_mb;
|
|
671
|
+
if (!result.cpu_percent && stat.cpu_percent)
|
|
672
|
+
result.cpu_percent = stat.cpu_percent;
|
|
660
673
|
}
|
|
661
|
-
catch { /* ignore */ }
|
|
662
674
|
}
|
|
663
675
|
return result;
|
|
664
676
|
}
|
|
@@ -670,6 +682,20 @@ async function phaseRunningCheck(instanceId) {
|
|
|
670
682
|
}
|
|
671
683
|
return { ok: true };
|
|
672
684
|
}
|
|
685
|
+
async function phaseResetTerminalJobBeforeStart(instanceId) {
|
|
686
|
+
const status = await getStatus(instanceId);
|
|
687
|
+
if (!["failed", "dead", "complete"].includes(String(status.status)))
|
|
688
|
+
return;
|
|
689
|
+
try {
|
|
690
|
+
const resp = await nomadDelete(`/v1/job/${jobId(instanceId)}?purge=false`);
|
|
691
|
+
if (!resp.ok && resp.status !== 404) {
|
|
692
|
+
console.warn(`[nomad] ${instanceId}: failed to stop terminal job before start (HTTP ${resp.status}): ${await resp.text()}`);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
catch (e) {
|
|
696
|
+
console.warn(`[nomad] ${instanceId}: failed to stop terminal job before start: ${e?.message ?? e}`);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
673
699
|
/**
|
|
674
700
|
* Phase 2: home-conflict check — dispatched through the adapter so
|
|
675
701
|
* framework code carries no agentType-specific knowledge. Adapters that
|
|
@@ -677,7 +703,7 @@ async function phaseRunningCheck(instanceId) {
|
|
|
677
703
|
* each instance owns its own bind-mount) leave the hook unset and this
|
|
678
704
|
* phase is a no-op.
|
|
679
705
|
*/
|
|
680
|
-
async function phaseHomeConflict(
|
|
706
|
+
async function phaseHomeConflict(_instanceId, sharedHomeIds) {
|
|
681
707
|
const homeConflicts = [];
|
|
682
708
|
for (const otherId of sharedHomeIds) {
|
|
683
709
|
const otherStatus = await getStatus(otherId);
|
|
@@ -881,6 +907,7 @@ export async function startInstance(instanceId) {
|
|
|
881
907
|
extra.code = running.code;
|
|
882
908
|
return failed("running_check", extra);
|
|
883
909
|
}
|
|
910
|
+
await phaseResetTerminalJobBeforeStart(instanceId);
|
|
884
911
|
const legacyManager = await getLegacyAppManager(instanceId);
|
|
885
912
|
if (legacyManager) {
|
|
886
913
|
const prep = await legacyManager.prepareStart(instanceId);
|
|
@@ -1118,6 +1145,82 @@ export async function getLogs(instanceId, lines = 200, logType = "stderr") {
|
|
|
1118
1145
|
return [];
|
|
1119
1146
|
}
|
|
1120
1147
|
const execFileAsync = promisify(execFileCb);
|
|
1148
|
+
const DOCKER_STATS_TTL_MS = 30_000;
|
|
1149
|
+
/** Field separator for the batched `docker stats --format` line. Exported so
|
|
1150
|
+
* tests can construct mock output without hardcoding the literal. */
|
|
1151
|
+
export const DOCKER_STATS_FIELD_SEP = "__JS__";
|
|
1152
|
+
let _dockerStatsEntry = null;
|
|
1153
|
+
let _dockerStatsInFlight = null;
|
|
1154
|
+
/** Test-only: reset the shared docker-stats snapshot so each test starts from
|
|
1155
|
+
* a cold cache (the 30s TTL + single-flight would otherwise leak one test's
|
|
1156
|
+
* mocked snapshot into the next). Not used by production code paths. */
|
|
1157
|
+
export function __resetDockerStatsCacheForTests() {
|
|
1158
|
+
_dockerStatsEntry = null;
|
|
1159
|
+
_dockerStatsInFlight = null;
|
|
1160
|
+
}
|
|
1161
|
+
function parseDockerMemUsageMb(memUsage) {
|
|
1162
|
+
// Format: "499.6MiB / 3GiB" — the used side is everything before "/".
|
|
1163
|
+
const used = (memUsage.split("/")[0] ?? "").trim();
|
|
1164
|
+
const match = used.match(/^([\d.]+)\s*(MiB|GiB|MB|GB|KiB|KB|B)?/i);
|
|
1165
|
+
if (!match)
|
|
1166
|
+
return 0;
|
|
1167
|
+
let mb = parseFloat(match[1]);
|
|
1168
|
+
if (!Number.isFinite(mb))
|
|
1169
|
+
return 0;
|
|
1170
|
+
const unit = (match[2] ?? "MiB").toLowerCase();
|
|
1171
|
+
if (unit === "gib" || unit === "gb")
|
|
1172
|
+
mb *= 1024;
|
|
1173
|
+
else if (unit === "kib" || unit === "kb")
|
|
1174
|
+
mb /= 1024;
|
|
1175
|
+
else if (unit === "b")
|
|
1176
|
+
mb /= 1024 * 1024;
|
|
1177
|
+
return Math.round(mb * 10) / 10;
|
|
1178
|
+
}
|
|
1179
|
+
async function loadDockerStatsSnapshot() {
|
|
1180
|
+
const snapshot = new Map();
|
|
1181
|
+
try {
|
|
1182
|
+
const fmt = `{{.Name}}${DOCKER_STATS_FIELD_SEP}{{.MemUsage}}${DOCKER_STATS_FIELD_SEP}{{.CPUPerc}}`;
|
|
1183
|
+
const { stdout } = await execFileAsync("docker", ["stats", "--no-stream", "--format", fmt], { timeout: 8_000 });
|
|
1184
|
+
for (const line of stdout.split("\n")) {
|
|
1185
|
+
const trimmed = line.trim();
|
|
1186
|
+
if (!trimmed)
|
|
1187
|
+
continue;
|
|
1188
|
+
const [name, memUsage, cpuPerc] = trimmed.split(DOCKER_STATS_FIELD_SEP);
|
|
1189
|
+
if (!name)
|
|
1190
|
+
continue;
|
|
1191
|
+
snapshot.set(name, {
|
|
1192
|
+
memory_mb: parseDockerMemUsageMb(memUsage ?? ""),
|
|
1193
|
+
cpu_percent: Math.round((parseFloat(cpuPerc ?? "") || 0) * 10) / 10,
|
|
1194
|
+
});
|
|
1195
|
+
}
|
|
1196
|
+
}
|
|
1197
|
+
catch {
|
|
1198
|
+
/* docker missing / timeout / daemon down → empty map, caller degrades */
|
|
1199
|
+
}
|
|
1200
|
+
return snapshot;
|
|
1201
|
+
}
|
|
1202
|
+
/**
|
|
1203
|
+
* Returns a per-container stats map, refreshing at most once per
|
|
1204
|
+
* DOCKER_STATS_TTL_MS. Concurrent callers (the `Promise.all` over every
|
|
1205
|
+
* instance in `GET /api/instances`) share a single in-flight docker call.
|
|
1206
|
+
*/
|
|
1207
|
+
async function getDockerMemSnapshot() {
|
|
1208
|
+
const now = Date.now();
|
|
1209
|
+
if (_dockerStatsEntry && now - _dockerStatsEntry.ts < DOCKER_STATS_TTL_MS) {
|
|
1210
|
+
return _dockerStatsEntry.data;
|
|
1211
|
+
}
|
|
1212
|
+
if (_dockerStatsInFlight)
|
|
1213
|
+
return _dockerStatsInFlight;
|
|
1214
|
+
_dockerStatsInFlight = loadDockerStatsSnapshot()
|
|
1215
|
+
.then((data) => {
|
|
1216
|
+
_dockerStatsEntry = { data, ts: Date.now() };
|
|
1217
|
+
return data;
|
|
1218
|
+
})
|
|
1219
|
+
.finally(() => {
|
|
1220
|
+
_dockerStatsInFlight = null;
|
|
1221
|
+
});
|
|
1222
|
+
return _dockerStatsInFlight;
|
|
1223
|
+
}
|
|
1121
1224
|
export async function exec(instanceId, command, timeoutMs = 120_000) {
|
|
1122
1225
|
const alloc = await getRunningAlloc(instanceId);
|
|
1123
1226
|
if (!alloc || alloc.ClientStatus !== "running") {
|
|
@@ -1214,7 +1317,7 @@ var UnifiedNomadJobs;
|
|
|
1214
1317
|
const MAX_MEMORY_MAX_MB = 4096; // 4 GB hard limit
|
|
1215
1318
|
const DEFAULT_PIDS_LIMIT = 512;
|
|
1216
1319
|
const NOMAD_CONFIG_PATH = join(JISHUSHELL_HOME, "nomad", "nomad.hcl");
|
|
1217
|
-
const
|
|
1320
|
+
const _DEFAULT_CWD = homedir();
|
|
1218
1321
|
function appDirForId(appId) {
|
|
1219
1322
|
return join(APPS_DIR, appId);
|
|
1220
1323
|
}
|
|
@@ -1408,12 +1511,12 @@ var UnifiedNomadJobs;
|
|
|
1408
1511
|
if (!s)
|
|
1409
1512
|
return defaultNs;
|
|
1410
1513
|
if (s.endsWith("ms"))
|
|
1411
|
-
return parseInt(s) * 1_000_000;
|
|
1514
|
+
return parseInt(s, 10) * 1_000_000;
|
|
1412
1515
|
if (s.endsWith("s"))
|
|
1413
|
-
return parseInt(s) * 1_000_000_000;
|
|
1516
|
+
return parseInt(s, 10) * 1_000_000_000;
|
|
1414
1517
|
if (s.endsWith("m"))
|
|
1415
|
-
return parseInt(s) * 60_000_000_000;
|
|
1416
|
-
return parseInt(s) * 1_000_000_000;
|
|
1518
|
+
return parseInt(s, 10) * 60_000_000_000;
|
|
1519
|
+
return parseInt(s, 10) * 1_000_000_000;
|
|
1417
1520
|
}
|
|
1418
1521
|
function portLabel(taskName, portName) {
|
|
1419
1522
|
const sanitize = (value) => value.replace(/[^a-zA-Z0-9_-]/g, "-");
|
|
@@ -1434,6 +1537,14 @@ var UnifiedNomadJobs;
|
|
|
1434
1537
|
function hostNetworkForPort(port) {
|
|
1435
1538
|
if ((port.visibility ?? "external") === "internal")
|
|
1436
1539
|
return undefined;
|
|
1540
|
+
// Honor explicit host_network from spec (e.g. weknora's ports declare
|
|
1541
|
+
// `host_network: docker_bridge` so peer tasks reach the published port
|
|
1542
|
+
// via host.docker.internal). Verify the named network is actually
|
|
1543
|
+
// declared in nomad.hcl — otherwise Nomad rejects job submission with
|
|
1544
|
+
// "unknown host network". Falls back to "external" when unset.
|
|
1545
|
+
const requested = (port.host_network ?? "").trim();
|
|
1546
|
+
if (requested && nomadConfigDeclaresHostNetwork(requested))
|
|
1547
|
+
return requested;
|
|
1437
1548
|
return nomadConfigDeclaresHostNetwork("external") ? "external" : undefined;
|
|
1438
1549
|
}
|
|
1439
1550
|
function specRequiresExternalHostNetwork(spec) {
|
|
@@ -1492,6 +1603,96 @@ var UnifiedNomadJobs;
|
|
|
1492
1603
|
: {}),
|
|
1493
1604
|
}));
|
|
1494
1605
|
}
|
|
1606
|
+
function isExternalAppTaskPort(port) {
|
|
1607
|
+
return (port.visibility ?? "external") !== "internal";
|
|
1608
|
+
}
|
|
1609
|
+
function readDeclaredHostPort(port) {
|
|
1610
|
+
const candidate = port.host_port ?? port.port;
|
|
1611
|
+
return Number.isInteger(candidate) && candidate > 0 ? candidate : null;
|
|
1612
|
+
}
|
|
1613
|
+
function applyPersistedAppSpecPortOverrides(appId, spec) {
|
|
1614
|
+
const meta = getInstance(appId);
|
|
1615
|
+
if (!meta)
|
|
1616
|
+
return spec;
|
|
1617
|
+
const runtime = getInstanceRuntime(appId);
|
|
1618
|
+
const runtimePorts = Array.isArray(runtime.ports) ? runtime.ports : [];
|
|
1619
|
+
const persistedGatewayPort = extractGatewayPort(runtime, resolveAgentType(meta));
|
|
1620
|
+
const totalExternalPorts = spec.tasks.reduce((count, task) => count + (task.ports ?? []).filter((port) => isExternalAppTaskPort(port)).length, 0);
|
|
1621
|
+
let changed = false;
|
|
1622
|
+
const tasks = spec.tasks.map((task) => {
|
|
1623
|
+
if (!Array.isArray(task.ports) || task.ports.length === 0)
|
|
1624
|
+
return task;
|
|
1625
|
+
let taskChanged = false;
|
|
1626
|
+
const ports = task.ports.map((port) => {
|
|
1627
|
+
if (!isExternalAppTaskPort(port))
|
|
1628
|
+
return port;
|
|
1629
|
+
const currentHostPort = readDeclaredHostPort(port);
|
|
1630
|
+
if (!currentHostPort)
|
|
1631
|
+
return port;
|
|
1632
|
+
let nextHostPort = null;
|
|
1633
|
+
const namedRuntimePort = typeof port.name === "string" && port.name
|
|
1634
|
+
? runtimePorts.find((candidate) => candidate?.name === port.name
|
|
1635
|
+
&& Number.isInteger(candidate?.hostPort)
|
|
1636
|
+
&& candidate.hostPort > 0)
|
|
1637
|
+
: null;
|
|
1638
|
+
if (namedRuntimePort) {
|
|
1639
|
+
nextHostPort = namedRuntimePort.hostPort;
|
|
1640
|
+
}
|
|
1641
|
+
else if (runtimePorts.length === 1
|
|
1642
|
+
&& totalExternalPorts === 1
|
|
1643
|
+
&& Number.isInteger(runtimePorts[0]?.hostPort)
|
|
1644
|
+
&& runtimePorts[0].hostPort > 0) {
|
|
1645
|
+
nextHostPort = runtimePorts[0].hostPort;
|
|
1646
|
+
}
|
|
1647
|
+
else if (totalExternalPorts === 1
|
|
1648
|
+
&& persistedGatewayPort != null
|
|
1649
|
+
&& persistedGatewayPort > 0) {
|
|
1650
|
+
nextHostPort = persistedGatewayPort;
|
|
1651
|
+
}
|
|
1652
|
+
if (!nextHostPort || nextHostPort === currentHostPort)
|
|
1653
|
+
return port;
|
|
1654
|
+
changed = true;
|
|
1655
|
+
taskChanged = true;
|
|
1656
|
+
return { ...port, host_port: nextHostPort };
|
|
1657
|
+
});
|
|
1658
|
+
return taskChanged ? { ...task, ports } : task;
|
|
1659
|
+
});
|
|
1660
|
+
return changed ? { ...spec, tasks } : spec;
|
|
1661
|
+
}
|
|
1662
|
+
async function maybeReallocateAppSpecHostPort(appId, spec, reason) {
|
|
1663
|
+
if (!getInstance(appId))
|
|
1664
|
+
return { spec, changed: false };
|
|
1665
|
+
const effectiveSpec = applyPersistedAppSpecPortOverrides(appId, spec);
|
|
1666
|
+
const currentGatewayPort = getGatewayPort(appId);
|
|
1667
|
+
if (!Number.isInteger(currentGatewayPort) || currentGatewayPort <= 0) {
|
|
1668
|
+
return { spec: effectiveSpec, changed: false };
|
|
1669
|
+
}
|
|
1670
|
+
const declaredPorts = effectiveSpec.tasks.flatMap((task) => (task.ports ?? [])
|
|
1671
|
+
.filter((port) => isExternalAppTaskPort(port))
|
|
1672
|
+
.map((port) => readDeclaredHostPort(port))
|
|
1673
|
+
.filter((port) => port != null));
|
|
1674
|
+
if (!declaredPorts.includes(currentGatewayPort)) {
|
|
1675
|
+
return { spec: effectiveSpec, changed: false };
|
|
1676
|
+
}
|
|
1677
|
+
if (!(await isPortInUse(currentGatewayPort))) {
|
|
1678
|
+
return { spec: effectiveSpec, changed: false };
|
|
1679
|
+
}
|
|
1680
|
+
try {
|
|
1681
|
+
const reallocation = await reallocateGatewayPort(appId);
|
|
1682
|
+
console.log(`[nomad] ${appId}: reallocated AppSpec host port ${reallocation.from} -> ${reallocation.to} (${reason})`);
|
|
1683
|
+
return {
|
|
1684
|
+
spec: applyPersistedAppSpecPortOverrides(appId, spec),
|
|
1685
|
+
changed: true,
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
catch (e) {
|
|
1689
|
+
return {
|
|
1690
|
+
spec: effectiveSpec,
|
|
1691
|
+
changed: false,
|
|
1692
|
+
error: `AppSpec host port ${currentGatewayPort} is held by another process and reallocation failed: ${e?.message ?? e}`,
|
|
1693
|
+
};
|
|
1694
|
+
}
|
|
1695
|
+
}
|
|
1495
1696
|
// ── Health check → Nomad service check builder ────────────────────────────
|
|
1496
1697
|
function buildServiceCheck(task, appId) {
|
|
1497
1698
|
const health = task.health;
|
|
@@ -1710,12 +1911,6 @@ var UnifiedNomadJobs;
|
|
|
1710
1911
|
return null;
|
|
1711
1912
|
return command.replace(/^~(?=\/|$)/, homedir());
|
|
1712
1913
|
}
|
|
1713
|
-
function taskCommandLine(task) {
|
|
1714
|
-
const command = expandTaskCommand(task.command);
|
|
1715
|
-
if (!command)
|
|
1716
|
-
return null;
|
|
1717
|
-
return [command, ...(task.args ?? []).map(String)].join(" ").trim();
|
|
1718
|
-
}
|
|
1719
1914
|
function commandLineMatchesTask(commandLine, task) {
|
|
1720
1915
|
const normalized = commandLine.trim();
|
|
1721
1916
|
const command = expandTaskCommand(task.command);
|
|
@@ -2264,6 +2459,21 @@ var UnifiedNomadJobs;
|
|
|
2264
2459
|
}
|
|
2265
2460
|
return declared;
|
|
2266
2461
|
})();
|
|
2462
|
+
// Validate cap_add against a tight allowlist. The full Linux cap surface
|
|
2463
|
+
// is large; we only honor capabilities image entrypoints actually need
|
|
2464
|
+
// for the canonical "start as root + gosu to user" pattern. Anything
|
|
2465
|
+
// outside this list is silently dropped — failing closed protects
|
|
2466
|
+
// against typo'd / hostile specs widening the container's capability
|
|
2467
|
+
// bounds beyond what the panel author signed off on.
|
|
2468
|
+
const ALLOWED_CAPS = new Set([
|
|
2469
|
+
"CHOWN", "DAC_OVERRIDE", "FOWNER",
|
|
2470
|
+
"SETUID", "SETGID", "SETPCAP", "NET_BIND_SERVICE",
|
|
2471
|
+
]);
|
|
2472
|
+
const capAdd = Array.isArray(task.cap_add)
|
|
2473
|
+
? task.cap_add
|
|
2474
|
+
.map((c) => typeof c === "string" ? c.trim().toUpperCase().replace(/^CAP_/, "") : "")
|
|
2475
|
+
.filter((c) => ALLOWED_CAPS.has(c))
|
|
2476
|
+
: [];
|
|
2267
2477
|
const taskDef = {
|
|
2268
2478
|
Name: task.name,
|
|
2269
2479
|
Driver: "docker",
|
|
@@ -2285,6 +2495,7 @@ var UnifiedNomadJobs;
|
|
|
2285
2495
|
...(publishedPorts.length > 0 ? { ports: publishedPorts } : {}),
|
|
2286
2496
|
extra_hosts: ["host.docker.internal:host-gateway"],
|
|
2287
2497
|
cap_drop: ["ALL"],
|
|
2498
|
+
...(capAdd.length > 0 ? { cap_add: capAdd } : {}),
|
|
2288
2499
|
security_opt: ["no-new-privileges"],
|
|
2289
2500
|
pids_limit: DEFAULT_PIDS_LIMIT,
|
|
2290
2501
|
readonly_rootfs: false,
|
|
@@ -2354,13 +2565,17 @@ var UnifiedNomadJobs;
|
|
|
2354
2565
|
? { Networks: [{ ReservedPorts: groupReservedPorts }] }
|
|
2355
2566
|
: {}),
|
|
2356
2567
|
RestartPolicy: {
|
|
2357
|
-
|
|
2358
|
-
|
|
2568
|
+
// 10 attempts × 15s delay = ~2.5min recovery window. Multi-task
|
|
2569
|
+
// app groups commonly need 3-5 restarts before sidecar
|
|
2570
|
+
// dependencies' external port-publish settles.
|
|
2571
|
+
Attempts: 10,
|
|
2572
|
+
Interval: 600_000_000_000,
|
|
2359
2573
|
Delay: 15_000_000_000,
|
|
2360
2574
|
Mode: "fail",
|
|
2361
2575
|
},
|
|
2362
2576
|
Reschedule: {
|
|
2363
|
-
Attempts:
|
|
2577
|
+
Attempts: 3,
|
|
2578
|
+
Interval: 3_600_000_000_000,
|
|
2364
2579
|
Unlimited: false,
|
|
2365
2580
|
},
|
|
2366
2581
|
Update: {
|
|
@@ -2485,6 +2700,7 @@ var UnifiedNomadJobs;
|
|
|
2485
2700
|
}
|
|
2486
2701
|
return statuses[0];
|
|
2487
2702
|
}
|
|
2703
|
+
UnifiedNomadJobs.aggregateHealthStatus = aggregateHealthStatus;
|
|
2488
2704
|
async function getRunningAlloc(appId) {
|
|
2489
2705
|
return pickLiveAlloc(await getAllocs(appId));
|
|
2490
2706
|
}
|
|
@@ -2616,28 +2832,17 @@ var UnifiedNomadJobs;
|
|
|
2616
2832
|
}
|
|
2617
2833
|
}
|
|
2618
2834
|
catch { /* ignore */ }
|
|
2619
|
-
// Fallback:
|
|
2620
|
-
//
|
|
2621
|
-
if (!result.memory_mb && allocId && ptName) {
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
const match = raw.match(/^([\d.]+)\s*(MiB|GiB|MB|GB|KiB|KB)/i);
|
|
2630
|
-
if (match) {
|
|
2631
|
-
let mb = parseFloat(match[1]);
|
|
2632
|
-
const unit = match[2].toLowerCase();
|
|
2633
|
-
if (unit === "gib" || unit === "gb")
|
|
2634
|
-
mb *= 1024;
|
|
2635
|
-
else if (unit === "kib" || unit === "kb")
|
|
2636
|
-
mb /= 1024;
|
|
2637
|
-
result.memory_mb = Math.round(mb * 10) / 10;
|
|
2638
|
-
}
|
|
2835
|
+
// Fallback: cgroup v2 (Pi / CIX) → Nomad alloc-stats are zero. Use the
|
|
2836
|
+
// shared cached `docker stats` snapshot rather than forking per-instance.
|
|
2837
|
+
if (!result.memory_mb && allocId && ptName && /^[a-f0-9-]+$/i.test(allocId)) {
|
|
2838
|
+
const containerName = `${ptName}-${allocId}`;
|
|
2839
|
+
const stat = (await getDockerMemSnapshot()).get(containerName);
|
|
2840
|
+
if (stat) {
|
|
2841
|
+
if (stat.memory_mb)
|
|
2842
|
+
result.memory_mb = stat.memory_mb;
|
|
2843
|
+
if (!result.cpu_percent && stat.cpu_percent)
|
|
2844
|
+
result.cpu_percent = stat.cpu_percent;
|
|
2639
2845
|
}
|
|
2640
|
-
catch { /* ignore */ }
|
|
2641
2846
|
}
|
|
2642
2847
|
return result;
|
|
2643
2848
|
}
|
|
@@ -2725,7 +2930,7 @@ var UnifiedNomadJobs;
|
|
|
2725
2930
|
if (adoptedExternal.conflicts.length > 0) {
|
|
2726
2931
|
return { ok: false, error: adoptedExternal.conflicts.join("; ") };
|
|
2727
2932
|
}
|
|
2728
|
-
|
|
2933
|
+
let effectiveSpec = applyPersistedAppSpecPortOverrides(appId, adoptedExternal.spec);
|
|
2729
2934
|
// Validate all images before submitting
|
|
2730
2935
|
for (const task of effectiveSpec.tasks) {
|
|
2731
2936
|
if (task.runtime === "container") {
|
|
@@ -2752,31 +2957,54 @@ var UnifiedNomadJobs;
|
|
|
2752
2957
|
if (hostNetworkError) {
|
|
2753
2958
|
return { ok: false, error: hostNetworkError };
|
|
2754
2959
|
}
|
|
2755
|
-
|
|
2756
|
-
|
|
2757
|
-
|
|
2758
|
-
|
|
2759
|
-
|
|
2760
|
-
return { ok: false, error: `Job build failed: ${e.message}` };
|
|
2960
|
+
if (driver === "docker") {
|
|
2961
|
+
const preflight = await maybeReallocateAppSpecHostPort(appId, effectiveSpec, "host_port_busy");
|
|
2962
|
+
if (preflight.error)
|
|
2963
|
+
return { ok: false, error: preflight.error };
|
|
2964
|
+
effectiveSpec = preflight.spec;
|
|
2761
2965
|
}
|
|
2762
|
-
|
|
2763
|
-
|
|
2764
|
-
|
|
2765
|
-
|
|
2766
|
-
return { ok: true, eval_id: data.EvalID };
|
|
2966
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
2967
|
+
let jobDef;
|
|
2968
|
+
try {
|
|
2969
|
+
jobDef = buildAppJob(effectiveSpec, appId, driver, extraEnv);
|
|
2767
2970
|
}
|
|
2768
|
-
|
|
2769
|
-
|
|
2770
|
-
|
|
2771
|
-
|
|
2772
|
-
|
|
2773
|
-
|
|
2774
|
-
|
|
2775
|
-
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2971
|
+
catch (e) {
|
|
2972
|
+
return { ok: false, error: `Job build failed: ${e.message}` };
|
|
2973
|
+
}
|
|
2974
|
+
let submitError = null;
|
|
2975
|
+
let netErr = false;
|
|
2976
|
+
try {
|
|
2977
|
+
const resp = await nomadPost("/v1/jobs", jobDef);
|
|
2978
|
+
if (resp.ok) {
|
|
2979
|
+
const data = await resp.json();
|
|
2980
|
+
// When the app was previously failed, verify it actually transitions
|
|
2981
|
+
// away from the failed state rather than reporting false success.
|
|
2982
|
+
if (status.status === "failed") {
|
|
2983
|
+
const recovered = await waitForRecovery(appId, 15_000, 2_000);
|
|
2984
|
+
if (!recovered) {
|
|
2985
|
+
return { ok: false, error: "App start submitted but instance remains in failed state. Check app logs for details.", eval_id: data.EvalID };
|
|
2986
|
+
}
|
|
2987
|
+
}
|
|
2988
|
+
return { ok: true, eval_id: data.EvalID };
|
|
2989
|
+
}
|
|
2990
|
+
submitError = await resp.text();
|
|
2991
|
+
}
|
|
2992
|
+
catch (e) {
|
|
2993
|
+
netErr = e?.message === "fetch failed" || e?.cause?.code === "ECONNREFUSED";
|
|
2994
|
+
submitError = netErr ? `Nomad 服务不可达 (${getNomadAddr()}),请先启动 Nomad` : e.message;
|
|
2995
|
+
}
|
|
2996
|
+
if (attempt === 0 && driver === "docker" && !netErr) {
|
|
2997
|
+
const retry = await maybeReallocateAppSpecHostPort(appId, effectiveSpec, "docker_race");
|
|
2998
|
+
if (retry.error)
|
|
2999
|
+
return { ok: false, error: retry.error };
|
|
3000
|
+
if (retry.changed) {
|
|
3001
|
+
effectiveSpec = retry.spec;
|
|
3002
|
+
continue;
|
|
3003
|
+
}
|
|
3004
|
+
}
|
|
3005
|
+
return { ok: false, error: submitError ?? "unknown error" };
|
|
2779
3006
|
}
|
|
3007
|
+
return { ok: false, error: "start retry exhausted" };
|
|
2780
3008
|
}
|
|
2781
3009
|
UnifiedNomadJobs.startAppJob = startAppJob;
|
|
2782
3010
|
/**
|
|
@@ -2796,6 +3024,21 @@ var UnifiedNomadJobs;
|
|
|
2796
3024
|
return false;
|
|
2797
3025
|
}
|
|
2798
3026
|
UnifiedNomadJobs.waitForRunning = waitForRunning;
|
|
3027
|
+
/**
|
|
3028
|
+
* Poll until the app job leaves the "failed" state or times out.
|
|
3029
|
+
* Used after start submission to verify actual recovery before reporting success.
|
|
3030
|
+
* Returns true if the app transitions away from "failed" (to pending/running/etc).
|
|
3031
|
+
*/
|
|
3032
|
+
async function waitForRecovery(appId, timeoutMs = 15_000, pollIntervalMs = 2_000) {
|
|
3033
|
+
const deadline = Date.now() + timeoutMs;
|
|
3034
|
+
while (Date.now() < deadline) {
|
|
3035
|
+
await new Promise((r) => setTimeout(r, pollIntervalMs));
|
|
3036
|
+
const status = await getAppStatus(appId);
|
|
3037
|
+
if (status.status !== "failed")
|
|
3038
|
+
return true;
|
|
3039
|
+
}
|
|
3040
|
+
return false;
|
|
3041
|
+
}
|
|
2799
3042
|
async function checkDependencies(spec) {
|
|
2800
3043
|
if (!spec.depends_on || Object.keys(spec.depends_on).length === 0) {
|
|
2801
3044
|
return { ok: true, errors: [] };
|
|
@@ -3705,4 +3948,6 @@ export const shouldAutoStartNomadJob = UnifiedNomadJobs.shouldAutoStart;
|
|
|
3705
3948
|
export const startNomadJobInstance = UnifiedNomadJobs.startInstance;
|
|
3706
3949
|
export const stopNomadJobInstance = UnifiedNomadJobs.stopInstance;
|
|
3707
3950
|
export const restartNomadJobInstance = UnifiedNomadJobs.restartInstance;
|
|
3951
|
+
// @internal — exposed for Phase 10.4 unit testing only.
|
|
3952
|
+
export const __aggregateHealthStatusForTests = UnifiedNomadJobs.aggregateHealthStatus;
|
|
3708
3953
|
//# sourceMappingURL=nomad-manager.js.map
|