dsh-plugin-local-infer 0.1.2 → 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 +32 -6
- package/cordis.patch.yml +7 -0
- package/docs/zh/README.md +24 -4
- package/dsh/client.js +4200 -4049
- package/package.json +33 -11
- package/packages/core/lib/adapter.d.ts +177 -11
- 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 +48 -20
- 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 +25 -14
- package/packages/core/lib/registrar.js +30 -10
- 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 +95 -15
- package/packages/providers/lib/acquire.js +239 -40
- package/packages/providers/lib/adapters.d.ts +155 -221
- package/packages/providers/lib/adapters.js +589 -935
- package/packages/providers/lib/debug.d.ts +1 -1
- package/packages/providers/lib/debug.js +1 -1
- package/packages/providers/lib/deps.d.ts +31 -212
- package/packages/providers/lib/deps.js +71 -205
- 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/{hyperqwen.d.ts → engines/hyperqwen/hyperqwen.d.ts} +2 -2
- package/packages/providers/lib/{hyperqwen.js → engines/hyperqwen/hyperqwen.js} +14 -2
- package/packages/providers/lib/engines/hyperqwen/hyperqwenAdapter.d.ts +38 -0
- package/packages/providers/lib/engines/hyperqwen/hyperqwenAdapter.js +115 -0
- 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/{infernix.d.ts → engines/infernix/infernix.d.ts} +7 -4
- package/packages/providers/lib/{infernix.js → engines/infernix/infernix.js} +53 -8
- package/packages/providers/lib/engines/infernix/infernixAdapter.d.ts +52 -0
- package/packages/providers/lib/engines/infernix/infernixAdapter.js +235 -0
- 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/{llamacpp.d.ts → engines/llamacpp/llamacpp.d.ts} +2 -2
- package/packages/providers/lib/{llamacpp.js → engines/llamacpp/llamacpp.js} +12 -2
- package/packages/providers/lib/engines/llamacpp/llamacppAdapter.d.ts +50 -0
- package/packages/providers/lib/engines/llamacpp/llamacppAdapter.js +274 -0
- 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/{ninfer.d.ts → engines/ninfer/ninfer.d.ts} +2 -2
- package/packages/providers/lib/{ninfer.js → engines/ninfer/ninfer.js} +25 -3
- package/packages/providers/lib/engines/ninfer/ninferAdapter.d.ts +81 -0
- package/packages/providers/lib/engines/ninfer/ninferAdapter.js +392 -0
- 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/strata.js +264 -0
- package/packages/providers/lib/engines/strata/strataAdapter.d.ts +97 -0
- package/packages/providers/lib/engines/strata/strataAdapter.js +463 -0
- 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 +8 -5
- package/packages/providers/lib/engines.js +22 -11
- 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 -24
- package/packages/providers/lib/index.js +46 -24
- package/packages/providers/lib/model-defaults.d.ts +10 -5
- package/packages/providers/lib/model-defaults.js +47 -5
- package/packages/providers/lib/platform.d.ts +8 -5
- package/packages/providers/lib/platform.js +27 -7
- 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 +187 -23
- package/packages/service/lib/index.js +667 -90
- 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 +166 -8
- 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 +89 -0
- package/packages/ui/lib/config.js +251 -0
- package/packages/ui/lib/deps.d.ts +4 -4
- 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 +216 -0
- package/packages/ui/lib/ops/engine.js +697 -0
- 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 +42 -0
- package/packages/ui/lib/ops/hf.js +150 -0
- package/packages/ui/lib/ops/logs.d.ts +78 -0
- package/packages/ui/lib/ops/logs.js +119 -0
- package/packages/ui/lib/ops/model.d.ts +71 -0
- package/packages/ui/lib/ops/model.js +123 -0
- package/packages/ui/lib/request.d.ts +106 -0
- package/packages/ui/lib/request.js +80 -0
- package/packages/ui/lib/route.d.ts +106 -422
- package/packages/ui/lib/route.js +136 -1845
- package/packages/ui/lib/routes/chat-template.d.ts +1 -0
- package/packages/ui/lib/routes/chat-template.js +1 -0
- package/packages/ui/lib/routes/deps.d.ts +1 -0
- package/packages/ui/lib/routes/deps.js +1 -0
- package/packages/ui/lib/routes/download.d.ts +1 -0
- package/packages/ui/lib/routes/download.js +1 -0
- package/packages/ui/lib/routes/get.d.ts +1 -0
- package/packages/ui/lib/routes/get.js +106 -0
- package/packages/ui/lib/routes/install.d.ts +16 -0
- package/packages/ui/lib/routes/install.js +36 -0
- package/packages/ui/lib/routes/launch.d.ts +24 -0
- package/packages/ui/lib/routes/launch.js +74 -0
- 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 +15 -0
- package/packages/ui/lib/routes/provider-delete.js +37 -0
- package/packages/ui/lib/routes/register.d.ts +16 -0
- package/packages/ui/lib/routes/register.js +28 -0
- package/packages/ui/lib/routes/save.d.ts +18 -0
- package/packages/ui/lib/routes/save.js +159 -0
- package/packages/ui/lib/routes/stop.d.ts +19 -0
- package/packages/ui/lib/routes/stop.js +32 -0
- 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 +15 -0
- package/packages/ui/lib/routes/summary.js +26 -0
- package/packages/ui/lib/routes/unregister.d.ts +14 -0
- package/packages/ui/lib/routes/unregister.js +26 -0
- package/packages/ui/lib/viewmodels/deps.d.ts +1 -0
- package/packages/ui/lib/viewmodels/deps.js +1 -0
- package/packages/ui/lib/viewmodels/depsHelpers.d.ts +1 -0
- package/packages/ui/lib/viewmodels/depsHelpers.js +94 -0
- package/packages/ui/lib/viewmodels/summary.d.ts +145 -0
- package/packages/ui/lib/viewmodels/summary.js +245 -0
- package/packages/ui/lib/viewmodels/summaryHelpers.d.ts +30 -0
- package/packages/ui/lib/viewmodels/summaryHelpers.js +42 -0
- 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/strata.d.ts +0 -15
- package/packages/providers/lib/strata.js +0 -14
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* mod-search bridge.
|
|
18
18
|
* @module dsh-plugin-local-infer/service
|
|
19
19
|
*/
|
|
20
|
-
import {
|
|
20
|
+
import { execFile, spawn } from 'node:child_process';
|
|
21
21
|
import { existsSync, mkdirSync, readdirSync, readFileSync, writeFileSync } from 'node:fs';
|
|
22
22
|
import { availableParallelism, cpus, homedir, platform, release, totalmem } from 'node:os';
|
|
23
23
|
import { dirname, join } from 'node:path';
|
|
@@ -25,7 +25,10 @@ import { Service } from '@deepseek-ai/cordis';
|
|
|
25
25
|
// Relative, not the root package subpath: a self-referencing bare specifier
|
|
26
26
|
// only resolves when the package sits inside a node_modules tree, so a
|
|
27
27
|
// local-path or pnpm-linked install of this repo would fail the import.
|
|
28
|
-
import { AGENT_DEFAULT_MODEL_NAMESPACE, agentDefaultOps, agentDefaultPointsAt, agentDefaultUnsetOps, buildModelEntry, detectHardware as detectHardwareFromProbes, detectPackageManager, evaluateGuidelines, estimateVram, hfChatTemplateCommand, hfDownloadCommand, isGpuBusy, LLM_PI_AI_NAMESPACE, matchLaunchSignature, modelGuidelines, reLinkRecord, computeProviderStatuses, modelRowsFrom, parseNvidiaSmiGpu, parseNvidiaSmiGpuUsage, presetFromFetch, providerRegistrationOps, providerRemovalOps, removeModelRow, renderInstallString, resolveDefaults as resolveDefaultsFromPresets, resolveOsReserve, runInstall, } from '../../core/lib/index.js';
|
|
28
|
+
import { AGENT_DEFAULT_MODEL_NAMESPACE, agentDefaultOps, agentDefaultPointsAt, agentDefaultUnsetOps, buildModelEntry, commandLogLine, detectHardware as detectHardwareFromProbes, detectPackageManager, evaluateGuidelines, estimateVram, guardSudoCommand, hfChatTemplateCommand, hfDatasetsCacheDirectory, hfDownloadCommand, hfHubCacheDirectory, isGpuBusy, LLM_PI_AI_NAMESPACE, matchLaunchSignature, modelGuidelines, reLinkRecord, computeProviderStatuses, modelRowsFrom, parseNvidiaSmiGpu, parseNvidiaSmiGpuUsage, presetFromFetch, providerRegistrationOps, providerRemovalOps, removeModelRow, renderInstallString, resolveDefaults as resolveDefaultsFromPresets, resolveOsReserve, runInstall, SUDO_PROBE_COMMAND, sudoFailureDetail, } from '../../core/lib/index.js';
|
|
29
|
+
import { RunLogStore, runLogKey } from "./run-logs.js";
|
|
30
|
+
import { findLaunchTarget } from "./launch-target.js";
|
|
31
|
+
export { RunLogStore, runLogKey, findLaunchTarget };
|
|
29
32
|
/**
|
|
30
33
|
* Local-inference failure. Extends Error with a stable `code`
|
|
31
34
|
* (`LOCAL_INFER_CONFLICT`, `LOCAL_INFER_INVALID_ENGINE`,
|
|
@@ -46,6 +49,13 @@ export class LocalInferError extends Error {
|
|
|
46
49
|
* "not available" instead.
|
|
47
50
|
*/
|
|
48
51
|
const PROBE_TIMEOUT_MS = 5000;
|
|
52
|
+
/**
|
|
53
|
+
* The page-fetch deadline the modsearch CLI is told to cap itself with (its
|
|
54
|
+
* --deadline argument, milliseconds as text): the CLI kills its own fetch
|
|
55
|
+
* past this, so the spawned bridge process returns instead of sitting on an
|
|
56
|
+
* unreachable URL.
|
|
57
|
+
*/
|
|
58
|
+
const MODSEARCH_DEADLINE_ARG = '60000';
|
|
49
59
|
/**
|
|
50
60
|
* The adopted-process poll interval. A re-adopted engine (a surviving process
|
|
51
61
|
* with no ChildProcess after a DSH restart) is watched for death through a
|
|
@@ -77,8 +87,8 @@ export class LocalInfer extends Service {
|
|
|
77
87
|
// confirm which model a live engine serves: a port the plugin launched reads
|
|
78
88
|
// as that model, a port it did not launch (a foreign engine) has no entry and
|
|
79
89
|
// reads as "engine running, model unknown". The map resets on a host
|
|
80
|
-
// restart, so a fresh start tracks only the launches it made itself.
|
|
81
|
-
//
|
|
90
|
+
// restart, so a fresh start tracks only the launches it made itself. A full
|
|
91
|
+
// PID cache is a future refinement.
|
|
82
92
|
launchedModels = new Map();
|
|
83
93
|
// The live ports tracked per launched engine (launches and adopted re-links
|
|
84
94
|
// both register). The death handler clears the engine's plugin-launched flag
|
|
@@ -94,11 +104,37 @@ export class LocalInfer extends Service {
|
|
|
94
104
|
// handle. A summary re-link that adopts this DSH's own launch record must
|
|
95
105
|
// not stack a poll watcher on top of the handle's close event.
|
|
96
106
|
handledPids = new Set();
|
|
107
|
+
// The engine keys (engineId:port) whose launch this service is in flight
|
|
108
|
+
// for: the deploy has started and the health poll has not settled. While a
|
|
109
|
+
// key is here the provider-status detector keeps the provider's linked read
|
|
110
|
+
// (running, not unsure), so an engine that sizes its own model (Strata, no
|
|
111
|
+
// model tie) or a launch mid-poll never flashes the "Unable to link" banner.
|
|
112
|
+
// The key clears when the launch settles (a healthy poll, a failed deploy).
|
|
113
|
+
launchingEngines = new Set();
|
|
114
|
+
// The engine keys (engineId:port) whose stop this service is in flight for:
|
|
115
|
+
// the stop step signalled the engine and the process has not died yet. While
|
|
116
|
+
// a key is here the detector keeps the linked read, so a stop never reads as
|
|
117
|
+
// an unlinked provider for the moment the engine takes to shut down. The key
|
|
118
|
+
// clears when the process dies (the death observer) or the detector sees the
|
|
119
|
+
// process down (a stop of an engine it cannot watch).
|
|
120
|
+
stoppingEngines = new Set();
|
|
97
121
|
// The route-registered hook for engine process deaths (a crash, a pkill, or
|
|
98
122
|
// a poll-detected death of an adopted process). The observer persists the
|
|
99
123
|
// provider entry's cleared state; a headless profile registers none and the
|
|
100
124
|
// service still settles its own live state.
|
|
101
125
|
deathObserver;
|
|
126
|
+
// The run-registered hooks for sudo commands the guard found waiting for a
|
|
127
|
+
// password on the CLI, one slot per run key (the runLogKey rule every run
|
|
128
|
+
// path shares). A guarded command announces through notifySudoNeeded to
|
|
129
|
+
// every registered observer (one CLI password wait blocks every plugin
|
|
130
|
+
// run), so concurrent runs keep their own note routing and a settle
|
|
131
|
+
// clears only its own slot. A headless profile registers none and the
|
|
132
|
+
// guard still announces internally and caps the wait.
|
|
133
|
+
sudoObservers = new Map();
|
|
134
|
+
// The shared per-run live log hub every provider process writes its output
|
|
135
|
+
// to, keyed by the runLogKey rule so the run paths and the streaming logs
|
|
136
|
+
// route predict one key per provider.
|
|
137
|
+
runLogs = new RunLogStore();
|
|
102
138
|
constructor(ctx) {
|
|
103
139
|
super(ctx, 'localInfer');
|
|
104
140
|
}
|
|
@@ -352,17 +388,41 @@ export class LocalInfer extends Service {
|
|
|
352
388
|
const port = signatures[index]?.port;
|
|
353
389
|
return port !== undefined ? { ...provider, port } : provider;
|
|
354
390
|
});
|
|
355
|
-
|
|
391
|
+
const statuses = computeProviderStatuses({ providers: priced, running, gpuBusy: isGpuBusy(gpuUsage) });
|
|
392
|
+
// The in-flight launch and stop state machine: a provider the launch or
|
|
393
|
+
// stop step is in flight for keeps its linked read (running, not unsure)
|
|
394
|
+
// while the engine process is still up, so neither a stop (the record is
|
|
395
|
+
// cleared but the process is still shutting down) nor a launch (an engine
|
|
396
|
+
// that sizes its own model carries no model tie) flashes the "Unable to
|
|
397
|
+
// link" banner. A stopped provider whose process is down reads as stopped,
|
|
398
|
+
// and the stopping flag clears with it, so a stop of an engine the service
|
|
399
|
+
// cannot watch (no death event) never leaves a stale flag behind.
|
|
400
|
+
for (const provider of priced) {
|
|
401
|
+
const key = this.engineKey(provider.engine, provider.port);
|
|
402
|
+
const launching = this.launchingEngines.has(key);
|
|
403
|
+
const stopping = this.stoppingEngines.has(key);
|
|
404
|
+
if (!launching && !stopping)
|
|
405
|
+
continue;
|
|
406
|
+
if (statuses[provider.id] === 'unsure') {
|
|
407
|
+
statuses[provider.id] = 'running';
|
|
408
|
+
}
|
|
409
|
+
else if (stopping) {
|
|
410
|
+
this.stoppingEngines.delete(key);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
return statuses;
|
|
356
414
|
}
|
|
357
415
|
/**
|
|
358
|
-
* unsureBannerKillCommands returns the
|
|
416
|
+
* unsureBannerKillCommands returns the stop commands for the engine
|
|
359
417
|
* processes the detector saw holding VRAM, for the "unsure" banner the card
|
|
360
418
|
* shows when no provider is confirmed running but at least one reads as
|
|
361
419
|
* unsure. A process is included when its command line carries an engine
|
|
362
420
|
* binary marker and the card's VRAM usage is above 30% of the total (the
|
|
363
421
|
* threshold below which a foreign engine is not considered to be holding
|
|
364
|
-
* the card). The
|
|
365
|
-
*
|
|
422
|
+
* the card). The command form switches on the host OS: one
|
|
423
|
+
* `Stop-Process -Id <pid>` per matching process on Windows (the PowerShell
|
|
424
|
+
* cmdlet the banner's text area runs), one `kill <pid>` on Linux and macOS,
|
|
425
|
+
* in pid order. An empty list means no matching process was seen, so the
|
|
366
426
|
* banner's text area renders empty.
|
|
367
427
|
*/
|
|
368
428
|
async unsureBannerKillCommands(options) {
|
|
@@ -382,10 +442,13 @@ export class LocalInfer extends Service {
|
|
|
382
442
|
gpuUsage.usedMiB / gpuUsage.totalMiB < 0.3) {
|
|
383
443
|
return [];
|
|
384
444
|
}
|
|
445
|
+
// The stop command form follows the host OS: Stop-Process on Windows,
|
|
446
|
+
// kill on Linux and macOS.
|
|
447
|
+
const isWindows = platform() === 'win32';
|
|
385
448
|
const commands = [];
|
|
386
449
|
for (const line of processes) {
|
|
387
450
|
if (markers.some((marker) => line.command.includes(marker))) {
|
|
388
|
-
commands.push(`kill ${line.pid}`);
|
|
451
|
+
commands.push(isWindows ? `Stop-Process -Id ${line.pid}` : `kill ${line.pid}`);
|
|
389
452
|
}
|
|
390
453
|
}
|
|
391
454
|
return commands;
|
|
@@ -415,14 +478,27 @@ export class LocalInfer extends Service {
|
|
|
415
478
|
* deps resolves the host deps an engine's deploy step needs for a build
|
|
416
479
|
* mode. A registered adapter that declares systemDeps wins; the provider-
|
|
417
480
|
* registered plan for the engine id is the fallback, and an engine with
|
|
418
|
-
* neither plans an empty list (nothing to install, the gate passes).
|
|
481
|
+
* neither plans an empty list (nothing to install, the gate passes). The
|
|
482
|
+
* local list is per-OS: a Windows host reads the plan's windows entry (the
|
|
483
|
+
* winget toolchain the providers sourced from the Windows profile), every
|
|
484
|
+
* other host reads the linux entry, so the System dependencies section
|
|
485
|
+
* shows the right toolchain for the host.
|
|
419
486
|
*/
|
|
420
487
|
async deps(engineId, buildMode, hw) {
|
|
421
488
|
const adapter = this.engineRegistry.get(engineId);
|
|
422
489
|
if (adapter !== undefined && typeof adapter.systemDeps === 'function') {
|
|
423
490
|
return adapter.systemDeps(buildMode, hw);
|
|
424
491
|
}
|
|
425
|
-
|
|
492
|
+
const plan = this.depPlanRegistry.get(engineId);
|
|
493
|
+
if (plan === undefined)
|
|
494
|
+
return [];
|
|
495
|
+
if (buildMode === 'local') {
|
|
496
|
+
// A Windows host reads the windows entry; every other host (linux,
|
|
497
|
+
// macos) reads the linux entry, which the providers source from the
|
|
498
|
+
// shared Linux profile.
|
|
499
|
+
return plan.local?.[hw.os] ?? plan.local?.linux ?? [];
|
|
500
|
+
}
|
|
501
|
+
return plan[buildMode] ?? [];
|
|
426
502
|
}
|
|
427
503
|
/**
|
|
428
504
|
* installString renders the copy/paste block for the resolved dep plan,
|
|
@@ -435,12 +511,29 @@ export class LocalInfer extends Service {
|
|
|
435
511
|
}
|
|
436
512
|
/**
|
|
437
513
|
* runInstall is the Play/Run path: it runs the resolved dep plan's commands
|
|
438
|
-
* through the shell runner and re-probes each dep. The runner
|
|
439
|
-
* per-command exit, so a failure names the command that broke
|
|
514
|
+
* through the guarded shell runner and re-probes each dep. The runner
|
|
515
|
+
* captures the per-command exit, so a failure names the command that broke,
|
|
516
|
+
* and a declared-sudo command whose probe finds no cached credentials
|
|
517
|
+
* announces the CLI prompt through the sudo observer and fails at the
|
|
518
|
+
* sudo deadline. options.onLog is the install run's log sink: the command
|
|
519
|
+
* line reaches it as a `$ <command>` line before the guarded runner spawns
|
|
520
|
+
* the child (and before the sudo guard's probe announces), so the run text
|
|
521
|
+
* carries the command ahead of the per-dep results. options.runner injects
|
|
522
|
+
* the command runner so a test reads the command lines without a real shell.
|
|
440
523
|
*/
|
|
441
|
-
async runInstall(engineId, buildMode, hw) {
|
|
524
|
+
async runInstall(engineId, buildMode, hw, options) {
|
|
442
525
|
const deps = await this.deps(engineId, buildMode, hw);
|
|
443
|
-
|
|
526
|
+
// The guarded runner is built per call so every sudo announcement
|
|
527
|
+
// reaches the observers registered when this call runs.
|
|
528
|
+
const runner = options?.runner ?? guardedShellRunner((command) => this.notifySudoNeeded(command));
|
|
529
|
+
// The audit wrapper logs each command the core provisioner actually runs
|
|
530
|
+
// (a satisfied dep never runs, so it never logs) before delegating to the
|
|
531
|
+
// runner. Keeping the wrapper here leaves the core provisioner pure.
|
|
532
|
+
const loggedRunner = (command, runOptions) => {
|
|
533
|
+
options?.onLog?.(commandLogLine(command));
|
|
534
|
+
return runner(command, runOptions);
|
|
535
|
+
};
|
|
536
|
+
return runInstall(deps, hw, loggedRunner);
|
|
444
537
|
}
|
|
445
538
|
/**
|
|
446
539
|
* launch is the lifecycle entry point: it resolves the adapter that rendered
|
|
@@ -465,15 +558,26 @@ export class LocalInfer extends Service {
|
|
|
465
558
|
// whether the git sync (fetch, rebase, rebuild) runs at all.
|
|
466
559
|
// exactOptionalPropertyTypes: only pass the option object when a key is
|
|
467
560
|
// present.
|
|
468
|
-
const hasDeployOptions = options?.allowRunDeps !== undefined ||
|
|
561
|
+
const hasDeployOptions = options?.allowRunDeps !== undefined ||
|
|
562
|
+
options?.autoRebuild !== undefined ||
|
|
563
|
+
options?.onLog !== undefined;
|
|
469
564
|
const deployOptions = hasDeployOptions
|
|
470
565
|
? {
|
|
471
566
|
...(options?.allowRunDeps !== undefined ? { allowRunDeps: options.allowRunDeps } : {}),
|
|
472
567
|
...(options?.autoRebuild !== undefined ? { autoRebuild: options.autoRebuild } : {}),
|
|
568
|
+
...(options?.onLog !== undefined ? { onLog: options.onLog } : {}),
|
|
473
569
|
}
|
|
474
570
|
: undefined;
|
|
571
|
+
// The in-flight launch flag: the provider-status detector keeps this
|
|
572
|
+
// provider's linked read while the deploy and health poll run, so an engine
|
|
573
|
+
// that sizes its own model (no model tie) never flashes "Unable to link".
|
|
574
|
+
const launchKey = this.engineKey(plan.engineId, plan.port);
|
|
575
|
+
this.launchingEngines.add(launchKey);
|
|
475
576
|
const deployResult = await adapter.deploy(plan, platform, onProgress, deployOptions);
|
|
476
577
|
if (!deployResult.ok) {
|
|
578
|
+
// A failed deploy settles the launch: clear the in-flight flag so the
|
|
579
|
+
// provider reads from the process state (stopped), not a stuck launch.
|
|
580
|
+
this.launchingEngines.delete(launchKey);
|
|
477
581
|
// exactOptionalPropertyTypes: only write the detail key when it is present.
|
|
478
582
|
const result = { plan, healthy: false };
|
|
479
583
|
if (deployResult.detail !== undefined) {
|
|
@@ -517,10 +621,20 @@ export class LocalInfer extends Service {
|
|
|
517
621
|
// "Launching..." while it waits for the engine to report ready.
|
|
518
622
|
onProgress?.('launch');
|
|
519
623
|
const health = await adapter.healthCheck(plan, options);
|
|
624
|
+
// The launch has settled (a healthy poll, a failed one, or a download that
|
|
625
|
+
// the card re-polls): clear the in-flight flag so the provider reads from
|
|
626
|
+
// the model tie (a healthy launch) or the process state (a failed one).
|
|
627
|
+
this.launchingEngines.delete(launchKey);
|
|
520
628
|
const launchResult = { plan, healthy: health.healthy };
|
|
521
629
|
if (health.detail !== undefined) {
|
|
522
630
|
launchResult.detail = health.detail;
|
|
523
631
|
}
|
|
632
|
+
// The start phase the poll observed (downloading, starting, ready, failed)
|
|
633
|
+
// rides the result so the card reads a long model download as in progress
|
|
634
|
+
// instead of a broken launch.
|
|
635
|
+
if (health.state !== undefined) {
|
|
636
|
+
launchResult.state = health.state;
|
|
637
|
+
}
|
|
524
638
|
// The launch record the route persists for the provider config entry: the
|
|
525
639
|
// engine's pid and the DSH process pid that launched it. The parent pid is
|
|
526
640
|
// only written when the host process identifies as DSH, so a non-DSH host
|
|
@@ -611,6 +725,86 @@ export class LocalInfer extends Service {
|
|
|
611
725
|
setEngineDeathObserver(observer) {
|
|
612
726
|
this.deathObserver = observer;
|
|
613
727
|
}
|
|
728
|
+
/**
|
|
729
|
+
* registerSudoObserver reserves `key`'s slot (the runLogKey rule every run
|
|
730
|
+
* path shares) for one run's hook for commands the sudo guard found
|
|
731
|
+
* waiting for a password on the launching CLI: the observer lands core's
|
|
732
|
+
* SUDO_CLI_NOTE line while the command waits. Registering a key that is
|
|
733
|
+
* taken replaces the old hook (a re-launch under the same key owns the
|
|
734
|
+
* note), and the disposer removes only its own hook, so a stale run's
|
|
735
|
+
* settle never clears a live one's routing. The run calls the disposer in
|
|
736
|
+
* a finally.
|
|
737
|
+
*/
|
|
738
|
+
registerSudoObserver(key, observer) {
|
|
739
|
+
this.sudoObservers.set(key, observer);
|
|
740
|
+
return () => {
|
|
741
|
+
if (this.sudoObservers.get(key) === observer) {
|
|
742
|
+
this.sudoObservers.delete(key);
|
|
743
|
+
}
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
/**
|
|
747
|
+
* notifySudoNeeded announces one guarded command to every registered sudo
|
|
748
|
+
* observer (the shared CLI password wait can block any run, and the
|
|
749
|
+
* announce seam does not name the run that triggered it). A throwing
|
|
750
|
+
* observer is isolated per announce: the note is a log courtesy, and a
|
|
751
|
+
* broken route must not fail the install that is already racing the sudo
|
|
752
|
+
* deadline. Published on the service surface so a command runner outside
|
|
753
|
+
* this package (the providers' lifecycle host) reports through the same
|
|
754
|
+
* seam.
|
|
755
|
+
*/
|
|
756
|
+
notifySudoNeeded(command) {
|
|
757
|
+
for (const observer of [...this.sudoObservers.values()]) {
|
|
758
|
+
try {
|
|
759
|
+
observer(command);
|
|
760
|
+
}
|
|
761
|
+
catch {
|
|
762
|
+
// The sudo deadline still settles a passwordless wait.
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
/**
|
|
767
|
+
* beginRun starts one provider process's live run-log run under the key
|
|
768
|
+
* runLogKey names: the buffer resets to the seed separator line, the
|
|
769
|
+
* running flag goes true, the seq restarts, and subscribers get the reset
|
|
770
|
+
* notice a mid-run attached reader needs to replace its display.
|
|
771
|
+
*/
|
|
772
|
+
beginRun(key, seed) {
|
|
773
|
+
this.runLogs.begin(key, seed);
|
|
774
|
+
}
|
|
775
|
+
/**
|
|
776
|
+
* appendRunLog appends one chunk to the key's live log (tail-capped to
|
|
777
|
+
* LOG_TAIL_BYTES) and notifies subscribers. Appending with no open run
|
|
778
|
+
* opens one lazily without a seed, so a late note (an engine-death line)
|
|
779
|
+
* still lands.
|
|
780
|
+
*/
|
|
781
|
+
appendRunLog(key, text) {
|
|
782
|
+
this.runLogs.append(key, text);
|
|
783
|
+
}
|
|
784
|
+
/**
|
|
785
|
+
* readRunLog answers the key's catch-up snapshot: the seq reached, the
|
|
786
|
+
* bounded tail text, and the running flag, the data a mid-run page
|
|
787
|
+
* refresh or a late-opened Logs pane renders from.
|
|
788
|
+
*/
|
|
789
|
+
readRunLog(key) {
|
|
790
|
+
return this.runLogs.read(key);
|
|
791
|
+
}
|
|
792
|
+
/**
|
|
793
|
+
* subscribeRunLog registers a listener for the key's run-log events and
|
|
794
|
+
* returns the disposer (the route calls it when its stream closes).
|
|
795
|
+
* Listeners are independent per connection; none can block a writer.
|
|
796
|
+
*/
|
|
797
|
+
subscribeRunLog(key, listener) {
|
|
798
|
+
return this.runLogs.subscribe(key, listener);
|
|
799
|
+
}
|
|
800
|
+
/**
|
|
801
|
+
* endRun settles the key's run: the running flag clears and subscribers
|
|
802
|
+
* get the end event. The text stays readable until the next beginRun, so
|
|
803
|
+
* a late Logs pane still shows the finished run.
|
|
804
|
+
*/
|
|
805
|
+
endRun(key) {
|
|
806
|
+
this.runLogs.end(key);
|
|
807
|
+
}
|
|
614
808
|
/**
|
|
615
809
|
* watchLaunchedHandle ties a launched child's one-shot exit to the live
|
|
616
810
|
* state: the host's close/error event (a crash, a pkill, or the stop kill)
|
|
@@ -671,6 +865,15 @@ export class LocalInfer extends Service {
|
|
|
671
865
|
timer.unref();
|
|
672
866
|
this.engineWatchers.set(port, { pid: record.pid, timer });
|
|
673
867
|
}
|
|
868
|
+
/**
|
|
869
|
+
* engineKey is the in-flight state machine's key for one engine: the engine
|
|
870
|
+
* id plus its port when the plan names one, so two configs of one engine on
|
|
871
|
+
* different ports are distinct engines. A plan with no port keys on the
|
|
872
|
+
* engine id alone.
|
|
873
|
+
*/
|
|
874
|
+
engineKey(engineId, port) {
|
|
875
|
+
return port !== undefined ? `${engineId}:${port}` : engineId;
|
|
876
|
+
}
|
|
674
877
|
/**
|
|
675
878
|
* stopEngineWatcher cancels one port's adopted-process poll, if it has one.
|
|
676
879
|
*/
|
|
@@ -693,6 +896,10 @@ export class LocalInfer extends Service {
|
|
|
693
896
|
*/
|
|
694
897
|
observeEngineDeath(event) {
|
|
695
898
|
if (event.port !== undefined) {
|
|
899
|
+
// The process is down: the in-flight stop (if one was in flight) has
|
|
900
|
+
// settled, so drop the flag and the deferred port-to-model tie the stop
|
|
901
|
+
// step kept for the provider's linked read.
|
|
902
|
+
this.stoppingEngines.delete(this.engineKey(event.engineId, event.port));
|
|
696
903
|
const tracked = this.launchedModels.get(event.port);
|
|
697
904
|
if (tracked !== undefined && tracked.engineId === event.engineId) {
|
|
698
905
|
this.launchedModels.delete(event.port);
|
|
@@ -715,19 +922,39 @@ export class LocalInfer extends Service {
|
|
|
715
922
|
/**
|
|
716
923
|
* stop signals the engine a launch started: it resolves the adapter by the
|
|
717
924
|
* plan's engineId and runs the adapter's stop step (kill the tracked process,
|
|
718
|
-
* stop the named container, or compose down).
|
|
925
|
+
* stop the named container, or compose down). The optional `log` is the
|
|
926
|
+
* stop run's log sink: the adapter writes the `$ <command>` line of every
|
|
927
|
+
* command it runs (compose down, docker stop, a pid signal) through it
|
|
928
|
+
* before the child runs, so the stop's live run reads the command first.
|
|
719
929
|
*/
|
|
720
|
-
async stop(plan) {
|
|
930
|
+
async stop(plan, log) {
|
|
721
931
|
const adapter = this.engineRegistry.get(plan.engineId);
|
|
722
932
|
if (adapter === undefined) {
|
|
723
933
|
throw new LocalInferError(`no engine adapter is registered for "${plan.engineId}"`, 'LOCAL_INFER_INVALID_ENGINE');
|
|
724
934
|
}
|
|
725
|
-
|
|
726
|
-
//
|
|
727
|
-
//
|
|
728
|
-
//
|
|
935
|
+
// The in-flight stop flag: the provider-status detector keeps this
|
|
936
|
+
// provider's linked read while the engine shuts down, so a stop never
|
|
937
|
+
// flashes the "Unable to link" banner for the moment the process takes to
|
|
938
|
+
// die. The flag clears when the process dies (the death observer) or the
|
|
939
|
+
// detector sees the process down (a stop of an engine it cannot watch).
|
|
940
|
+
const stopKey = this.engineKey(plan.engineId, plan.port);
|
|
941
|
+
this.stoppingEngines.add(stopKey);
|
|
942
|
+
try {
|
|
943
|
+
await adapter.stop(plan, log);
|
|
944
|
+
}
|
|
945
|
+
catch (error) {
|
|
946
|
+
// A stop that failed (a kill the host could not deliver) settles the
|
|
947
|
+
// in-flight state: the flag clears, so the provider reads from the
|
|
948
|
+
// process state, not a stop stuck in flight.
|
|
949
|
+
this.stoppingEngines.delete(stopKey);
|
|
950
|
+
throw error;
|
|
951
|
+
}
|
|
952
|
+
// Clear the live-port ledger and the plugin-launched flag, and cancel any
|
|
953
|
+
// adopted-process watcher standing on the port. The port-to-model tie is
|
|
954
|
+
// deferred to the death observer, so the provider keeps its linked read
|
|
955
|
+
// (running, not unsure) for the moment the engine is still shutting down;
|
|
956
|
+
// the observer drops it the instant the process is down.
|
|
729
957
|
if (plan.port !== undefined) {
|
|
730
|
-
this.launchedModels.delete(plan.port);
|
|
731
958
|
this.stopEngineWatcher(plan.port);
|
|
732
959
|
const ports = this.launchedPorts.get(plan.engineId);
|
|
733
960
|
if (ports !== undefined) {
|
|
@@ -836,7 +1063,10 @@ export class LocalInfer extends Service {
|
|
|
836
1063
|
preset: undefined,
|
|
837
1064
|
stored: false,
|
|
838
1065
|
degraded: true,
|
|
839
|
-
|
|
1066
|
+
// The URL rides the caller-visible detail: fetchPreset is not a
|
|
1067
|
+
// provider process (no run-log key to write a `> GET` line into), so
|
|
1068
|
+
// the audited rule is served by naming the queried URL here instead.
|
|
1069
|
+
detail: `the modsearch fetch failed for ${source.url}: ${detail}; the built-in presets apply`,
|
|
840
1070
|
};
|
|
841
1071
|
}
|
|
842
1072
|
const preset = presetFromFetch(source, content, todayIso());
|
|
@@ -845,13 +1075,26 @@ export class LocalInfer extends Service {
|
|
|
845
1075
|
return { preset, stored: true, degraded: false };
|
|
846
1076
|
}
|
|
847
1077
|
/**
|
|
848
|
-
* defaultModelDirectory is the plugin's model download directory:
|
|
849
|
-
*
|
|
850
|
-
*
|
|
851
|
-
*
|
|
1078
|
+
* defaultModelDirectory is the plugin's model download directory: the
|
|
1079
|
+
* Hugging Face hub cache the hf CLI and huggingface_hub share (HF_HUB_CACHE
|
|
1080
|
+
* when set, then $HF_HOME/hub, then ~/.cache/huggingface/hub), so a model
|
|
1081
|
+
* the plugin pulls lands where every other HF tooling looks. The directory
|
|
1082
|
+
* is created when absent, so the field's default is always a real path the
|
|
1083
|
+
* deploy step can target.
|
|
852
1084
|
*/
|
|
853
1085
|
defaultModelDirectory() {
|
|
854
|
-
const directory =
|
|
1086
|
+
const directory = hfHubCacheDirectory();
|
|
1087
|
+
mkdirSync(directory, { recursive: true });
|
|
1088
|
+
return directory;
|
|
1089
|
+
}
|
|
1090
|
+
/**
|
|
1091
|
+
* defaultDatasetDirectory is the plugin's dataset download directory: the
|
|
1092
|
+
* Hugging Face datasets cache (HF_DATASETS_CACHE when set, then
|
|
1093
|
+
* $HF_HOME/datasets, then ~/.cache/huggingface/datasets). Created when
|
|
1094
|
+
* absent, for the same reason the model directory is.
|
|
1095
|
+
*/
|
|
1096
|
+
defaultDatasetDirectory() {
|
|
1097
|
+
const directory = hfDatasetsCacheDirectory();
|
|
855
1098
|
mkdirSync(directory, { recursive: true });
|
|
856
1099
|
return directory;
|
|
857
1100
|
}
|
|
@@ -864,7 +1107,10 @@ export class LocalInfer extends Service {
|
|
|
864
1107
|
const injected = options?.cliPath;
|
|
865
1108
|
if (injected !== undefined && injected.trim() !== '')
|
|
866
1109
|
return injected;
|
|
867
|
-
|
|
1110
|
+
// The presence probe is `which` on Linux/macOS and `where` on Windows, so
|
|
1111
|
+
// an installed hf CLI is found on every host.
|
|
1112
|
+
const probe = platform() === 'win32' ? 'where' : 'which';
|
|
1113
|
+
const which = await runCommand(probe, ['hf']);
|
|
868
1114
|
if (which.ok && which.stdout.trim() !== '')
|
|
869
1115
|
return which.stdout.trim();
|
|
870
1116
|
return null;
|
|
@@ -873,7 +1119,10 @@ export class LocalInfer extends Service {
|
|
|
873
1119
|
* downloadModel runs the hf CLI download for one default model into the
|
|
874
1120
|
* model directory: it creates the target directory when absent (the
|
|
875
1121
|
* recursive flag covers the parents too), renders the command through the
|
|
876
|
-
* core builder (the single source the card shows),
|
|
1122
|
+
* core builder (the single source the card shows), logs the rendered
|
|
1123
|
+
* command's `$ <command>` line through options.onLog before the runner
|
|
1124
|
+
* spawns the child (so every caller's run reads command first, output
|
|
1125
|
+
* after, and the line is written once, here), and runs it through the
|
|
877
1126
|
* injected runner when one is given, so a test captures the command without
|
|
878
1127
|
* a live hf. A failure keeps the runner's detail, so the card names the
|
|
879
1128
|
* broken step.
|
|
@@ -881,7 +1130,9 @@ export class LocalInfer extends Service {
|
|
|
881
1130
|
async downloadModel(entry, directory, options) {
|
|
882
1131
|
mkdirSync(directory, { recursive: true });
|
|
883
1132
|
const runner = options?.runner ?? downloadRunner;
|
|
884
|
-
const
|
|
1133
|
+
const command = hfDownloadCommand(entry, directory, options?.useHubCache === true);
|
|
1134
|
+
options?.onLog?.(commandLogLine(command));
|
|
1135
|
+
const result = await runner(command, options?.signal, options?.onChunk);
|
|
885
1136
|
if (result.ok)
|
|
886
1137
|
return { ok: true };
|
|
887
1138
|
return { ok: false, detail: `model download failed: ${result.detail ?? 'unknown'}` };
|
|
@@ -890,13 +1141,16 @@ export class LocalInfer extends Service {
|
|
|
890
1141
|
* downloadChatTemplate pulls the chat template file for one HF repo
|
|
891
1142
|
* reference into the model directory, so a selected template (the Froggeric
|
|
892
1143
|
* set, a custom repo) works out of the box. It shares the model download's
|
|
893
|
-
* recursive directory creation, runner,
|
|
894
|
-
* small file, but the hf CLI's cold start and auth
|
|
1144
|
+
* recursive directory creation, runner, log line before the spawn, and long
|
|
1145
|
+
* timeout: a template is a small file, but the hf CLI's cold start and auth
|
|
1146
|
+
* are the same cost.
|
|
895
1147
|
*/
|
|
896
1148
|
async downloadChatTemplate(repo, directory, options) {
|
|
897
1149
|
mkdirSync(directory, { recursive: true });
|
|
898
1150
|
const runner = options?.runner ?? downloadRunner;
|
|
899
|
-
const
|
|
1151
|
+
const command = hfChatTemplateCommand(repo, directory, options?.useHubCache === true);
|
|
1152
|
+
options?.onLog?.(commandLogLine(command));
|
|
1153
|
+
const result = await runner(command, options?.signal, options?.onChunk);
|
|
900
1154
|
if (result.ok)
|
|
901
1155
|
return { ok: true };
|
|
902
1156
|
return { ok: false, detail: `chat template download failed: ${result.detail ?? 'unknown'}` };
|
|
@@ -946,6 +1200,71 @@ export class LocalInfer extends Service {
|
|
|
946
1200
|
}
|
|
947
1201
|
return entries;
|
|
948
1202
|
}
|
|
1203
|
+
/**
|
|
1204
|
+
* listModelCache lists the hub cache's `models--*` directories, the data the
|
|
1205
|
+
* per-engine model dropdown's cache scan draws from when the model directory
|
|
1206
|
+
* is the plugin default (the hub cache). Each entry carries the repo id the
|
|
1207
|
+
* directory names (the `{namespace}/{name}` decoded from the
|
|
1208
|
+
* `models--{ns}--{name}` form) and the artifact file names its snapshots
|
|
1209
|
+
* hold (the union across every commit). A missing or unreadable cache reads
|
|
1210
|
+
* as an empty list, which the route renders as "no cached models".
|
|
1211
|
+
*/
|
|
1212
|
+
listModelCache() {
|
|
1213
|
+
const hub = hfHubCacheDirectory();
|
|
1214
|
+
let dirents;
|
|
1215
|
+
try {
|
|
1216
|
+
dirents = readdirSync(hub, { withFileTypes: true });
|
|
1217
|
+
}
|
|
1218
|
+
catch {
|
|
1219
|
+
return [];
|
|
1220
|
+
}
|
|
1221
|
+
const entries = [];
|
|
1222
|
+
for (const dirent of dirents) {
|
|
1223
|
+
if (!dirent.isDirectory() || !dirent.name.startsWith('models--'))
|
|
1224
|
+
continue;
|
|
1225
|
+
// Decode models--{ns}--{name} back to {ns}/{name}: the name is the
|
|
1226
|
+
// segment after the last --, the namespace everything before it.
|
|
1227
|
+
const body = dirent.name.slice('models--'.length);
|
|
1228
|
+
const separator = body.lastIndexOf('--');
|
|
1229
|
+
if (separator <= 0)
|
|
1230
|
+
continue;
|
|
1231
|
+
const repoId = `${body.slice(0, separator)}/${body.slice(separator + 2)}`;
|
|
1232
|
+
entries.push({ repoId, files: this.snapshotFiles(hub, dirent.name) });
|
|
1233
|
+
}
|
|
1234
|
+
return entries;
|
|
1235
|
+
}
|
|
1236
|
+
/**
|
|
1237
|
+
* snapshotFiles unions the file names across every commit directory under a
|
|
1238
|
+
* `models--*` dir's `snapshots/`, the artifact set the per-engine cache scan
|
|
1239
|
+
* filters. A missing snapshots folder reads as an empty set.
|
|
1240
|
+
*/
|
|
1241
|
+
snapshotFiles(hub, modelDir) {
|
|
1242
|
+
const snapshots = join(hub, modelDir, 'snapshots');
|
|
1243
|
+
let commits;
|
|
1244
|
+
try {
|
|
1245
|
+
commits = readdirSync(snapshots, { withFileTypes: true });
|
|
1246
|
+
}
|
|
1247
|
+
catch {
|
|
1248
|
+
return [];
|
|
1249
|
+
}
|
|
1250
|
+
const files = new Set();
|
|
1251
|
+
for (const commit of commits) {
|
|
1252
|
+
if (!commit.isDirectory())
|
|
1253
|
+
continue;
|
|
1254
|
+
let commitFiles;
|
|
1255
|
+
try {
|
|
1256
|
+
commitFiles = readdirSync(join(snapshots, commit.name), { withFileTypes: true });
|
|
1257
|
+
}
|
|
1258
|
+
catch {
|
|
1259
|
+
continue;
|
|
1260
|
+
}
|
|
1261
|
+
for (const file of commitFiles) {
|
|
1262
|
+
if (file.isFile())
|
|
1263
|
+
files.add(file.name);
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
return [...files];
|
|
1267
|
+
}
|
|
949
1268
|
/**
|
|
950
1269
|
* specsForEngine reads the parameter surface of the registered adapter for
|
|
951
1270
|
* an engine id, so the defaults resolver floors the result on that engine's
|
|
@@ -970,7 +1289,9 @@ export class LocalInfer extends Service {
|
|
|
970
1289
|
const env = process.env.MODSEARCH_CLI;
|
|
971
1290
|
if (env !== undefined && env.trim() !== '')
|
|
972
1291
|
return env;
|
|
973
|
-
|
|
1292
|
+
// The presence probe is `which` on Linux/macOS and `where` on Windows.
|
|
1293
|
+
const probe = platform() === 'win32' ? 'where' : 'which';
|
|
1294
|
+
const which = await runCommand(probe, ['modsearch']);
|
|
974
1295
|
if (which.ok && which.stdout.trim() !== '')
|
|
975
1296
|
return which.stdout.trim();
|
|
976
1297
|
return null;
|
|
@@ -983,7 +1304,7 @@ export class LocalInfer extends Service {
|
|
|
983
1304
|
*/
|
|
984
1305
|
fetchPage(url, cliPath) {
|
|
985
1306
|
return new Promise((resolve, reject) => {
|
|
986
|
-
const child = spawn('node', [cliPath, 'search', '-u', url, '--deadline',
|
|
1307
|
+
const child = spawn('node', [cliPath, 'search', '-u', url, '--deadline', MODSEARCH_DEADLINE_ARG], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
987
1308
|
let stdout = '';
|
|
988
1309
|
let stderr = '';
|
|
989
1310
|
child.stdout.on('data', (chunk) => {
|
|
@@ -1239,36 +1560,132 @@ function runCommand(cmd, args) {
|
|
|
1239
1560
|
*/
|
|
1240
1561
|
const INSTALL_OUTPUT_TAIL = 1024;
|
|
1241
1562
|
/**
|
|
1242
|
-
*
|
|
1243
|
-
* the
|
|
1244
|
-
*
|
|
1245
|
-
*
|
|
1563
|
+
* The high-water mark the streaming output trims to: the collected output
|
|
1564
|
+
* passes the mark only for a chatty command, and the trim keeps the last
|
|
1565
|
+
* kilobyte the failure detail shows, the same rule the providers' streaming
|
|
1566
|
+
* host applies.
|
|
1246
1567
|
*/
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1568
|
+
const INSTALL_OUTPUT_HIGH_WATER = INSTALL_OUTPUT_TAIL * 2;
|
|
1569
|
+
/**
|
|
1570
|
+
* tailOutput keeps a bounded tail of a streamed command's combined output:
|
|
1571
|
+
* append grows the buffer and trims it to the high-water mark, and tail
|
|
1572
|
+
* returns the trimmed last kilobyte a failure detail shows. The install and
|
|
1573
|
+
* download runners share it, so a chatty failure stays readable without
|
|
1574
|
+
* growing the host's memory.
|
|
1575
|
+
*/
|
|
1576
|
+
function tailOutput() {
|
|
1249
1577
|
let output = '';
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1254
|
-
|
|
1578
|
+
return {
|
|
1579
|
+
append: (text) => {
|
|
1580
|
+
output += text;
|
|
1581
|
+
if (output.length > INSTALL_OUTPUT_HIGH_WATER) {
|
|
1582
|
+
output = output.slice(-INSTALL_OUTPUT_HIGH_WATER);
|
|
1583
|
+
}
|
|
1584
|
+
},
|
|
1585
|
+
tail: () => output.trim().slice(-INSTALL_OUTPUT_TAIL),
|
|
1255
1586
|
};
|
|
1256
|
-
|
|
1257
|
-
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1587
|
+
}
|
|
1588
|
+
/**
|
|
1589
|
+
* sudoProbe answers whether sudo already holds cached (or NOPASSWD)
|
|
1590
|
+
* credentials: it runs SUDO_PROBE_COMMAND non-interactively through the
|
|
1591
|
+
* probe timeout and reads a clean exit as "no prompt is coming". A refused
|
|
1592
|
+
* -n, a missing sudo, and a hung probe all read as "a prompt is coming",
|
|
1593
|
+
* the safe side: the run announces and caps instead of dead-locking on a
|
|
1594
|
+
* TTY the card user cannot see.
|
|
1595
|
+
*/
|
|
1596
|
+
export async function sudoProbe() {
|
|
1597
|
+
const [command, ...args] = SUDO_PROBE_COMMAND;
|
|
1598
|
+
if (command === undefined)
|
|
1599
|
+
return false;
|
|
1600
|
+
const { ok } = await runCommand(command, [...args]);
|
|
1601
|
+
return ok;
|
|
1602
|
+
}
|
|
1603
|
+
/**
|
|
1604
|
+
* sudoGuardFor is the service-side binding of core's guardSudoCommand: it
|
|
1605
|
+
* supplies sudoProbe, so this package's install runner and the providers'
|
|
1606
|
+
* lifecycle host drive one probe and one rule from one function. Calling
|
|
1607
|
+
* it is itself the sudo declaration (needsSudo is true); callers gate on
|
|
1608
|
+
* their own needsSudo fact first and never run a plain command through it.
|
|
1609
|
+
*/
|
|
1610
|
+
export function sudoGuardFor(command, notify, kill, timeoutMs) {
|
|
1611
|
+
return guardSudoCommand({
|
|
1612
|
+
needsSudo: true,
|
|
1613
|
+
command,
|
|
1614
|
+
probe: sudoProbe,
|
|
1615
|
+
notify,
|
|
1616
|
+
kill,
|
|
1617
|
+
...(timeoutMs === undefined ? {} : { timeoutMs }),
|
|
1263
1618
|
});
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1619
|
+
}
|
|
1620
|
+
/**
|
|
1621
|
+
* guardedShellRunner builds the Play/Run runner: it runs one command line
|
|
1622
|
+
* through the system shell and reports the exit. stdin stays closed. When
|
|
1623
|
+
* core hands the command the dep's sudo declaration and sudoProbe finds no
|
|
1624
|
+
* cached credentials, core's guard announces the CLI prompt through notify
|
|
1625
|
+
* and arms the password deadline; its fire signals the child through the
|
|
1626
|
+
* shared signalChild rule (SIGINT, then SIGTERM; the group signal reaches a
|
|
1627
|
+
* sudo survivor of a compound line) and the run settles with
|
|
1628
|
+
* sudoFailureDetail instead of the shell's bare signal exit. The guard adds
|
|
1629
|
+
* only probe, notify, and deadline to the existing spawn; the copy/paste
|
|
1630
|
+
* path stays the fallback for a passwordless host.
|
|
1631
|
+
*/
|
|
1632
|
+
export function guardedShellRunner(notify) {
|
|
1633
|
+
return async (command, options) => {
|
|
1634
|
+
let child;
|
|
1635
|
+
const guard = options?.needsSudo === true
|
|
1636
|
+
? await sudoGuardFor(command, notify, () => {
|
|
1637
|
+
if (child !== undefined)
|
|
1638
|
+
signalChild(child);
|
|
1639
|
+
})
|
|
1640
|
+
: undefined;
|
|
1641
|
+
try {
|
|
1642
|
+
const outcome = await new Promise((resolve) => {
|
|
1643
|
+
// The local keeps the spawn overload's non-null stream typing; the
|
|
1644
|
+
// outer child exists for the guard's kill. The shell spawns detached
|
|
1645
|
+
// so it leads its own process group and signalChild's negative-pid
|
|
1646
|
+
// signal reaches the whole line. libuv's detached spawn only changes
|
|
1647
|
+
// the process group (Node >= 22 ships libuv >= 1.48, which does not
|
|
1648
|
+
// create a session), so a sudo prompt still reaches the launching
|
|
1649
|
+
// terminal and the guard's whole point survives the detach.
|
|
1650
|
+
const started = spawn(command, {
|
|
1651
|
+
shell: true,
|
|
1652
|
+
detached: true,
|
|
1653
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1654
|
+
});
|
|
1655
|
+
child = started;
|
|
1656
|
+
detachedChildren.add(started);
|
|
1657
|
+
const tail = tailOutput();
|
|
1658
|
+
const append = (chunk) => {
|
|
1659
|
+
tail.append(chunk.toString('utf8'));
|
|
1660
|
+
};
|
|
1661
|
+
started.stdout.on('data', append);
|
|
1662
|
+
started.stderr.on('data', append);
|
|
1663
|
+
// Without a listener, a child that fails to start (a missing shell) emits
|
|
1664
|
+
// an unlistened 'error' and throws it as an uncaughtException through the
|
|
1665
|
+
// host. Read it as a failed command instead.
|
|
1666
|
+
started.on('error', (error) => {
|
|
1667
|
+
resolve({ ok: false, detail: error.message });
|
|
1668
|
+
});
|
|
1669
|
+
started.on('close', code => {
|
|
1670
|
+
if (code === 0) {
|
|
1671
|
+
resolve({ ok: true });
|
|
1672
|
+
return;
|
|
1673
|
+
}
|
|
1674
|
+
resolve({ ok: false, detail: `exit ${code}: ${tail.tail()}` });
|
|
1675
|
+
});
|
|
1676
|
+
});
|
|
1677
|
+
if (guard !== undefined && guard.timedOut() && !outcome.ok) {
|
|
1678
|
+
// The password deadline killed the child: name the missed sudo
|
|
1679
|
+
// prompt instead of the shell's signal exit.
|
|
1680
|
+
return { ok: false, detail: sudoFailureDetail(command) };
|
|
1681
|
+
}
|
|
1682
|
+
return outcome;
|
|
1268
1683
|
}
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
}
|
|
1684
|
+
finally {
|
|
1685
|
+
guard?.disarm();
|
|
1686
|
+
}
|
|
1687
|
+
};
|
|
1688
|
+
}
|
|
1272
1689
|
/**
|
|
1273
1690
|
* The download timeout. A model download moves tens of GiB, so the cap is
|
|
1274
1691
|
* hours, not the probe's seconds; a hung download must not kill the panel,
|
|
@@ -1276,20 +1693,126 @@ const shellRunCommand = (command) => new Promise(resolve => {
|
|
|
1276
1693
|
*/
|
|
1277
1694
|
const DOWNLOAD_TIMEOUT_MS = 7_200_000;
|
|
1278
1695
|
/**
|
|
1279
|
-
*
|
|
1280
|
-
*
|
|
1281
|
-
*
|
|
1282
|
-
* the broken line.
|
|
1696
|
+
* The cancel grace period in ms: an aborted download gets SIGINT first, and a
|
|
1697
|
+
* child still alive after the grace period gets SIGTERM, so a stuck hf CLI
|
|
1698
|
+
* cannot keep the run open.
|
|
1283
1699
|
*/
|
|
1284
|
-
const
|
|
1285
|
-
|
|
1286
|
-
|
|
1700
|
+
const DOWNLOAD_ABORT_GRACE_MS = 5_000;
|
|
1701
|
+
/**
|
|
1702
|
+
* The install shells the guarded runner spawned detached (process-group
|
|
1703
|
+
* leaders). signalProcess reads the set so the negative-pid group signal
|
|
1704
|
+
* only ever targets a child that actually leads its group; a child that
|
|
1705
|
+
* stays in the host's group (the download runner's shell) gets the direct
|
|
1706
|
+
* signal, so a stray negative pid can never fan out beyond the child.
|
|
1707
|
+
*/
|
|
1708
|
+
const detachedChildren = new WeakSet();
|
|
1709
|
+
/**
|
|
1710
|
+
* signalProcess delivers one signal to one child: a process-group leader
|
|
1711
|
+
* gets it through its negative pid, so a compound command line
|
|
1712
|
+
* (`a && sudo b`) dies whole instead of leaving sudo as a group survivor;
|
|
1713
|
+
* every other child gets the direct signal. A group that is already gone
|
|
1714
|
+
* (ESRCH) or a platform that rejects negative pids falls back to the direct
|
|
1715
|
+
* signal, which reaches the shell itself.
|
|
1716
|
+
*/
|
|
1717
|
+
function signalProcess(child, signal) {
|
|
1718
|
+
if (child.pid !== undefined && detachedChildren.has(child)) {
|
|
1719
|
+
try {
|
|
1720
|
+
process.kill(-child.pid, signal);
|
|
1721
|
+
return;
|
|
1722
|
+
}
|
|
1723
|
+
catch {
|
|
1724
|
+
// Group gone or unsupported: the direct signal below still reaches the
|
|
1725
|
+
// shell, and a child that already exited no-ops the way ChildProcess
|
|
1726
|
+
// .kill always does.
|
|
1727
|
+
}
|
|
1728
|
+
}
|
|
1729
|
+
child.kill(signal);
|
|
1730
|
+
}
|
|
1731
|
+
/**
|
|
1732
|
+
* signalChild applies the cancel rule: SIGINT first, SIGTERM after the grace
|
|
1733
|
+
* period if the child is still alive. The child already marked killed or
|
|
1734
|
+
* exited is left alone. The optional graceMs overrides the 5 s constant so a
|
|
1735
|
+
* test can exercise the escalation without waiting. The abort watch and the
|
|
1736
|
+
* sudo deadline kill share this one rule, the mirror of the providers'
|
|
1737
|
+
* lifecycle host copy (packages/providers/src/adapters.ts, signalChild):
|
|
1738
|
+
* each package owns its child-process wiring, so a change to the escalation
|
|
1739
|
+
* rule lands in two places by design.
|
|
1740
|
+
*/
|
|
1741
|
+
function signalChild(child, graceMs) {
|
|
1742
|
+
if (child.killed || child.exitCode !== null)
|
|
1743
|
+
return;
|
|
1744
|
+
signalProcess(child, 'SIGINT');
|
|
1745
|
+
const escalate = setTimeout(() => {
|
|
1746
|
+
// A group signal never marks ChildProcess.kill, so the guard reads the
|
|
1747
|
+
// exit state: a child that has neither exited nor been signalled is
|
|
1748
|
+
// still alive and gets SIGTERM.
|
|
1749
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
1750
|
+
signalProcess(child, 'SIGTERM');
|
|
1751
|
+
}, graceMs ?? DOWNLOAD_ABORT_GRACE_MS);
|
|
1752
|
+
escalate.unref();
|
|
1753
|
+
}
|
|
1754
|
+
/**
|
|
1755
|
+
* watchChildAbort kills the in-flight child when the signal aborts (the card's
|
|
1756
|
+
* cancel): SIGINT first, SIGTERM after the grace period if the child is still
|
|
1757
|
+
* alive, through the shared signalChild rule. A signal that is already
|
|
1758
|
+
* aborted kills immediately. A near-identical copy lives in the providers
|
|
1759
|
+
* package (packages/providers/src/adapters.ts, the lifecycle host's run and
|
|
1760
|
+
* runStream abort path): each package owns its own child-process wiring, and
|
|
1761
|
+
* the guard fix (the SIGTERM escalation) was applied to both copies, so a
|
|
1762
|
+
* change to the escalation rule lands in two places by design.
|
|
1763
|
+
*/
|
|
1764
|
+
function watchChildAbort(child, signal, graceMs) {
|
|
1765
|
+
if (signal === undefined)
|
|
1766
|
+
return;
|
|
1767
|
+
const onAbort = () => {
|
|
1768
|
+
signalChild(child, graceMs);
|
|
1769
|
+
};
|
|
1770
|
+
if (signal.aborted) {
|
|
1771
|
+
onAbort();
|
|
1772
|
+
return;
|
|
1773
|
+
}
|
|
1774
|
+
signal.addEventListener('abort', onAbort, { once: true });
|
|
1775
|
+
}
|
|
1776
|
+
/**
|
|
1777
|
+
* downloadRunner is the model-download runner: it spawns the rendered hf CLI
|
|
1778
|
+
* command through the system shell and reports the exit. The spawned child's
|
|
1779
|
+
* stdout and stderr chunks stream to onChunk as they land (the hf CLI's tqdm
|
|
1780
|
+
* progress), while the output tail keeps the last kilobyte, the same rule the
|
|
1781
|
+
* install runner uses, so a failure names the broken line. An abort on the
|
|
1782
|
+
* optional signal interrupts the in-flight child (SIGINT, then SIGTERM), so a
|
|
1783
|
+
* canceled download stops instead of running to the timeout. The optional
|
|
1784
|
+
* graceMs overrides the 5 s SIGTERM escalation window for the abort path.
|
|
1785
|
+
*/
|
|
1786
|
+
export const downloadRunner = (command, signal, onChunk, graceMs) => new Promise(resolve => {
|
|
1787
|
+
const child = spawn(command, {
|
|
1788
|
+
shell: true,
|
|
1789
|
+
timeout: DOWNLOAD_TIMEOUT_MS,
|
|
1790
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
1791
|
+
});
|
|
1792
|
+
const tail = tailOutput();
|
|
1793
|
+
const append = (chunk) => {
|
|
1794
|
+
const text = chunk.toString('utf8');
|
|
1795
|
+
tail.append(text);
|
|
1796
|
+
onChunk?.(text);
|
|
1797
|
+
};
|
|
1798
|
+
child.stdout.on('data', append);
|
|
1799
|
+
child.stderr.on('data', append);
|
|
1800
|
+
// Without a listener, a child that fails to start (a missing shell) emits
|
|
1801
|
+
// an unlistened 'error' and throws it as an uncaughtException through the
|
|
1802
|
+
// host. Read it as a failed command instead.
|
|
1803
|
+
child.on('error', (error) => {
|
|
1804
|
+
resolve({ ok: false, detail: error.message });
|
|
1805
|
+
});
|
|
1806
|
+
child.on('close', (code, killSignal) => {
|
|
1807
|
+
if (code === 0) {
|
|
1287
1808
|
resolve({ ok: true });
|
|
1288
1809
|
return;
|
|
1289
1810
|
}
|
|
1290
|
-
const detail =
|
|
1291
|
-
|
|
1811
|
+
const detail = tail.tail();
|
|
1812
|
+
const label = code !== null ? `exit ${code}` : `killed by ${killSignal ?? 'a signal'}`;
|
|
1813
|
+
resolve({ ok: false, detail: detail === '' ? label : `${label}: ${detail}` });
|
|
1292
1814
|
});
|
|
1815
|
+
watchChildAbort(child, signal, graceMs);
|
|
1293
1816
|
});
|
|
1294
1817
|
/**
|
|
1295
1818
|
* hasSafetensorsIn reports whether one directory holds a `.safetensors`
|
|
@@ -1331,15 +1854,21 @@ async function probeGpuUsage() {
|
|
|
1331
1854
|
return result.ok ? parseNvidiaSmiGpuUsage(result.stdout) : null;
|
|
1332
1855
|
}
|
|
1333
1856
|
/**
|
|
1334
|
-
* probeProcesses reads the running process table
|
|
1335
|
-
*
|
|
1336
|
-
*
|
|
1337
|
-
*
|
|
1338
|
-
*
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1857
|
+
* probeProcesses reads the running process table into process lines the
|
|
1858
|
+
* launch-signature matcher consumes. The path switches on the host OS:
|
|
1859
|
+
* Windows reads the table through PowerShell's Win32_Process CIM class (the
|
|
1860
|
+
* pid and the full command line), and Linux and macOS read it through
|
|
1861
|
+
* `ps -eo pid,args`. The env element of a line is read from the Linux /proc
|
|
1862
|
+
* environ, and only for a pid whose command line carries one of the engine
|
|
1863
|
+
* markers: a full table of env reads is slow, and the matcher's env-key
|
|
1864
|
+
* element only fires on a marker-matched process anyway. A host without /proc
|
|
1865
|
+
* (macOS, Windows) degrades to env-less lines, which the matcher reads as a
|
|
1866
|
+
* neutral env element.
|
|
1341
1867
|
*/
|
|
1342
1868
|
async function probeProcesses(markers) {
|
|
1869
|
+
if (platform() === 'win32') {
|
|
1870
|
+
return probeWindowsProcesses();
|
|
1871
|
+
}
|
|
1343
1872
|
const result = await runCommand('ps', ['-eo', 'pid,args']);
|
|
1344
1873
|
if (!result.ok)
|
|
1345
1874
|
return [];
|
|
@@ -1362,6 +1891,51 @@ async function probeProcesses(markers) {
|
|
|
1362
1891
|
}
|
|
1363
1892
|
return lines;
|
|
1364
1893
|
}
|
|
1894
|
+
/**
|
|
1895
|
+
* parseWindowsProcessTable reads the `pid<TAB>commandLine` rows PowerShell's
|
|
1896
|
+
* Win32_Process probe emits into process lines the launch-signature matcher
|
|
1897
|
+
* consumes. Each row is split on its first tab: the pid is the left field, the
|
|
1898
|
+
* command line the right. A row with no tab (a system process with an empty
|
|
1899
|
+
* command line, trimmed to its pid alone) is skipped, and a non-numeric pid is
|
|
1900
|
+
* dropped. The env element stays undefined: Windows has no /proc environ to
|
|
1901
|
+
* read, and the matcher reads a missing env as neutral.
|
|
1902
|
+
*/
|
|
1903
|
+
export function parseWindowsProcessTable(stdout) {
|
|
1904
|
+
const lines = [];
|
|
1905
|
+
for (const raw of stdout.split('\n')) {
|
|
1906
|
+
const trimmed = raw.trim();
|
|
1907
|
+
if (trimmed === '')
|
|
1908
|
+
continue;
|
|
1909
|
+
const tab = trimmed.indexOf('\t');
|
|
1910
|
+
if (tab <= 0)
|
|
1911
|
+
continue;
|
|
1912
|
+
const pid = Number(trimmed.slice(0, tab));
|
|
1913
|
+
const command = trimmed.slice(tab + 1).trim();
|
|
1914
|
+
if (!Number.isFinite(pid) || command === '')
|
|
1915
|
+
continue;
|
|
1916
|
+
lines.push({ pid, command });
|
|
1917
|
+
}
|
|
1918
|
+
return lines;
|
|
1919
|
+
}
|
|
1920
|
+
/**
|
|
1921
|
+
* probeWindowsProcesses reads the running process table through PowerShell's
|
|
1922
|
+
* Win32_Process CIM class (the pid and the full command line), the Windows
|
|
1923
|
+
* binding for probeProcesses. The probe emits one `pid<TAB>commandLine` row
|
|
1924
|
+
* per process, which parseWindowsProcessTable turns into lines. A host without
|
|
1925
|
+
* the CIM service (a hung WMI) or a missing powershell degrades to an empty
|
|
1926
|
+
* table, which the detector reads as "no foreign engine" rather than a crash.
|
|
1927
|
+
*/
|
|
1928
|
+
async function probeWindowsProcesses() {
|
|
1929
|
+
const result = await runCommand('powershell', [
|
|
1930
|
+
'-NoProfile',
|
|
1931
|
+
'-NonInteractive',
|
|
1932
|
+
'-Command',
|
|
1933
|
+
'Get-CimInstance Win32_Process | ForEach-Object { "{0}`t{1}" -f $_.ProcessId, $_.CommandLine }',
|
|
1934
|
+
]);
|
|
1935
|
+
if (!result.ok)
|
|
1936
|
+
return [];
|
|
1937
|
+
return parseWindowsProcessTable(result.stdout);
|
|
1938
|
+
}
|
|
1365
1939
|
/**
|
|
1366
1940
|
* readProcEnviron reads one process's environment out of its /proc environ
|
|
1367
1941
|
* file (NUL-separated KEY=VALUE entries). Any failure (no /proc, the process
|
|
@@ -1445,12 +2019,14 @@ async function probeDocker() {
|
|
|
1445
2019
|
return result.ok;
|
|
1446
2020
|
}
|
|
1447
2021
|
/**
|
|
1448
|
-
* probePackageManager checks
|
|
1449
|
-
*
|
|
1450
|
-
* presence
|
|
2022
|
+
* probePackageManager checks whether a package-manager binary exists on the
|
|
2023
|
+
* host. The probe is `which` on Linux and macOS and `where` on Windows (the
|
|
2024
|
+
* OS's own presence probe), so each answer is a plain presence flag the pure
|
|
2025
|
+
* detectPackageManager mapping turns into the winner.
|
|
1451
2026
|
*/
|
|
1452
|
-
async function probePackageManager(binary) {
|
|
1453
|
-
const
|
|
2027
|
+
async function probePackageManager(binary, os) {
|
|
2028
|
+
const probe = os === 'windows' ? 'where' : 'which';
|
|
2029
|
+
const result = await runCommand(probe, [binary]);
|
|
1454
2030
|
return result.ok;
|
|
1455
2031
|
}
|
|
1456
2032
|
/**
|
|
@@ -1467,13 +2043,14 @@ export async function runHardwareProbes() {
|
|
|
1467
2043
|
darwin: 'macos',
|
|
1468
2044
|
};
|
|
1469
2045
|
const os = osMap[platform()] ?? 'linux';
|
|
1470
|
-
const [gpu, dockerAvailable, apt, dnf, pacman, brew] = await Promise.all([
|
|
2046
|
+
const [gpu, dockerAvailable, apt, dnf, pacman, brew, winget] = await Promise.all([
|
|
1471
2047
|
probeGpu(),
|
|
1472
2048
|
probeDocker(),
|
|
1473
|
-
probePackageManager('apt-get'),
|
|
1474
|
-
probePackageManager('dnf'),
|
|
1475
|
-
probePackageManager('pacman'),
|
|
1476
|
-
probePackageManager('brew'),
|
|
2049
|
+
probePackageManager('apt-get', os),
|
|
2050
|
+
probePackageManager('dnf', os),
|
|
2051
|
+
probePackageManager('pacman', os),
|
|
2052
|
+
probePackageManager('brew', os),
|
|
2053
|
+
probePackageManager('winget', os),
|
|
1477
2054
|
]);
|
|
1478
2055
|
return {
|
|
1479
2056
|
os,
|
|
@@ -1482,8 +2059,8 @@ export async function runHardwareProbes() {
|
|
|
1482
2059
|
wsl2: os === 'linux' && release().toLowerCase().includes('wsl'),
|
|
1483
2060
|
hostRamGiB: totalmem() / 2 ** 30,
|
|
1484
2061
|
gpu,
|
|
1485
|
-
packageManager: detectPackageManager({ os, apt, dnf, pacman, brew }) ?? null,
|
|
2062
|
+
packageManager: detectPackageManager({ os, apt, dnf, pacman, brew, winget }) ?? null,
|
|
1486
2063
|
};
|
|
1487
2064
|
}
|
|
1488
2065
|
export default LocalInfer;
|
|
1489
|
-
//# sourceMappingURL=index.js.map
|
|
2066
|
+
//# sourceMappingURL=index.js.map
|