dsh-plugin-local-infer 0.1.3 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +22 -4
- package/cordis.patch.yml +7 -0
- package/docs/zh/README.md +14 -2
- package/dsh/client.js +4195 -4522
- package/package.json +28 -11
- package/packages/core/lib/adapter.d.ts +133 -9
- package/packages/core/lib/adapter.js +74 -1
- package/packages/core/lib/config.d.ts +1 -1
- package/packages/core/lib/config.js +1 -1
- package/packages/core/lib/debug.d.ts +1 -1
- package/packages/core/lib/debug.js +1 -1
- package/packages/core/lib/defaults.d.ts +1 -1
- package/packages/core/lib/defaults.js +1 -1
- package/packages/core/lib/deps.d.ts +44 -17
- package/packages/core/lib/deps.js +24 -8
- package/packages/core/lib/estimator.d.ts +1 -1
- package/packages/core/lib/estimator.js +1 -1
- package/packages/core/lib/guideline.d.ts +1 -1
- package/packages/core/lib/guideline.js +1 -1
- package/packages/core/lib/hardware.d.ts +1 -1
- package/packages/core/lib/hardware.js +1 -1
- package/packages/core/lib/hf-cache.d.ts +70 -0
- package/packages/core/lib/hf-cache.js +83 -0
- package/packages/core/lib/index.d.ts +16 -9
- package/packages/core/lib/index.js +10 -6
- package/packages/core/lib/infer-cache.d.ts +39 -0
- package/packages/core/lib/infer-cache.js +53 -0
- package/packages/core/lib/launch-command.d.ts +8 -4
- package/packages/core/lib/launch-command.js +13 -6
- package/packages/core/lib/launch-record.d.ts +1 -1
- package/packages/core/lib/launch-record.js +1 -1
- package/packages/core/lib/logs.d.ts +8 -1
- package/packages/core/lib/logs.js +12 -4
- package/packages/core/lib/models.d.ts +120 -14
- package/packages/core/lib/models.js +209 -17
- package/packages/core/lib/parameter.d.ts +31 -3
- package/packages/core/lib/parameter.js +1 -1
- package/packages/core/lib/progress.d.ts +54 -0
- package/packages/core/lib/progress.js +120 -0
- package/packages/core/lib/registrar.d.ts +20 -9
- package/packages/core/lib/registrar.js +27 -7
- package/packages/core/lib/reserve.d.ts +11 -3
- package/packages/core/lib/reserve.js +6 -4
- package/packages/core/lib/status.d.ts +12 -6
- package/packages/core/lib/status.js +21 -10
- package/packages/core/lib/sudo.d.ts +80 -0
- package/packages/core/lib/sudo.js +78 -0
- package/packages/core/package.json +1 -1
- package/packages/orchestrator/lib/config.d.ts +63 -0
- package/packages/orchestrator/lib/config.js +111 -0
- package/packages/orchestrator/lib/index.d.ts +51 -0
- package/packages/orchestrator/lib/index.js +73 -0
- package/packages/orchestrator/lib/route.d.ts +60 -0
- package/packages/orchestrator/lib/route.js +159 -0
- package/packages/orchestrator/locale/en.json +6 -0
- package/packages/orchestrator/package.json +39 -0
- package/packages/orchestrator/prompt.md +87 -0
- package/packages/providers/lib/acquire.d.ts +92 -14
- package/packages/providers/lib/acquire.js +215 -37
- package/packages/providers/lib/adapters.d.ts +136 -12
- package/packages/providers/lib/adapters.js +597 -70
- package/packages/providers/lib/debug.d.ts +1 -1
- package/packages/providers/lib/debug.js +1 -1
- package/packages/providers/lib/deps.d.ts +27 -230
- package/packages/providers/lib/deps.js +61 -214
- package/packages/providers/lib/engines/basalt/basalt.d.ts +130 -0
- package/packages/providers/lib/engines/basalt/basalt.js +304 -0
- package/packages/providers/lib/engines/basalt/basaltAdapter.d.ts +85 -0
- package/packages/providers/lib/engines/basalt/basaltAdapter.js +439 -0
- package/packages/providers/lib/engines/basalt/basaltLinux.d.ts +23 -0
- package/packages/providers/lib/engines/basalt/basaltLinux.js +47 -0
- package/packages/providers/lib/engines/basalt/basaltMacOS.d.ts +17 -0
- package/packages/providers/lib/engines/basalt/basaltMacOS.js +19 -0
- package/packages/providers/lib/engines/basalt/basaltWindows.d.ts +24 -0
- package/packages/providers/lib/engines/basalt/basaltWindows.js +49 -0
- package/packages/providers/lib/engines/engineOs.d.ts +24 -0
- package/packages/providers/lib/engines/engineOs.js +17 -0
- package/packages/providers/lib/engines/{hyperqwen.d.ts → hyperqwen/hyperqwen.d.ts} +2 -2
- package/packages/providers/lib/engines/{hyperqwen.js → hyperqwen/hyperqwen.js} +14 -2
- package/packages/providers/lib/engines/{hyperqwenAdapter.d.ts → hyperqwen/hyperqwenAdapter.d.ts} +6 -6
- package/packages/providers/lib/engines/{hyperqwenAdapter.js → hyperqwen/hyperqwenAdapter.js} +24 -14
- package/packages/providers/lib/engines/hyperqwen/hyperqwenArch.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenArch.js +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenDebian.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenDebian.js +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenFedora.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenFedora.js +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenLinux.d.ts +10 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenLinux.js +14 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenMacOS.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenMacOS.js +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenWindows.d.ts +9 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenWindows.js +9 -0
- package/packages/providers/lib/engines/{infernix.d.ts → infernix/infernix.d.ts} +7 -4
- package/packages/providers/lib/engines/{infernix.js → infernix/infernix.js} +52 -7
- package/packages/providers/lib/engines/{infernixAdapter.d.ts → infernix/infernixAdapter.d.ts} +5 -6
- package/packages/providers/lib/engines/{infernixAdapter.js → infernix/infernixAdapter.js} +75 -42
- package/packages/providers/lib/engines/infernix/infernixArch.d.ts +9 -0
- package/packages/providers/lib/engines/infernix/infernixArch.js +9 -0
- package/packages/providers/lib/engines/infernix/infernixDebian.d.ts +9 -0
- package/packages/providers/lib/engines/infernix/infernixDebian.js +9 -0
- package/packages/providers/lib/engines/infernix/infernixFedora.d.ts +9 -0
- package/packages/providers/lib/engines/infernix/infernixFedora.js +9 -0
- package/packages/providers/lib/engines/infernix/infernixLinux.d.ts +17 -0
- package/packages/providers/lib/engines/infernix/infernixLinux.js +35 -0
- package/packages/providers/lib/engines/infernix/infernixMacOS.d.ts +16 -0
- package/packages/providers/lib/engines/infernix/infernixMacOS.js +18 -0
- package/packages/providers/lib/engines/infernix/infernixWindows.d.ts +16 -0
- package/packages/providers/lib/engines/infernix/infernixWindows.js +34 -0
- package/packages/providers/lib/engines/{llamacpp.d.ts → llamacpp/llamacpp.d.ts} +2 -2
- package/packages/providers/lib/engines/{llamacpp.js → llamacpp/llamacpp.js} +12 -2
- package/packages/providers/lib/engines/{llamacppAdapter.d.ts → llamacpp/llamacppAdapter.d.ts} +5 -6
- package/packages/providers/lib/engines/{llamacppAdapter.js → llamacpp/llamacppAdapter.js} +87 -45
- package/packages/providers/lib/engines/llamacpp/llamacppArch.d.ts +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppArch.js +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppDebian.d.ts +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppDebian.js +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppFedora.d.ts +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppFedora.js +9 -0
- package/packages/providers/lib/engines/llamacpp/llamacppLinux.d.ts +17 -0
- package/packages/providers/lib/engines/llamacpp/llamacppLinux.js +28 -0
- package/packages/providers/lib/engines/llamacpp/llamacppMacOS.d.ts +15 -0
- package/packages/providers/lib/engines/llamacpp/llamacppMacOS.js +17 -0
- package/packages/providers/lib/engines/llamacpp/llamacppWindows.d.ts +15 -0
- package/packages/providers/lib/engines/llamacpp/llamacppWindows.js +26 -0
- package/packages/providers/lib/engines/{ninfer.d.ts → ninfer/ninfer.d.ts} +2 -2
- package/packages/providers/lib/engines/{ninfer.js → ninfer/ninfer.js} +24 -2
- package/packages/providers/lib/engines/{ninferAdapter.d.ts → ninfer/ninferAdapter.d.ts} +5 -6
- package/packages/providers/lib/engines/{ninferAdapter.js → ninfer/ninferAdapter.js} +123 -55
- package/packages/providers/lib/engines/ninfer/ninferArch.d.ts +9 -0
- package/packages/providers/lib/engines/ninfer/ninferArch.js +9 -0
- package/packages/providers/lib/engines/ninfer/ninferDebian.d.ts +9 -0
- package/packages/providers/lib/engines/ninfer/ninferDebian.js +9 -0
- package/packages/providers/lib/engines/ninfer/ninferFedora.d.ts +9 -0
- package/packages/providers/lib/engines/ninfer/ninferFedora.js +9 -0
- package/packages/providers/lib/engines/ninfer/ninferLinux.d.ts +17 -0
- package/packages/providers/lib/engines/ninfer/ninferLinux.js +37 -0
- package/packages/providers/lib/engines/ninfer/ninferMacOS.d.ts +15 -0
- package/packages/providers/lib/engines/ninfer/ninferMacOS.js +17 -0
- package/packages/providers/lib/engines/ninfer/ninferWindows.d.ts +29 -0
- package/packages/providers/lib/engines/ninfer/ninferWindows.js +59 -0
- package/packages/providers/lib/engines/strata/strata.d.ts +1 -0
- package/packages/providers/lib/engines/{strata.js → strata/strata.js} +70 -3
- package/packages/providers/lib/engines/{strataAdapter.d.ts → strata/strataAdapter.d.ts} +5 -6
- package/packages/providers/lib/engines/{strataAdapter.js → strata/strataAdapter.js} +147 -111
- package/packages/providers/lib/engines/strata/strataArch.d.ts +9 -0
- package/packages/providers/lib/engines/strata/strataArch.js +9 -0
- package/packages/providers/lib/engines/strata/strataDebian.d.ts +9 -0
- package/packages/providers/lib/engines/strata/strataDebian.js +9 -0
- package/packages/providers/lib/engines/strata/strataFedora.d.ts +9 -0
- package/packages/providers/lib/engines/strata/strataFedora.js +9 -0
- package/packages/providers/lib/engines/strata/strataLinux.d.ts +21 -0
- package/packages/providers/lib/engines/strata/strataLinux.js +30 -0
- package/packages/providers/lib/engines/strata/strataMacOS.d.ts +16 -0
- package/packages/providers/lib/engines/strata/strataMacOS.js +17 -0
- package/packages/providers/lib/engines/strata/strataWindows.d.ts +27 -0
- package/packages/providers/lib/engines/strata/strataWindows.js +37 -0
- package/packages/providers/lib/engines.d.ts +7 -5
- package/packages/providers/lib/engines.js +21 -10
- package/packages/providers/lib/guidelines.d.ts +5 -4
- package/packages/providers/lib/guidelines.js +18 -5
- package/packages/providers/lib/index.d.ts +33 -29
- package/packages/providers/lib/index.js +45 -33
- package/packages/providers/lib/model-defaults.d.ts +5 -1
- package/packages/providers/lib/model-defaults.js +25 -1
- package/packages/providers/lib/platform.d.ts +8 -5
- package/packages/providers/lib/platform.js +23 -9
- package/packages/providers/lib/presets.d.ts +6 -4
- package/packages/providers/lib/presets.js +53 -5
- package/packages/providers/lib/systemDeps.d.ts +313 -0
- package/packages/providers/lib/systemDeps.js +284 -0
- package/packages/providers/lib/vram.d.ts +34 -8
- package/packages/providers/lib/vram.js +81 -8
- package/packages/providers/package.json +2 -2
- package/packages/service/lib/index.d.ts +176 -21
- package/packages/service/lib/index.js +589 -86
- package/packages/service/lib/launch-target.d.ts +19 -0
- package/packages/service/lib/launch-target.js +28 -0
- package/packages/service/lib/run-logs.d.ts +72 -0
- package/packages/service/lib/run-logs.js +125 -0
- package/packages/service/lib/types.d.ts +163 -6
- package/packages/service/lib/types.js +1 -1
- package/packages/service/package.json +2 -2
- package/packages/tool/lib/index.d.ts +1 -1
- package/packages/tool/lib/index.js +1 -1
- package/packages/tool/package.json +2 -2
- package/packages/ui/lib/config.d.ts +12 -2
- package/packages/ui/lib/config.js +18 -2
- package/packages/ui/lib/deps.d.ts +1 -1
- package/packages/ui/lib/deps.js +1 -1
- package/packages/ui/lib/form.d.ts +1 -1
- package/packages/ui/lib/form.js +1 -1
- package/packages/ui/lib/index.d.ts +7 -1
- package/packages/ui/lib/index.js +12 -2
- package/packages/ui/lib/meter.d.ts +1 -1
- package/packages/ui/lib/meter.js +1 -1
- package/packages/ui/lib/ops/engine.d.ts +142 -34
- package/packages/ui/lib/ops/engine.js +469 -265
- package/packages/ui/lib/ops/get-steps.d.ts +98 -0
- package/packages/ui/lib/ops/get-steps.js +359 -0
- package/packages/ui/lib/ops/hf.d.ts +26 -11
- package/packages/ui/lib/ops/hf.js +115 -25
- package/packages/ui/lib/ops/logs.d.ts +70 -15
- package/packages/ui/lib/ops/logs.js +96 -35
- package/packages/ui/lib/ops/model.d.ts +2 -10
- package/packages/ui/lib/ops/model.js +9 -16
- package/packages/ui/lib/request.d.ts +1 -1
- package/packages/ui/lib/request.js +1 -1
- package/packages/ui/lib/route.d.ts +22 -6
- package/packages/ui/lib/route.js +32 -9
- package/packages/ui/lib/routes/chat-template.d.ts +1 -14
- package/packages/ui/lib/routes/chat-template.js +1 -37
- package/packages/ui/lib/routes/deps.d.ts +1 -16
- package/packages/ui/lib/routes/deps.js +1 -27
- package/packages/ui/lib/routes/download.d.ts +1 -16
- package/packages/ui/lib/routes/download.js +1 -48
- package/packages/ui/lib/routes/get.d.ts +1 -18
- package/packages/ui/lib/routes/get.js +66 -17
- package/packages/ui/lib/routes/install.d.ts +1 -1
- package/packages/ui/lib/routes/install.js +1 -1
- package/packages/ui/lib/routes/launch.d.ts +13 -7
- package/packages/ui/lib/routes/launch.js +33 -9
- package/packages/ui/lib/routes/logs.d.ts +57 -0
- package/packages/ui/lib/routes/logs.js +117 -0
- package/packages/ui/lib/routes/provider-delete.d.ts +1 -1
- package/packages/ui/lib/routes/provider-delete.js +1 -1
- package/packages/ui/lib/routes/register.d.ts +1 -1
- package/packages/ui/lib/routes/register.js +1 -1
- package/packages/ui/lib/routes/save.d.ts +1 -1
- package/packages/ui/lib/routes/save.js +22 -2
- package/packages/ui/lib/routes/stop.d.ts +7 -4
- package/packages/ui/lib/routes/stop.js +7 -4
- package/packages/ui/lib/routes/stream.d.ts +40 -0
- package/packages/ui/lib/routes/stream.js +68 -0
- package/packages/ui/lib/routes/summary.d.ts +1 -1
- package/packages/ui/lib/routes/summary.js +1 -1
- package/packages/ui/lib/routes/unregister.d.ts +1 -1
- package/packages/ui/lib/routes/unregister.js +1 -1
- package/packages/ui/lib/viewmodels/deps.d.ts +1 -31
- package/packages/ui/lib/viewmodels/deps.js +1 -89
- package/packages/ui/lib/viewmodels/depsHelpers.d.ts +1 -28
- package/packages/ui/lib/viewmodels/depsHelpers.js +72 -11
- package/packages/ui/lib/viewmodels/summary.d.ts +7 -2
- package/packages/ui/lib/viewmodels/summary.js +55 -35
- package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +1 -1
- package/packages/ui/lib/viewmodels/summaryHelpers.js +1 -1
- package/packages/ui/lib/warnings.d.ts +1 -1
- package/packages/ui/lib/warnings.js +1 -1
- package/packages/ui/package.json +4 -4
- package/packages/providers/lib/engines/strata.d.ts +0 -53
|
@@ -1,24 +1,53 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* The shared engine-adapter helpers: the spec-to-argv and spec-to-env
|
|
3
|
-
* translators the concrete adapters (the `engines
|
|
3
|
+
* translators the concrete adapters (the `engines/<engine>/<engine>Adapter.ts`
|
|
4
|
+
* files) compose
|
|
4
5
|
* into a `LaunchPlan`, the card's Custom Arguments translators, the
|
|
5
6
|
* `LifecycleHost` the deploy, health-check, and stop steps drive, and the
|
|
6
7
|
* health poll. The node implementation (`nodeLifecycleHost`) is what the
|
|
7
8
|
* service wires in on a live host.
|
|
8
9
|
* @module dsh-plugin-local-infer/providers/adapters
|
|
9
10
|
*/
|
|
10
|
-
import { exec, spawn as nodeSpawn } from 'node:child_process';
|
|
11
|
+
import { exec, execFile, spawn as nodeSpawn } from 'node:child_process';
|
|
12
|
+
import { mkdir, writeFile as fsWriteFile } from 'node:fs/promises';
|
|
13
|
+
import { createRequire } from 'node:module';
|
|
14
|
+
import { dirname, isAbsolute, join } from 'node:path';
|
|
15
|
+
import { commandLogLine, guardSudoCommand, LOG_TAIL_BYTES, SUDO_PROBE_COMMAND, sudoFailureDetail, urlLogLine, } from '../../core/lib/index.js';
|
|
16
|
+
/**
|
|
17
|
+
* deployTargets normalizes a spec's `deploy` declaration into a target list: a
|
|
18
|
+
* single target wraps in a one-element list, a list passes through, and an
|
|
19
|
+
* absent `deploy` reads as an empty list (the un-migrated fallback). The
|
|
20
|
+
* spec-to-spelling helpers key off this list, so a multi-target spec
|
|
21
|
+
* (cli + env, the Strata shared specs) contributes to every matching helper.
|
|
22
|
+
*/
|
|
23
|
+
export function deployTargets(spec) {
|
|
24
|
+
const deploy = spec.deploy;
|
|
25
|
+
if (deploy === undefined)
|
|
26
|
+
return [];
|
|
27
|
+
// Array.isArray narrows to any[], which does not exclude the readonly list
|
|
28
|
+
// arm, so the single-target arm is cast to keep the union narrow.
|
|
29
|
+
if (Array.isArray(deploy))
|
|
30
|
+
return deploy;
|
|
31
|
+
return [deploy];
|
|
32
|
+
}
|
|
11
33
|
/**
|
|
12
34
|
* flagsFromSpecs turns a spec surface and a config into the CLI flag list: each
|
|
13
35
|
* spec's `toArgv` runs over the config value (falling back to the spec
|
|
14
36
|
* default), and empty results are skipped. A spec with no `toArgv` contributes
|
|
15
|
-
* nothing.
|
|
37
|
+
* nothing. A declared `deploy` target gates the spelling: a `cli` target in the
|
|
38
|
+
* spec's target list (or no target, the un-migrated fallback that reads the
|
|
39
|
+
* `toArgv` presence) spells CLI flags, so a spec that deploys only to the env
|
|
40
|
+
* or a config file contributes no flag even when it carries a `toArgv`
|
|
41
|
+
* spelling.
|
|
16
42
|
*/
|
|
17
43
|
export function flagsFromSpecs(specs, config) {
|
|
18
44
|
const flags = [];
|
|
19
45
|
for (const spec of specs) {
|
|
20
46
|
if (spec.toArgv === undefined)
|
|
21
47
|
continue;
|
|
48
|
+
const targets = deployTargets(spec);
|
|
49
|
+
if (targets.length > 0 && !targets.some((target) => target.target === 'cli'))
|
|
50
|
+
continue;
|
|
22
51
|
const value = config[spec.key] ?? spec.defaultValue;
|
|
23
52
|
if (value === undefined)
|
|
24
53
|
continue;
|
|
@@ -81,14 +110,21 @@ export function customArgEnv(config) {
|
|
|
81
110
|
}
|
|
82
111
|
/**
|
|
83
112
|
* envFromSpecs turns a spec surface and a config into the environment map: each
|
|
84
|
-
* spec's `toEnv` runs over the config value (falling back to the spec
|
|
85
|
-
*
|
|
113
|
+
* spec's `toEnv` runs over the config value (falling back to the spec default).
|
|
114
|
+
* A spec with no `toEnv` contributes nothing. A declared `deploy` target gates
|
|
115
|
+
* the spelling: an `env` target in the spec's target list (or no target, the
|
|
116
|
+
* un-migrated fallback that reads the `toEnv` presence) spells env vars, so a
|
|
117
|
+
* spec that deploys only to the CLI or a config file contributes no var even
|
|
118
|
+
* when it carries a `toEnv` spelling.
|
|
86
119
|
*/
|
|
87
120
|
export function envFromSpecs(specs, config) {
|
|
88
121
|
const env = {};
|
|
89
122
|
for (const spec of specs) {
|
|
90
123
|
if (spec.toEnv === undefined)
|
|
91
124
|
continue;
|
|
125
|
+
const targets = deployTargets(spec);
|
|
126
|
+
if (targets.length > 0 && !targets.some((target) => target.target === 'env'))
|
|
127
|
+
continue;
|
|
92
128
|
const value = config[spec.key] ?? spec.defaultValue;
|
|
93
129
|
if (value === undefined)
|
|
94
130
|
continue;
|
|
@@ -97,65 +133,436 @@ export function envFromSpecs(specs, config) {
|
|
|
97
133
|
}
|
|
98
134
|
return env;
|
|
99
135
|
}
|
|
136
|
+
/**
|
|
137
|
+
* configFileFromSpecs turns a spec surface and a config into the content of the
|
|
138
|
+
* config file the deploy step writes: every spec whose target list carries a
|
|
139
|
+
* `config` target resolves its value (`toConfig(value) ?? value`, falling back
|
|
140
|
+
* to the spec default) and nests it under that target's `block` / `key` (the
|
|
141
|
+
* key defaults to the spec key), then the result renders in the requested
|
|
142
|
+
* format. A spec with no `deploy`, or no `config` target, contributes nothing.
|
|
143
|
+
*
|
|
144
|
+
* A `block` ending in `[]` (e.g. `models[]`) follows the array-entry
|
|
145
|
+
* convention: the key lands inside the single object of a one-element array
|
|
146
|
+
* named by the prefix, and every spec sharing that block merges into that one
|
|
147
|
+
* entry. A plain block name nests as before, into one top-level object.
|
|
148
|
+
*
|
|
149
|
+
* A spec that carries both a `config` and a `cli` target contributes nothing
|
|
150
|
+
* here: the launch spells the CLI flag and leaves the on-disk config file (and
|
|
151
|
+
* any hand edits in it) untouched. Only a spec whose sole deploy path is the
|
|
152
|
+
* config file writes it.
|
|
153
|
+
*
|
|
154
|
+
* The YAML format loads the `yaml` package lazily (a root devDependency, so the
|
|
155
|
+
* JSON and env paths never touch it at runtime); a missing package throws only
|
|
156
|
+
* when the YAML format is requested.
|
|
157
|
+
*/
|
|
158
|
+
export function configFileFromSpecs(specs, config, format) {
|
|
159
|
+
const object = {};
|
|
160
|
+
for (const spec of specs) {
|
|
161
|
+
const targets = deployTargets(spec);
|
|
162
|
+
const deployTarget = targets.find((target) => target.target === 'config');
|
|
163
|
+
if (deployTarget === undefined)
|
|
164
|
+
continue;
|
|
165
|
+
// Prefer the CLI flag over the config write: a spec that also carries a
|
|
166
|
+
// cli target spells the flag and leaves the on-disk config file (and any
|
|
167
|
+
// hand edits in it) untouched.
|
|
168
|
+
if (targets.some((target) => target.target === 'cli'))
|
|
169
|
+
continue;
|
|
170
|
+
const value = config[spec.key] ?? spec.defaultValue;
|
|
171
|
+
if (value === undefined)
|
|
172
|
+
continue;
|
|
173
|
+
const resolved = spec.toConfig === undefined ? value : (spec.toConfig(value) ?? value);
|
|
174
|
+
const block = deployTarget.block;
|
|
175
|
+
const key = deployTarget.key ?? spec.key;
|
|
176
|
+
if (block === undefined) {
|
|
177
|
+
object[key] = resolved;
|
|
178
|
+
continue;
|
|
179
|
+
}
|
|
180
|
+
configBlockEntry(object, block)[key] = resolved;
|
|
181
|
+
}
|
|
182
|
+
switch (format) {
|
|
183
|
+
case 'json':
|
|
184
|
+
return JSON.stringify(object, null, 2);
|
|
185
|
+
case 'env':
|
|
186
|
+
return renderEnv(object);
|
|
187
|
+
case 'yaml':
|
|
188
|
+
return renderYaml(object);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
/**
|
|
192
|
+
* ARRAY_BLOCK_PATTERN matches the array-entry block convention: the name in
|
|
193
|
+
* front of `[]` is the array's key, the `[]` marks the block's keys as entry
|
|
194
|
+
* keys. A plain block name never matches.
|
|
195
|
+
*/
|
|
196
|
+
const ARRAY_BLOCK_PATTERN = /^(?<name>.+)\[\]$/;
|
|
197
|
+
/**
|
|
198
|
+
* configBlockEntry returns the object one config block merges its keys into,
|
|
199
|
+
* creating it on first use. A plain block names a top-level object. A `name[]`
|
|
200
|
+
* block names the single object of a one-element array under `name`, so every
|
|
201
|
+
* spec sharing the block writes into the same entry.
|
|
202
|
+
*/
|
|
203
|
+
function configBlockEntry(object, block) {
|
|
204
|
+
const arrayBlock = ARRAY_BLOCK_PATTERN.exec(block);
|
|
205
|
+
const name = arrayBlock?.groups?.name ?? block;
|
|
206
|
+
const existing = object[name];
|
|
207
|
+
// The array form reads back the entry it created and re-stamps the
|
|
208
|
+
// one-element array, so the rendered shape stays `{"name": [{...}]}`.
|
|
209
|
+
const held = arrayBlock === null
|
|
210
|
+
? existing
|
|
211
|
+
: Array.isArray(existing)
|
|
212
|
+
? existing[0]
|
|
213
|
+
: undefined;
|
|
214
|
+
const entry = held !== null && typeof held === 'object'
|
|
215
|
+
? held
|
|
216
|
+
: {};
|
|
217
|
+
object[name] = arrayBlock === null ? entry : [entry];
|
|
218
|
+
return entry;
|
|
219
|
+
}
|
|
220
|
+
/**
|
|
221
|
+
* renderEnv flattens a nested config object into flat `KEY=VALUE` lines: a
|
|
222
|
+
* top-level block object emits one line per leaf (the leaf key, not the block),
|
|
223
|
+
* and a top-level scalar emits itself. Non-string leaves stringify through
|
|
224
|
+
* String (numbers, booleans) or JSON (objects, arrays).
|
|
225
|
+
*/
|
|
226
|
+
function renderEnv(object) {
|
|
227
|
+
const lines = [];
|
|
228
|
+
for (const [key, value] of Object.entries(object)) {
|
|
229
|
+
if (value !== null && typeof value === 'object') {
|
|
230
|
+
for (const [leaf, leafValue] of Object.entries(value)) {
|
|
231
|
+
lines.push(`${leaf}=${envValue(leafValue)}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
else {
|
|
235
|
+
lines.push(`${key}=${envValue(value)}`);
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
return lines.join('\n');
|
|
239
|
+
}
|
|
240
|
+
/**
|
|
241
|
+
* envValue stringifies one `.env` leaf: strings pass through, numbers and
|
|
242
|
+
* booleans through String, and anything else through JSON.
|
|
243
|
+
*/
|
|
244
|
+
function envValue(value) {
|
|
245
|
+
if (typeof value === 'string')
|
|
246
|
+
return value;
|
|
247
|
+
if (typeof value === 'number' || typeof value === 'boolean')
|
|
248
|
+
return String(value);
|
|
249
|
+
return JSON.stringify(value);
|
|
250
|
+
}
|
|
251
|
+
/**
|
|
252
|
+
* renderYaml stringifies a config object through the `yaml` package. The
|
|
253
|
+
* package is a root devDependency (the YAML format is a test and optional path),
|
|
254
|
+
* so it loads lazily through createRequire and the JSON/env paths never pay for
|
|
255
|
+
* it at runtime.
|
|
256
|
+
*/
|
|
257
|
+
function renderYaml(object) {
|
|
258
|
+
// createRequire anchors the lookup at this file, so the root devDependency
|
|
259
|
+
// resolves through the parent node_modules even from the compiled lib/.
|
|
260
|
+
const { stringify } = createRequire(import.meta.url)('yaml');
|
|
261
|
+
return stringify(object);
|
|
262
|
+
}
|
|
263
|
+
/**
|
|
264
|
+
* The cancel grace period in ms: an aborted run gets SIGINT first, and a child
|
|
265
|
+
* still alive after the grace period gets SIGTERM, so a stuck child cannot
|
|
266
|
+
* keep the run open.
|
|
267
|
+
*/
|
|
268
|
+
const RUN_ABORT_GRACE_MS = 5_000;
|
|
269
|
+
/**
|
|
270
|
+
* The plain-run cap in ms: a 10-minute ceiling, so a hung CUDA build cannot
|
|
271
|
+
* hang the panel. The streaming surface carries no overall cap (a full CUDA
|
|
272
|
+
* build legitimately runs long), so the ceiling applies to run only.
|
|
273
|
+
*/
|
|
274
|
+
const RUN_TIMEOUT_MS = 600_000;
|
|
275
|
+
/**
|
|
276
|
+
* The tail kept of a command's combined output: the detail a failed
|
|
277
|
+
* CommandOutcome carries, and the stdout tail a clean-exit CommandOutcome
|
|
278
|
+
* carries, are each the last KiB of stdout plus stderr, so a chatty run
|
|
279
|
+
* stays readable without growing the host's memory or the log sink.
|
|
280
|
+
*/
|
|
281
|
+
const COMMAND_DETAIL_TAIL = 1_024;
|
|
282
|
+
/**
|
|
283
|
+
* The streaming output's high-water mark and tail: a streamed command's
|
|
284
|
+
* collected output is trimmed to the last KiB once it passes the mark, so a
|
|
285
|
+
* long build's stream cannot grow the host's memory without bound.
|
|
286
|
+
*/
|
|
287
|
+
const STREAM_OUTPUT_HIGH_WATER = 2_048;
|
|
288
|
+
const STREAM_OUTPUT_TAIL = 1_024;
|
|
289
|
+
/**
|
|
290
|
+
* The credential probe's timeout: a hung sudo must not sit in front of the
|
|
291
|
+
* guarded command, so a slow probe reads as "a prompt is coming" (the safe
|
|
292
|
+
* side), matching the service install runner's probe timeout.
|
|
293
|
+
*/
|
|
294
|
+
const SUDO_PROBE_TIMEOUT_MS = 5_000;
|
|
295
|
+
/**
|
|
296
|
+
* sudoProbeCached answers whether sudo already holds cached (or NOPASSWD)
|
|
297
|
+
* credentials: it runs SUDO_PROBE_COMMAND non-interactively and reads a clean
|
|
298
|
+
* exit as cached. A refused -n, a missing sudo, and a hung probe all read as
|
|
299
|
+
* "a prompt is coming", the safe side. Each package owns its child-process
|
|
300
|
+
* wiring (the watchChildAbort note), so the lifecycle host keeps its own copy
|
|
301
|
+
* beside the service's sudoProbe.
|
|
302
|
+
*/
|
|
303
|
+
function sudoProbeCached() {
|
|
304
|
+
const [command, ...args] = SUDO_PROBE_COMMAND;
|
|
305
|
+
if (command === undefined)
|
|
306
|
+
return Promise.resolve(false);
|
|
307
|
+
return new Promise(resolve => {
|
|
308
|
+
execFile(command, args, { timeout: SUDO_PROBE_TIMEOUT_MS }, (error) => {
|
|
309
|
+
resolve(error === null);
|
|
310
|
+
});
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
/**
|
|
314
|
+
* The streaming children createNodeLifecycleHost spawned detached (process-
|
|
315
|
+
* group leaders). signalProcess reads the set so the negative-pid group
|
|
316
|
+
* signal only ever targets a child that actually leads its group: the plain
|
|
317
|
+
* run's exec'd child (exec's options cannot set detached) stays in the
|
|
318
|
+
* host's group and gets the direct signal.
|
|
319
|
+
*/
|
|
320
|
+
const detachedChildren = new WeakSet();
|
|
321
|
+
/**
|
|
322
|
+
* signalProcess delivers one signal to one child: a process-group leader
|
|
323
|
+
* gets it through its negative pid, so a compound command line
|
|
324
|
+
* (`a && sudo b`) dies whole instead of leaving sudo as a group survivor;
|
|
325
|
+
* every other child gets the direct signal. A simple sudo line is already
|
|
326
|
+
* safe there: the shell execs a lone command, so the shell process is sudo
|
|
327
|
+
* itself. A group that is already gone (ESRCH) or a platform that rejects
|
|
328
|
+
* negative pids falls back to the direct signal, which reaches the shell.
|
|
329
|
+
*/
|
|
330
|
+
function signalProcess(child, signal) {
|
|
331
|
+
if (child.pid !== undefined && detachedChildren.has(child)) {
|
|
332
|
+
try {
|
|
333
|
+
process.kill(-child.pid, signal);
|
|
334
|
+
return;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
// Group gone or unsupported: the direct signal below still reaches the
|
|
338
|
+
// shell, and a child that already exited no-ops the way ChildProcess
|
|
339
|
+
// .kill always does.
|
|
340
|
+
}
|
|
341
|
+
}
|
|
342
|
+
child.kill(signal);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* signalChild signals an in-flight child with the cancel rule: SIGINT first,
|
|
346
|
+
* and SIGTERM after the grace period if the child is still alive. The child
|
|
347
|
+
* already marked killed or exited is left alone. The optional graceMs
|
|
348
|
+
* overrides the 5 s constant so a test can exercise the escalation without
|
|
349
|
+
* waiting. The abort watch and the sudo deadline kill share this one rule,
|
|
350
|
+
* the mirror of the service's copy (packages/service/src/index.ts,
|
|
351
|
+
* signalChild): each package owns its child-process wiring, so a change to
|
|
352
|
+
* the escalation rule lands in two places by design.
|
|
353
|
+
*/
|
|
354
|
+
function signalChild(child, graceMs) {
|
|
355
|
+
if (child.killed || child.exitCode !== null)
|
|
356
|
+
return;
|
|
357
|
+
signalProcess(child, 'SIGINT');
|
|
358
|
+
const escalate = setTimeout(() => {
|
|
359
|
+
// A group signal never marks ChildProcess.kill, so the guard reads the
|
|
360
|
+
// exit state: a child that has neither exited nor been signalled is
|
|
361
|
+
// still alive and gets SIGTERM.
|
|
362
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
363
|
+
signalProcess(child, 'SIGTERM');
|
|
364
|
+
}, graceMs ?? RUN_ABORT_GRACE_MS);
|
|
365
|
+
escalate.unref();
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* watchChildAbort kills the in-flight child when the RunOptions signal
|
|
369
|
+
* aborts (the card's cancel of a Get/Launch download) through the shared
|
|
370
|
+
* signalChild rule. A signal that is already aborted kills immediately. A
|
|
371
|
+
* near-identical copy lives in the service package
|
|
372
|
+
* (packages/service/src/index.ts, the download runner's abort path): each
|
|
373
|
+
* package owns its own child-process wiring, so a change to the escalation
|
|
374
|
+
* rule lands in two places by design.
|
|
375
|
+
*/
|
|
376
|
+
function watchChildAbort(child, signal, graceMs) {
|
|
377
|
+
if (signal === undefined)
|
|
378
|
+
return;
|
|
379
|
+
const onAbort = () => {
|
|
380
|
+
signalChild(child, graceMs);
|
|
381
|
+
};
|
|
382
|
+
if (signal.aborted) {
|
|
383
|
+
onAbort();
|
|
384
|
+
return;
|
|
385
|
+
}
|
|
386
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
387
|
+
}
|
|
100
388
|
/**
|
|
101
389
|
* createNodeLifecycleHost builds the node `LifecycleHost`: `run` through the
|
|
102
390
|
* system shell (a 10-minute cap, so a hung CUDA build cannot hang the panel),
|
|
103
|
-
* `spawn` a detached child the stop step can signal,
|
|
104
|
-
*
|
|
391
|
+
* `spawn` a detached child the stop step can signal, `writeFile` the config
|
|
392
|
+
* file the deploy step renders (creating the parent directory), and `fetch`
|
|
393
|
+
* the health URL. A fetch that throws (no route, connection refused) reads as status 0,
|
|
105
394
|
* which the poll treats as not-ready. When `debug` is provided, the host logs
|
|
106
395
|
* every command it issues (each `run` line and each `spawn` argv) through it,
|
|
107
|
-
* so a DSH debug-mode host captures the full build-to-launch sequence.
|
|
396
|
+
* so a DSH debug-mode host captures the full build-to-launch sequence. A
|
|
397
|
+
* `RunOptions.shell` (the engine OS profile's shell, e.g. pwsh.exe for
|
|
398
|
+
* the Windows vcpkg route) runs the command line in that shell instead of the
|
|
399
|
+
* system default. A `RunOptions.graceMs` overrides the 5 s SIGTERM escalation
|
|
400
|
+
* window of the abort path for that run. The optional `sudo` hook wires the
|
|
401
|
+
* sudo-prompt rule: a run whose `RunOptions.needsSudo` declares sudo probes
|
|
402
|
+
* cached credentials first, announces a waiting prompt through the hook once,
|
|
403
|
+
* and dies at the password deadline with the `sudoFailureDetail` outcome. A
|
|
404
|
+
* host built without the hook never probes, announces, or caps a run.
|
|
108
405
|
*/
|
|
109
|
-
export function createNodeLifecycleHost(debug) {
|
|
406
|
+
export function createNodeLifecycleHost(debug, sudo) {
|
|
110
407
|
// The debug logger is a no-op outside debug mode, so the guard reads as
|
|
111
408
|
// "log when a real sink is wired" and the command path stays quiet otherwise.
|
|
112
409
|
const log = debug ?? undefined;
|
|
410
|
+
/**
|
|
411
|
+
* guardSudo drives core's sudo-prompt rule for one declared run: the run
|
|
412
|
+
* carries needsSudo and the host has the hook, so the command probes first,
|
|
413
|
+
* announces once, and arms the password deadline whose fire calls kill. An
|
|
414
|
+
* undeclared run, or a host without the hook, resolves undefined: the run
|
|
415
|
+
* goes through exactly as before.
|
|
416
|
+
*/
|
|
417
|
+
const guardSudo = async (command, options, kill) => {
|
|
418
|
+
if (sudo === undefined || options?.needsSudo !== true)
|
|
419
|
+
return undefined;
|
|
420
|
+
return guardSudoCommand({
|
|
421
|
+
needsSudo: true,
|
|
422
|
+
command,
|
|
423
|
+
probe: sudoProbeCached,
|
|
424
|
+
notify: sudo.notify,
|
|
425
|
+
kill,
|
|
426
|
+
...(sudo.timeoutMs === undefined ? {} : { timeoutMs: sudo.timeoutMs }),
|
|
427
|
+
});
|
|
428
|
+
};
|
|
113
429
|
return {
|
|
114
|
-
async run(command) {
|
|
430
|
+
async run(command, options) {
|
|
115
431
|
log?.(`run: ${command}`);
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
432
|
+
// The guard arms before the exec; the outer child exists so the
|
|
433
|
+
// deadline's kill reaches the run through the shared signalChild rule.
|
|
434
|
+
// The exec'd child cannot spawn detached (exec's options forbid it), so
|
|
435
|
+
// it takes the direct signal; the deploy lines this path runs are
|
|
436
|
+
// single `sudo ln -sf ...` commands, whose shell execs sudo itself.
|
|
437
|
+
let child;
|
|
438
|
+
const guard = await guardSudo(command, options, () => {
|
|
439
|
+
if (child !== undefined)
|
|
440
|
+
signalChild(child, options?.graceMs);
|
|
125
441
|
});
|
|
442
|
+
try {
|
|
443
|
+
const outcome = await new Promise((resolve) => {
|
|
444
|
+
// A profile shell (the Windows vcpkg route's pwsh.exe) runs the
|
|
445
|
+
// command line in that shell; absent keeps the system default.
|
|
446
|
+
const started = exec(command, { timeout: RUN_TIMEOUT_MS, shell: options?.shell }, (error, stdout, stderr) => {
|
|
447
|
+
if (error === null) {
|
|
448
|
+
// A clean exit still carries its output tail: the run's log
|
|
449
|
+
// sink (auditRun, the Get extras fallback) appends it after
|
|
450
|
+
// the `$ <command>` line, so success output reaches the Logs
|
|
451
|
+
// section the way streamed output does.
|
|
452
|
+
const output = `${stdout ?? ''}${stderr ?? ''}`.trim();
|
|
453
|
+
resolve(output === ''
|
|
454
|
+
? { ok: true }
|
|
455
|
+
: { ok: true, stdout: output.slice(-COMMAND_DETAIL_TAIL) });
|
|
456
|
+
return;
|
|
457
|
+
}
|
|
458
|
+
const detail = `${stderr ?? ''}${stdout ?? ''}${String(error)}`.trim().slice(-COMMAND_DETAIL_TAIL);
|
|
459
|
+
resolve({ ok: false, detail });
|
|
460
|
+
});
|
|
461
|
+
child = started;
|
|
462
|
+
watchChildAbort(started, options?.signal, options?.graceMs);
|
|
463
|
+
});
|
|
464
|
+
// The password deadline killed the child: name the missed sudo prompt
|
|
465
|
+
// regardless of the exit the signalled child reported.
|
|
466
|
+
if (guard?.timedOut() === true) {
|
|
467
|
+
return { ok: false, detail: sudoFailureDetail(command) };
|
|
468
|
+
}
|
|
469
|
+
return outcome;
|
|
470
|
+
}
|
|
471
|
+
finally {
|
|
472
|
+
// The command settled (its close or error path resolved it): the
|
|
473
|
+
// password deadline is no longer owed.
|
|
474
|
+
guard?.disarm();
|
|
475
|
+
}
|
|
126
476
|
},
|
|
127
|
-
async runStream(command, onOutput) {
|
|
477
|
+
async runStream(command, onOutput, options) {
|
|
128
478
|
log?.(`run: ${command}`);
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
let output = '';
|
|
134
|
-
const append = (chunk) => {
|
|
135
|
-
const text = chunk.toString('utf8');
|
|
136
|
-
onOutput(text);
|
|
137
|
-
output += text;
|
|
138
|
-
if (output.length > 2048) {
|
|
139
|
-
output = output.slice(-1024);
|
|
140
|
-
}
|
|
141
|
-
};
|
|
142
|
-
child.stdout.on('data', append);
|
|
143
|
-
child.stderr.on('data', append);
|
|
144
|
-
child.on('error', error => {
|
|
145
|
-
resolve({ ok: false, detail: error.message });
|
|
146
|
-
});
|
|
147
|
-
child.on('close', code => {
|
|
148
|
-
if (code === 0) {
|
|
149
|
-
resolve({ ok: true });
|
|
150
|
-
return;
|
|
151
|
-
}
|
|
152
|
-
resolve({ ok: false, detail: `exit ${code}: ${output.trim().slice(-1024)}` });
|
|
153
|
-
});
|
|
479
|
+
let child;
|
|
480
|
+
const guard = await guardSudo(command, options, () => {
|
|
481
|
+
if (child !== undefined)
|
|
482
|
+
signalChild(child, options?.graceMs);
|
|
154
483
|
});
|
|
484
|
+
try {
|
|
485
|
+
const outcome = await new Promise((resolve) => {
|
|
486
|
+
// No overall timeout: a full CUDA build legitimately runs long, and
|
|
487
|
+
// the card prices a stalled step with its own output-idle watchdog.
|
|
488
|
+
// A profile shell (the Windows vcpkg route's pwsh.exe) runs the
|
|
489
|
+
// command line in that shell; absent keeps the system default.
|
|
490
|
+
// The build shell spawns detached so it leads its own process
|
|
491
|
+
// group and signalProcess's negative-pid signal reaches the whole
|
|
492
|
+
// line (build commands are compound). libuv's detached spawn only
|
|
493
|
+
// changes the process group (Node >= 22 ships libuv >= 1.48, which
|
|
494
|
+
// does not create a session), so a sudo prompt still reaches the
|
|
495
|
+
// launching terminal.
|
|
496
|
+
const started = nodeSpawn(command, {
|
|
497
|
+
shell: options?.shell ?? true,
|
|
498
|
+
detached: true,
|
|
499
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
500
|
+
});
|
|
501
|
+
child = started;
|
|
502
|
+
detachedChildren.add(started);
|
|
503
|
+
watchChildAbort(started, options?.signal, options?.graceMs);
|
|
504
|
+
let output = '';
|
|
505
|
+
const append = (chunk) => {
|
|
506
|
+
const text = chunk.toString('utf8');
|
|
507
|
+
onOutput(text);
|
|
508
|
+
output += text;
|
|
509
|
+
if (output.length > STREAM_OUTPUT_HIGH_WATER) {
|
|
510
|
+
output = output.slice(-STREAM_OUTPUT_TAIL);
|
|
511
|
+
}
|
|
512
|
+
};
|
|
513
|
+
started.stdout.on('data', append);
|
|
514
|
+
started.stderr.on('data', append);
|
|
515
|
+
started.on('error', error => {
|
|
516
|
+
resolve({ ok: false, detail: error.message });
|
|
517
|
+
});
|
|
518
|
+
started.on('close', code => {
|
|
519
|
+
if (code === 0) {
|
|
520
|
+
// The stdout tail of a clean exit (see the plain run's rule); a
|
|
521
|
+
// streaming caller has already seen the chunks live, so only a
|
|
522
|
+
// sink-less reader needs the collected tail.
|
|
523
|
+
const tail = output.trim();
|
|
524
|
+
resolve(tail === '' ? { ok: true } : { ok: true, stdout: tail.slice(-COMMAND_DETAIL_TAIL) });
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
resolve({
|
|
528
|
+
ok: false,
|
|
529
|
+
detail: `exit ${code}: ${output.trim().slice(-COMMAND_DETAIL_TAIL)}`,
|
|
530
|
+
});
|
|
531
|
+
});
|
|
532
|
+
});
|
|
533
|
+
// The password deadline killed the child: name the missed sudo prompt
|
|
534
|
+
// regardless of the exit the signalled child reported.
|
|
535
|
+
if (guard?.timedOut() === true) {
|
|
536
|
+
return { ok: false, detail: sudoFailureDetail(command) };
|
|
537
|
+
}
|
|
538
|
+
return outcome;
|
|
539
|
+
}
|
|
540
|
+
finally {
|
|
541
|
+
guard?.disarm();
|
|
542
|
+
}
|
|
155
543
|
},
|
|
156
544
|
spawn(argv, env, cwd) {
|
|
157
545
|
log?.(`spawn: ${argv.join(' ')}${cwd !== undefined ? ` (cwd: ${cwd})` : ''}`);
|
|
158
|
-
|
|
546
|
+
// An empty argv (or an empty command) would spawn '' and hang the watcher
|
|
547
|
+
// on a child that never starts. Settle the handle with a spawn error
|
|
548
|
+
// detail instead, so the launch step reports the missing command.
|
|
549
|
+
const command = argv[0];
|
|
550
|
+
if (command === undefined || command === '') {
|
|
551
|
+
const detail = 'spawn: no command in argv';
|
|
552
|
+
const info = { code: null, signal: null, error: detail };
|
|
553
|
+
return {
|
|
554
|
+
pid: -1,
|
|
555
|
+
kill: () => { },
|
|
556
|
+
logs: detail,
|
|
557
|
+
onExit: (listener) => {
|
|
558
|
+
listener(info);
|
|
559
|
+
},
|
|
560
|
+
// It settled with the spawn error at construction, so the state read
|
|
561
|
+
// reports true from the start.
|
|
562
|
+
hasExited: () => true,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
const child = nodeSpawn(command, argv.slice(1), {
|
|
159
566
|
env: { ...process.env, ...env },
|
|
160
567
|
cwd,
|
|
161
568
|
detached: true,
|
|
@@ -170,7 +577,7 @@ export function createNodeLifecycleHost(debug) {
|
|
|
170
577
|
// bound; the tail is what the log file and the card keep.
|
|
171
578
|
let logs = '';
|
|
172
579
|
const append = (chunk) => {
|
|
173
|
-
logs = (logs + chunk.toString('utf8')).slice(-
|
|
580
|
+
logs = (logs + chunk.toString('utf8')).slice(-LOG_TAIL_BYTES);
|
|
174
581
|
};
|
|
175
582
|
child.stdout?.on('data', append);
|
|
176
583
|
child.stderr?.on('data', append);
|
|
@@ -180,10 +587,10 @@ export function createNodeLifecycleHost(debug) {
|
|
|
180
587
|
// breaks on a crashing child both emit 'error' here, so every path gets
|
|
181
588
|
// a listener and the failure lands in the logs instead of the crash.
|
|
182
589
|
child.stdout?.on('error', (error) => {
|
|
183
|
-
logs = (logs + `stdout error: ${error.message}\n`).slice(-
|
|
590
|
+
logs = (logs + `stdout error: ${error.message}\n`).slice(-LOG_TAIL_BYTES);
|
|
184
591
|
});
|
|
185
592
|
child.stderr?.on('error', (error) => {
|
|
186
|
-
logs = (logs + `stderr error: ${error.message}\n`).slice(-
|
|
593
|
+
logs = (logs + `stderr error: ${error.message}\n`).slice(-LOG_TAIL_BYTES);
|
|
187
594
|
});
|
|
188
595
|
// The one-shot exit contract: 'error' names a child that never ran,
|
|
189
596
|
// 'exit' carries the code or signal, and 'close' follows once the pipes
|
|
@@ -208,7 +615,7 @@ export function createNodeLifecycleHost(debug) {
|
|
|
208
615
|
};
|
|
209
616
|
let exitSignal = null;
|
|
210
617
|
child.on('error', (error) => {
|
|
211
|
-
logs = (logs + `spawn error: ${error.message}\n`).slice(-
|
|
618
|
+
logs = (logs + `spawn error: ${error.message}\n`).slice(-LOG_TAIL_BYTES);
|
|
212
619
|
// The error fires before the stdio drains; settle immediately so a
|
|
213
620
|
// launch of a missing binary reports rather than hangs the watcher.
|
|
214
621
|
settle({ code: null, signal: null, error: error.message });
|
|
@@ -237,8 +644,19 @@ export function createNodeLifecycleHost(debug) {
|
|
|
237
644
|
}
|
|
238
645
|
listeners.add(listener);
|
|
239
646
|
},
|
|
647
|
+
// A state read of the settle state the onExit listener reads: a live
|
|
648
|
+
// child reports false, and a child that ended (a crash, a pkill, or
|
|
649
|
+
// a spawn failure) reports true.
|
|
650
|
+
hasExited: () => exitInfo !== null,
|
|
240
651
|
};
|
|
241
652
|
},
|
|
653
|
+
async writeFile(path, content) {
|
|
654
|
+
log?.(`writeFile: ${path}`);
|
|
655
|
+
// The parent directory may not exist (a fresh model directory), so the
|
|
656
|
+
// mkdir runs before the write; recursive keeps an existing dir a no-op.
|
|
657
|
+
await mkdir(dirname(path), { recursive: true });
|
|
658
|
+
await fsWriteFile(path, content);
|
|
659
|
+
},
|
|
242
660
|
async fetch(url) {
|
|
243
661
|
try {
|
|
244
662
|
const response = await fetch(url);
|
|
@@ -276,29 +694,138 @@ export function trackChild(children, key, handle) {
|
|
|
276
694
|
}
|
|
277
695
|
});
|
|
278
696
|
}
|
|
697
|
+
/**
|
|
698
|
+
* resolveLaunchArgv resolves a plan's launch argv against the engine
|
|
699
|
+
* directory the spawn runs in: a relative path that carries a separator (a
|
|
700
|
+
* build-tree binary, the NInfer Windows tree) is joined into an absolute
|
|
701
|
+
* path, and a bare name (a binary on the path after the rebuild's symlink
|
|
702
|
+
* step) is left as-is, so the spawn resolves the right command on every
|
|
703
|
+
* platform.
|
|
704
|
+
*/
|
|
705
|
+
export function resolveLaunchArgv(engineDir, argv) {
|
|
706
|
+
const resolved = [...(argv ?? [])];
|
|
707
|
+
const first = resolved[0];
|
|
708
|
+
if (engineDir !== undefined &&
|
|
709
|
+
first !== undefined &&
|
|
710
|
+
!isAbsolute(first) &&
|
|
711
|
+
(first.includes('\\') || first.includes('/'))) {
|
|
712
|
+
resolved[0] = join(engineDir, first);
|
|
713
|
+
}
|
|
714
|
+
return resolved;
|
|
715
|
+
}
|
|
716
|
+
/**
|
|
717
|
+
* auditRun runs one mutating host command through the run's log sink: the
|
|
718
|
+
* `$ <command>` line reaches the sink before the child spawns, and the
|
|
719
|
+
* outcome's success output tail reaches it after the child settles, so the
|
|
720
|
+
* launch or stop run reads the command ahead of its result (the URL-query
|
|
721
|
+
* rule applied to execs: line first, settled outcome after). No sink (a
|
|
722
|
+
* headless call) and the command runs as before. A failed outcome lands no
|
|
723
|
+
* output here: the caller's own failure line carries the detail's output
|
|
724
|
+
* tail. The deploy and stop steps share this one audit rule for their direct
|
|
725
|
+
* `host.run` calls; the git and build steps run through the acquire step's
|
|
726
|
+
* own command-first echo.
|
|
727
|
+
*/
|
|
728
|
+
export async function auditRun(host, command, log, options) {
|
|
729
|
+
log?.(commandLogLine(command));
|
|
730
|
+
const outcome = await host.run(command, options);
|
|
731
|
+
if (log !== undefined && outcome.ok && outcome.stdout !== undefined && outcome.stdout !== '') {
|
|
732
|
+
// The host capped the tail (COMMAND_DETAIL_TAIL), so a docker pull or an
|
|
733
|
+
// hf download lands its closing lines without refilling the run.
|
|
734
|
+
log(outcome.stdout.endsWith('\n') ? outcome.stdout : `${outcome.stdout}\n`);
|
|
735
|
+
}
|
|
736
|
+
return outcome;
|
|
737
|
+
}
|
|
738
|
+
/**
|
|
739
|
+
* stopProcessPlan signals the engine process a stop targets when the adapter
|
|
740
|
+
* tracks no child handle for it: an engine a DSH restart re-adopted from a
|
|
741
|
+
* stored launch record carries no in-memory handle, so the stop signals the
|
|
742
|
+
* pid the record stamped on the plan (kill on Linux and macOS, taskkill on a
|
|
743
|
+
* Windows plan). The optional `log` is the stop run's log sink: the signal
|
|
744
|
+
* command's `$ <command>` line reaches it before the child runs. A plan that
|
|
745
|
+
* names no pid is a no-op (nothing to signal), and a kill the host reports as
|
|
746
|
+
* failed throws with the host's detail, so the stop route names the broken
|
|
747
|
+
* step in the card's Logs section.
|
|
748
|
+
*/
|
|
749
|
+
export async function stopProcessPlan(host, plan, log) {
|
|
750
|
+
if (plan.pid === undefined)
|
|
751
|
+
return;
|
|
752
|
+
const command = plan.os === 'windows' ? `taskkill /PID ${plan.pid} /T` : `kill ${plan.pid}`;
|
|
753
|
+
const outcome = await auditRun(host, command, log);
|
|
754
|
+
if (!outcome.ok) {
|
|
755
|
+
throw new Error(`stop: signal pid ${plan.pid} failed: ${outcome.detail ?? 'unknown'}`);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
/**
|
|
759
|
+
* The health poll's defaults: the interval between attempts in ms and the
|
|
760
|
+
* attempt cap. The service passes the config's healthPollMs and
|
|
761
|
+
* healthMaxAttempts through, and an adapter that polls without options (or
|
|
762
|
+
* with a partial one) reads these, so the defaults have one home.
|
|
763
|
+
*/
|
|
764
|
+
export const HEALTH_POLL_DEFAULT_MS = 1_000;
|
|
765
|
+
export const HEALTH_POLL_MAX_ATTEMPTS = 120;
|
|
766
|
+
/**
|
|
767
|
+
* logHealthPoll brackets a health poll with the URL-query logging rule (the
|
|
768
|
+
* exec rule's URL twin): the one `> GET <url>` line reaches the run's log
|
|
769
|
+
* sink before the first request, the retries stay silent, and the settled
|
|
770
|
+
* verdict line (`> GET <url> ready`, Strata's still-downloading line, or the
|
|
771
|
+
* `!!` failure with the last detail) lands after the poll returns. The
|
|
772
|
+
* shared poll and the Strata poll both wrap their loop in this, so the rule
|
|
773
|
+
* has one home. `log` absent (a headless poll) runs the poll unlogged.
|
|
774
|
+
*/
|
|
775
|
+
export async function logHealthPoll(log, url, poll) {
|
|
776
|
+
log?.(urlLogLine(url));
|
|
777
|
+
const result = await poll();
|
|
778
|
+
if (log !== undefined) {
|
|
779
|
+
log(result.healthy
|
|
780
|
+
? urlLogLine(url, 'ready')
|
|
781
|
+
: result.state === 'downloading'
|
|
782
|
+
? urlLogLine(url, `still downloading${result.detail !== undefined ? `: ${result.detail}` : ''}`)
|
|
783
|
+
: `!! health check failed for ${url}: ${result.detail ?? `last status ${result.status ?? 'unknown'}`}\n`);
|
|
784
|
+
}
|
|
785
|
+
return result;
|
|
786
|
+
}
|
|
279
787
|
/**
|
|
280
788
|
* pollHealth polls a plan's health URL until a 200 or the attempt cap, the
|
|
281
789
|
* same loop the qwen-ninfer launcher runs. A poll interval of 0 skips the
|
|
282
|
-
* delay, which the tests use to run the loop without a real sleep.
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
790
|
+
* delay, which the tests use to run the loop without a real sleep. `handle`
|
|
791
|
+
* is the tracked child the deploy step spawned: when it reports it has
|
|
792
|
+
* exited (hasExited true), the poll ends immediately with a failed result
|
|
793
|
+
* instead of running the full attempt cap, so a crash reports at once. A
|
|
794
|
+
* handle that cannot report its exit state (a stub) keeps the full poll.
|
|
795
|
+
* The poll's URL logging (the one `> GET` line ahead of the settled verdict)
|
|
796
|
+
* rides the shared logHealthPoll wrap when options.onLog carries the run's
|
|
797
|
+
* log sink.
|
|
798
|
+
*/
|
|
799
|
+
export async function pollHealth(host, plan, options, handle) {
|
|
800
|
+
return logHealthPoll(options?.onLog, plan.healthUrl, async () => {
|
|
801
|
+
const pollMs = options?.pollMs ?? HEALTH_POLL_DEFAULT_MS;
|
|
802
|
+
const maxAttempts = options?.maxAttempts ?? HEALTH_POLL_MAX_ATTEMPTS;
|
|
803
|
+
let last = { status: 0, ok: false };
|
|
804
|
+
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
|
|
805
|
+
// The state read runs before the fetch: a child that settled (a crash,
|
|
806
|
+
// a pkill, or a spawn failure) will never report 200, so the poll ends
|
|
807
|
+
// here with a failed result naming the exit.
|
|
808
|
+
if (handle?.hasExited?.() === true) {
|
|
809
|
+
return {
|
|
810
|
+
healthy: false,
|
|
811
|
+
state: 'failed',
|
|
812
|
+
detail: 'the engine process exited before it reported ready',
|
|
813
|
+
};
|
|
814
|
+
}
|
|
815
|
+
last = await host.fetch(plan.healthUrl);
|
|
816
|
+
if (last.status === 200) {
|
|
817
|
+
return { healthy: true, status: 200 };
|
|
818
|
+
}
|
|
819
|
+
if (pollMs > 0 && attempt < maxAttempts) {
|
|
820
|
+
await delay(pollMs);
|
|
821
|
+
}
|
|
295
822
|
}
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
};
|
|
823
|
+
return {
|
|
824
|
+
healthy: false,
|
|
825
|
+
status: last.status,
|
|
826
|
+
detail: `health check did not return 200 within ${maxAttempts} attempts`,
|
|
827
|
+
};
|
|
828
|
+
});
|
|
302
829
|
}
|
|
303
830
|
/**
|
|
304
831
|
* delay waits `ms` milliseconds. The health polls use it between attempts; a
|
|
@@ -310,4 +837,4 @@ export function delay(ms) {
|
|
|
310
837
|
setTimeout(resolve, ms);
|
|
311
838
|
});
|
|
312
839
|
}
|
|
313
|
-
//# sourceMappingURL=adapters.js.map
|
|
840
|
+
//# sourceMappingURL=adapters.js.map
|