labgate 0.5.39 → 0.5.42
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 +132 -248
- package/dist/cli.js +9 -33
- package/dist/cli.js.map +1 -1
- package/dist/lib/config.d.ts +19 -3
- package/dist/lib/config.js +154 -75
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/container.d.ts +11 -9
- package/dist/lib/container.js +749 -282
- package/dist/lib/container.js.map +1 -1
- package/dist/lib/dataset-mcp.js +2 -9
- package/dist/lib/dataset-mcp.js.map +1 -1
- package/dist/lib/display-mcp.d.ts +2 -2
- package/dist/lib/display-mcp.js +17 -38
- package/dist/lib/display-mcp.js.map +1 -1
- package/dist/lib/doctor.js +8 -0
- package/dist/lib/doctor.js.map +1 -1
- package/dist/lib/explorer-claude.js +36 -1
- package/dist/lib/explorer-claude.js.map +1 -1
- package/dist/lib/explorer-eval.js +3 -2
- package/dist/lib/explorer-eval.js.map +1 -1
- package/dist/lib/image-pull-lock.d.ts +18 -0
- package/dist/lib/image-pull-lock.js +167 -0
- package/dist/lib/image-pull-lock.js.map +1 -0
- package/dist/lib/init.js +22 -19
- package/dist/lib/init.js.map +1 -1
- package/dist/lib/slurm-cli-passthrough.d.ts +12 -2
- package/dist/lib/slurm-cli-passthrough.js +401 -143
- package/dist/lib/slurm-cli-passthrough.js.map +1 -1
- package/dist/lib/startup-stage-lock.d.ts +21 -0
- package/dist/lib/startup-stage-lock.js +196 -0
- package/dist/lib/startup-stage-lock.js.map +1 -0
- package/dist/lib/ui.d.ts +40 -0
- package/dist/lib/ui.html +4953 -3366
- package/dist/lib/ui.js +1771 -297
- package/dist/lib/ui.js.map +1 -1
- package/dist/lib/web-terminal-startup-readiness.d.ts +8 -0
- package/dist/lib/web-terminal-startup-readiness.js +29 -0
- package/dist/lib/web-terminal-startup-readiness.js.map +1 -0
- package/dist/lib/web-terminal.d.ts +51 -0
- package/dist/lib/web-terminal.js +171 -1
- package/dist/lib/web-terminal.js.map +1 -1
- package/dist/mcp-bundles/dataset-mcp.bundle.mjs +144 -93
- package/dist/mcp-bundles/display-mcp.bundle.mjs +35 -43
- package/dist/mcp-bundles/explorer-mcp.bundle.mjs +263 -146
- package/dist/mcp-bundles/results-mcp.bundle.mjs +39 -41
- package/dist/mcp-bundles/slurm-mcp.bundle.mjs +19 -21
- package/package.json +1 -1
|
@@ -2,9 +2,12 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.stageSlurmCliPassthrough = stageSlurmCliPassthrough;
|
|
4
4
|
const child_process_1 = require("child_process");
|
|
5
|
+
const crypto_1 = require("crypto");
|
|
5
6
|
const fs_1 = require("fs");
|
|
6
7
|
const path_1 = require("path");
|
|
8
|
+
const os_1 = require("os");
|
|
7
9
|
const config_js_1 = require("./config.js");
|
|
10
|
+
const startup_stage_lock_js_1 = require("./startup-stage-lock.js");
|
|
8
11
|
const DEFAULT_SLURM_COMMANDS = [
|
|
9
12
|
'srun',
|
|
10
13
|
'sbatch',
|
|
@@ -14,6 +17,12 @@ const DEFAULT_SLURM_COMMANDS = [
|
|
|
14
17
|
'sinfo',
|
|
15
18
|
'scontrol',
|
|
16
19
|
];
|
|
20
|
+
const SLURM_STAGE_SCHEMA_VERSION = 3;
|
|
21
|
+
const SLURM_STAGE_CURRENT_LINK = 'current';
|
|
22
|
+
const SLURM_STAGE_STORE_DIR = '.stage-store';
|
|
23
|
+
const SLURM_STAGE_LOCK_DIR = 'startup-stage.lock';
|
|
24
|
+
const SLURM_STAGE_MANIFEST_FILE = 'manifest.json';
|
|
25
|
+
const SLURM_STAGE_ROOT_IN_CONTAINER = '/home/sandbox/.labgate/slurm/current';
|
|
17
26
|
const SKIP_LIB_BASENAMES = new Set([
|
|
18
27
|
'linux-vdso.so.1',
|
|
19
28
|
'libc.so.6',
|
|
@@ -94,6 +103,77 @@ function copyFileBestEffort(src, dst) {
|
|
|
94
103
|
// Best effort
|
|
95
104
|
}
|
|
96
105
|
}
|
|
106
|
+
function sha256File(path) {
|
|
107
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
108
|
+
hash.update((0, fs_1.readFileSync)(path));
|
|
109
|
+
return hash.digest('hex');
|
|
110
|
+
}
|
|
111
|
+
function fingerprintPath(path, options = {}) {
|
|
112
|
+
try {
|
|
113
|
+
const st = (0, fs_1.statSync)(path);
|
|
114
|
+
if (!st.isFile())
|
|
115
|
+
return null;
|
|
116
|
+
const fingerprint = {
|
|
117
|
+
path,
|
|
118
|
+
size: st.size,
|
|
119
|
+
mtimeMs: Math.floor(st.mtimeMs),
|
|
120
|
+
};
|
|
121
|
+
if (options.hashContent) {
|
|
122
|
+
fingerprint.sha256 = sha256File(path);
|
|
123
|
+
}
|
|
124
|
+
return fingerprint;
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
return null;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
function updateDirectoryHashRecursive(rootDir, relativeDir, hash) {
|
|
131
|
+
const dirPath = relativeDir ? (0, path_1.join)(rootDir, relativeDir) : rootDir;
|
|
132
|
+
let entryCount = 0;
|
|
133
|
+
const entries = (0, fs_1.readdirSync)(dirPath).sort((a, b) => a.localeCompare(b));
|
|
134
|
+
for (const entry of entries) {
|
|
135
|
+
const relativePath = relativeDir ? (0, path_1.join)(relativeDir, entry) : entry;
|
|
136
|
+
const normalizedRelativePath = relativePath.replace(/\\/g, '/');
|
|
137
|
+
const absPath = (0, path_1.join)(rootDir, relativePath);
|
|
138
|
+
const st = (0, fs_1.lstatSync)(absPath);
|
|
139
|
+
if (st.isDirectory()) {
|
|
140
|
+
hash.update(`dir:${normalizedRelativePath}\n`);
|
|
141
|
+
entryCount += 1 + updateDirectoryHashRecursive(rootDir, relativePath, hash);
|
|
142
|
+
continue;
|
|
143
|
+
}
|
|
144
|
+
if (st.isSymbolicLink()) {
|
|
145
|
+
hash.update(`link:${normalizedRelativePath}\n`);
|
|
146
|
+
hash.update((0, fs_1.readlinkSync)(absPath));
|
|
147
|
+
hash.update('\n');
|
|
148
|
+
entryCount += 1;
|
|
149
|
+
continue;
|
|
150
|
+
}
|
|
151
|
+
if (!st.isFile())
|
|
152
|
+
continue;
|
|
153
|
+
hash.update(`file:${normalizedRelativePath}:${st.size}:${Math.floor(st.mtimeMs)}\n`);
|
|
154
|
+
entryCount += 1;
|
|
155
|
+
}
|
|
156
|
+
return entryCount;
|
|
157
|
+
}
|
|
158
|
+
function fingerprintDirectory(path) {
|
|
159
|
+
if (!path)
|
|
160
|
+
return null;
|
|
161
|
+
try {
|
|
162
|
+
const st = (0, fs_1.statSync)(path);
|
|
163
|
+
if (!st.isDirectory())
|
|
164
|
+
return null;
|
|
165
|
+
const hash = (0, crypto_1.createHash)('sha256');
|
|
166
|
+
const entryCount = updateDirectoryHashRecursive(path, '', hash);
|
|
167
|
+
return {
|
|
168
|
+
path,
|
|
169
|
+
entryCount,
|
|
170
|
+
sha256: hash.digest('hex'),
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
97
177
|
function collectLinkedLibs(filePath, out) {
|
|
98
178
|
try {
|
|
99
179
|
const lddOut = (0, child_process_1.execFileSync)('ldd', [filePath], { encoding: 'utf-8' });
|
|
@@ -195,9 +275,10 @@ function buildHostProxyClientScript() {
|
|
|
195
275
|
'import { createConnection } from "net";',
|
|
196
276
|
'',
|
|
197
277
|
'const [socketPath, command, ...args] = process.argv.slice(2);',
|
|
278
|
+
'const UNAVAILABLE_EXIT_CODE = 245;',
|
|
198
279
|
'if (!socketPath || !command) {',
|
|
199
280
|
' console.error("[labgate] host proxy usage: <socket> <command> [args...]");',
|
|
200
|
-
|
|
281
|
+
` process.exit(UNAVAILABLE_EXIT_CODE);`,
|
|
201
282
|
'}',
|
|
202
283
|
'',
|
|
203
284
|
'const readStdin = async () => {',
|
|
@@ -223,7 +304,7 @@ function buildHostProxyClientScript() {
|
|
|
223
304
|
' sock.on("data", (d) => { raw += d; });',
|
|
224
305
|
' sock.on("error", (err) => {',
|
|
225
306
|
' console.error(`[labgate] host SLURM proxy error: ${err.message}`);',
|
|
226
|
-
|
|
307
|
+
` process.exit(UNAVAILABLE_EXIT_CODE);`,
|
|
227
308
|
' });',
|
|
228
309
|
' sock.on("end", () => {',
|
|
229
310
|
' try {',
|
|
@@ -234,7 +315,7 @@ function buildHostProxyClientScript() {
|
|
|
234
315
|
' process.exit(code);',
|
|
235
316
|
' } catch (err) {',
|
|
236
317
|
' console.error(`[labgate] invalid proxy response: ${err?.message ?? String(err)}`);',
|
|
237
|
-
|
|
318
|
+
` process.exit(UNAVAILABLE_EXIT_CODE);`,
|
|
238
319
|
' }',
|
|
239
320
|
' });',
|
|
240
321
|
' sock.write(JSON.stringify(req));',
|
|
@@ -243,17 +324,18 @@ function buildHostProxyClientScript() {
|
|
|
243
324
|
'',
|
|
244
325
|
'main().catch((err) => {',
|
|
245
326
|
' console.error(`[labgate] host SLURM proxy failure: ${err?.message ?? String(err)}`);',
|
|
246
|
-
|
|
327
|
+
` process.exit(UNAVAILABLE_EXIT_CODE);`,
|
|
247
328
|
'});',
|
|
248
329
|
'',
|
|
249
330
|
].join('\n');
|
|
250
331
|
}
|
|
251
|
-
function buildSlurmWrapperScript(cmd, stageRootInContainer, hostProxySocketInContainer) {
|
|
332
|
+
function buildSlurmWrapperScript(cmd, stageRootInContainer, hostProxySocketInContainer, options = {}) {
|
|
333
|
+
const proxyOnly = options.proxyOnly ?? false;
|
|
252
334
|
const real = `${stageRootInContainer}/bin/${cmd}`;
|
|
253
335
|
const lib = `${stageRootInContainer}/lib`;
|
|
254
336
|
const conf = `${stageRootInContainer}/etc/slurm.conf`;
|
|
255
337
|
const proxyClient = `${stageRootInContainer}/host-proxy-client.mjs`;
|
|
256
|
-
|
|
338
|
+
const lines = [
|
|
257
339
|
'#!/usr/bin/env bash',
|
|
258
340
|
'set -euo pipefail',
|
|
259
341
|
`REAL="${real}"`,
|
|
@@ -262,25 +344,102 @@ function buildSlurmWrapperScript(cmd, stageRootInContainer, hostProxySocketInCon
|
|
|
262
344
|
`PROXY_SOCKET="${hostProxySocketInContainer || ''}"`,
|
|
263
345
|
`PROXY_CLIENT="${proxyClient}"`,
|
|
264
346
|
'if [ -n "$PROXY_SOCKET" ] && [ -S "$PROXY_SOCKET" ] && [ -f "$PROXY_CLIENT" ] && command -v node >/dev/null 2>&1; then',
|
|
265
|
-
|
|
347
|
+
' if node "$PROXY_CLIENT" "$PROXY_SOCKET" "${cmd}" "$@"; then',
|
|
266
348
|
' exit 0',
|
|
349
|
+
' else',
|
|
350
|
+
' proxy_exit_code=$?',
|
|
351
|
+
' if [ "$proxy_exit_code" -ne 245 ]; then',
|
|
352
|
+
' exit "$proxy_exit_code"',
|
|
353
|
+
' fi',
|
|
267
354
|
' fi',
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
'
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
355
|
+
];
|
|
356
|
+
if (proxyOnly) {
|
|
357
|
+
lines.push('fi', `echo "[labgate] host SLURM proxy unavailable for '${cmd}' in proxy-only mode." >&2`, 'echo "[labgate] Fix: ensure the LabGate host proxy is running and host SLURM commands are available, then restart the session." >&2', 'exit 127', '');
|
|
358
|
+
return lines.join('\n');
|
|
359
|
+
}
|
|
360
|
+
lines.push(' echo "[labgate] host SLURM proxy unavailable; falling back to staged CLI." >&2', 'fi', 'if [ ! -x "$REAL" ]; then', ` echo "[labgate] SLURM CLI '${cmd}' is not staged in this sandbox." >&2`, ' echo "[labgate] Fix: ensure SLURM is available on the host (e.g. login node), then restart the LabGate session." >&2', ' exit 127', 'fi', 'if [ -d "$LIBDIR" ]; then', ' export LD_LIBRARY_PATH="$LIBDIR${LD_LIBRARY_PATH:+:$LD_LIBRARY_PATH}"', 'fi', 'if [ -z "${SLURM_CONF:-}" ] && [ -f "$CONF" ]; then', ' export SLURM_CONF="$CONF"', 'fi', 'exec "$REAL" "$@"', '');
|
|
361
|
+
return lines.join('\n');
|
|
362
|
+
}
|
|
363
|
+
function removeSlurmPath(path) {
|
|
364
|
+
try {
|
|
365
|
+
(0, fs_1.rmSync)(path, { recursive: true, force: true });
|
|
366
|
+
}
|
|
367
|
+
catch {
|
|
368
|
+
// Best effort cleanup.
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
function cleanupSlurmWrappers(wrapperDir, keep, candidates) {
|
|
372
|
+
const keepSet = new Set(keep);
|
|
373
|
+
for (const cmd of new Set(candidates)) {
|
|
374
|
+
if (keepSet.has(cmd))
|
|
375
|
+
continue;
|
|
376
|
+
removeSlurmPath((0, path_1.join)(wrapperDir, cmd));
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function readSlurmStageManifest(stageRoot) {
|
|
380
|
+
try {
|
|
381
|
+
const raw = JSON.parse((0, fs_1.readFileSync)((0, path_1.join)(stageRoot, SLURM_STAGE_MANIFEST_FILE), 'utf-8'));
|
|
382
|
+
if (!raw || typeof raw !== 'object')
|
|
383
|
+
return null;
|
|
384
|
+
if (Number(raw.schema_version) !== SLURM_STAGE_SCHEMA_VERSION)
|
|
385
|
+
return null;
|
|
386
|
+
return raw;
|
|
387
|
+
}
|
|
388
|
+
catch {
|
|
389
|
+
return null;
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
function getSlurmStageLockPath(sandboxHome) {
|
|
393
|
+
return (0, path_1.join)(sandboxHome, '.labgate', 'locks', SLURM_STAGE_LOCK_DIR);
|
|
394
|
+
}
|
|
395
|
+
function computeStageFingerprint(input) {
|
|
396
|
+
return (0, crypto_1.createHash)('sha256').update(JSON.stringify(input)).digest('hex');
|
|
397
|
+
}
|
|
398
|
+
function ensureStageStoreLink(stageRoot, targetDir) {
|
|
399
|
+
const currentLink = (0, path_1.join)(stageRoot, SLURM_STAGE_CURRENT_LINK);
|
|
400
|
+
const tempLink = (0, path_1.join)(stageRoot, `.${SLURM_STAGE_CURRENT_LINK}.tmp-${process.pid}-${(0, crypto_1.randomBytes)(6).toString('hex')}`);
|
|
401
|
+
let currentExists = false;
|
|
402
|
+
let currentIsSymlink = false;
|
|
403
|
+
try {
|
|
404
|
+
currentIsSymlink = (0, fs_1.lstatSync)(currentLink).isSymbolicLink();
|
|
405
|
+
currentExists = true;
|
|
406
|
+
}
|
|
407
|
+
catch {
|
|
408
|
+
currentExists = false;
|
|
409
|
+
currentIsSymlink = false;
|
|
410
|
+
}
|
|
411
|
+
(0, fs_1.symlinkSync)(targetDir, tempLink, 'dir');
|
|
412
|
+
if (currentExists && currentIsSymlink) {
|
|
413
|
+
try {
|
|
414
|
+
const currentTarget = (0, path_1.resolve)(stageRoot, (0, fs_1.readlinkSync)(currentLink));
|
|
415
|
+
if (currentTarget === targetDir) {
|
|
416
|
+
removeSlurmPath(tempLink);
|
|
417
|
+
return;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
// Fall through to replace.
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
if (!currentExists || currentIsSymlink) {
|
|
425
|
+
(0, fs_1.renameSync)(tempLink, currentLink);
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
const legacyDir = (0, path_1.join)(stageRoot, `.${SLURM_STAGE_CURRENT_LINK}.legacy-${(0, crypto_1.randomBytes)(6).toString('hex')}`);
|
|
429
|
+
(0, fs_1.renameSync)(currentLink, legacyDir);
|
|
430
|
+
try {
|
|
431
|
+
(0, fs_1.renameSync)(tempLink, currentLink);
|
|
432
|
+
}
|
|
433
|
+
catch (err) {
|
|
434
|
+
(0, fs_1.renameSync)(legacyDir, currentLink);
|
|
435
|
+
throw err;
|
|
436
|
+
}
|
|
437
|
+
removeSlurmPath(legacyDir);
|
|
438
|
+
}
|
|
439
|
+
function cleanupLegacyStageLayout(stageRoot) {
|
|
440
|
+
for (const name of ['bin', 'lib', 'etc', 'plugins', 'host-proxy-client.mjs']) {
|
|
441
|
+
removeSlurmPath((0, path_1.join)(stageRoot, name));
|
|
442
|
+
}
|
|
284
443
|
}
|
|
285
444
|
function tryExtractPluginDir(slurmConfText) {
|
|
286
445
|
for (const raw of slurmConfText.split('\n')) {
|
|
@@ -335,33 +494,35 @@ function rewriteDaemonUsers(slurmConfText) {
|
|
|
335
494
|
* in $HOME/.labgate-bin so they are available inside the container even when the image
|
|
336
495
|
* does not ship with SLURM.
|
|
337
496
|
*
|
|
338
|
-
*
|
|
339
|
-
*
|
|
497
|
+
* When host proxy mode is preferred, this writes proxy-only wrappers without paying the
|
|
498
|
+
* binary/plugin copy cost on every startup. Otherwise it stages binaries + ldd libraries
|
|
499
|
+
* + slurm.conf (if found). The host-side session launcher is responsible for bind-mounting
|
|
500
|
+
* /run/munge when present.
|
|
340
501
|
*/
|
|
341
502
|
function stageSlurmCliPassthrough(options) {
|
|
342
503
|
const env = options.env ?? process.env;
|
|
343
504
|
const commands = options.commands ?? [...DEFAULT_SLURM_COMMANDS];
|
|
344
505
|
const installMissingWrappers = options.installMissingWrappers ?? true;
|
|
345
506
|
const hostProxySocketInContainer = options.hostProxySocketInContainer ?? null;
|
|
507
|
+
const preferHostProxy = options.preferHostProxy ?? false;
|
|
346
508
|
const stageRoot = (0, path_1.join)(options.sandboxHome, '.labgate', 'slurm');
|
|
347
|
-
const
|
|
348
|
-
const libDir = (0, path_1.join)(stageRoot, 'lib');
|
|
349
|
-
const etcDir = (0, path_1.join)(stageRoot, 'etc');
|
|
509
|
+
const storeRoot = (0, path_1.join)(stageRoot, SLURM_STAGE_STORE_DIR);
|
|
350
510
|
const wrapperDir = (0, path_1.join)(options.sandboxHome, '.labgate-bin');
|
|
351
|
-
const
|
|
352
|
-
const staged = [];
|
|
511
|
+
const lockPath = getSlurmStageLockPath(options.sandboxHome);
|
|
353
512
|
const skipped = [];
|
|
354
513
|
try {
|
|
355
514
|
(0, config_js_1.ensurePrivateDir)((0, path_1.join)(options.sandboxHome, '.labgate'));
|
|
515
|
+
(0, config_js_1.ensurePrivateDir)((0, path_1.join)(options.sandboxHome, '.labgate', 'locks'));
|
|
356
516
|
(0, config_js_1.ensurePrivateDir)(stageRoot);
|
|
357
|
-
(0, config_js_1.ensurePrivateDir)(
|
|
358
|
-
(0, config_js_1.ensurePrivateDir)(libDir);
|
|
359
|
-
(0, config_js_1.ensurePrivateDir)(etcDir);
|
|
517
|
+
(0, config_js_1.ensurePrivateDir)(storeRoot);
|
|
360
518
|
(0, config_js_1.ensurePrivateDir)(wrapperDir);
|
|
361
519
|
}
|
|
362
520
|
catch (err) {
|
|
363
521
|
return {
|
|
364
522
|
ok: false,
|
|
523
|
+
mode: 'staged',
|
|
524
|
+
reused: false,
|
|
525
|
+
hostCommands: [],
|
|
365
526
|
staged: [],
|
|
366
527
|
skipped: commands,
|
|
367
528
|
errors: [`Failed to initialize SLURM stage directories: ${err?.message ?? String(err)}`],
|
|
@@ -369,7 +530,7 @@ function stageSlurmCliPassthrough(options) {
|
|
|
369
530
|
stageRoot,
|
|
370
531
|
};
|
|
371
532
|
}
|
|
372
|
-
// 1) Resolve host command paths
|
|
533
|
+
// 1) Resolve host command paths.
|
|
373
534
|
const hostPaths = new Map();
|
|
374
535
|
for (const cmd of commands) {
|
|
375
536
|
const hostPath = which(cmd, env);
|
|
@@ -378,134 +539,231 @@ function stageSlurmCliPassthrough(options) {
|
|
|
378
539
|
continue;
|
|
379
540
|
}
|
|
380
541
|
hostPaths.set(cmd, hostPath);
|
|
381
|
-
|
|
542
|
+
}
|
|
543
|
+
const hostCommands = Array.from(hostPaths.keys());
|
|
544
|
+
const proxyOnly = !!hostProxySocketInContainer && preferHostProxy && hostCommands.length > 0;
|
|
545
|
+
const sampleCmd = hostPaths.get('squeue') ?? hostPaths.get('scontrol') ?? hostPaths.get('sbatch') ?? null;
|
|
546
|
+
const slurmConfPath = proxyOnly ? null : resolveSlurmConf(sampleCmd, env);
|
|
547
|
+
let slurmConfText = '';
|
|
548
|
+
if (slurmConfPath) {
|
|
382
549
|
try {
|
|
383
|
-
(0, fs_1.
|
|
384
|
-
// Strip any unexpected setuid/setgid bits from host binaries.
|
|
385
|
-
try {
|
|
386
|
-
(0, fs_1.chmodSync)(target, 0o755);
|
|
387
|
-
}
|
|
388
|
-
catch { /* ignore */ }
|
|
389
|
-
staged.push(cmd);
|
|
550
|
+
slurmConfText = (0, fs_1.readFileSync)(slurmConfPath, 'utf-8');
|
|
390
551
|
}
|
|
391
|
-
catch
|
|
392
|
-
|
|
393
|
-
errors.push(`Failed to stage ${cmd} from ${hostPath}: ${err?.message ?? String(err)}`);
|
|
552
|
+
catch {
|
|
553
|
+
slurmConfText = '';
|
|
394
554
|
}
|
|
395
555
|
}
|
|
396
|
-
|
|
397
|
-
const
|
|
398
|
-
|
|
556
|
+
const pluginDir = proxyOnly ? null : resolvePluginDir(sampleCmd, slurmConfText, env);
|
|
557
|
+
const slurmConfFingerprint = (proxyOnly || !slurmConfPath) ? null : fingerprintPath(slurmConfPath, { hashContent: true });
|
|
558
|
+
const pluginDirFingerprint = proxyOnly ? null : fingerprintDirectory(pluginDir);
|
|
559
|
+
const commandFingerprints = hostCommands
|
|
560
|
+
.map((cmd) => {
|
|
399
561
|
const hostPath = hostPaths.get(cmd);
|
|
400
562
|
if (!hostPath)
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
563
|
+
return null;
|
|
564
|
+
const fp = fingerprintPath(hostPath);
|
|
565
|
+
if (!fp)
|
|
566
|
+
return null;
|
|
567
|
+
return { command: cmd, ...fp };
|
|
568
|
+
})
|
|
569
|
+
.filter((entry) => !!entry);
|
|
570
|
+
const fingerprint = computeStageFingerprint({
|
|
571
|
+
schema_version: SLURM_STAGE_SCHEMA_VERSION,
|
|
572
|
+
host: (0, os_1.hostname)(),
|
|
573
|
+
mode: proxyOnly ? 'proxy-only' : 'staged',
|
|
574
|
+
requested_commands: commands,
|
|
575
|
+
host_commands: commandFingerprints,
|
|
576
|
+
slurm_conf: slurmConfFingerprint,
|
|
577
|
+
plugin_dir: pluginDirFingerprint,
|
|
578
|
+
host_proxy_socket: hostProxySocketInContainer,
|
|
579
|
+
});
|
|
580
|
+
try {
|
|
581
|
+
return (0, startup_stage_lock_js_1.withStartupStageLock)(lockPath, 'slurm-stage', () => {
|
|
582
|
+
(0, config_js_1.ensurePrivateDir)(stageRoot);
|
|
583
|
+
(0, config_js_1.ensurePrivateDir)(storeRoot);
|
|
584
|
+
(0, config_js_1.ensurePrivateDir)(wrapperDir);
|
|
585
|
+
cleanupLegacyStageLayout(stageRoot);
|
|
586
|
+
const stageMode = proxyOnly ? 'proxy-only' : 'staged';
|
|
587
|
+
const storeId = fingerprint;
|
|
588
|
+
const storeDir = (0, path_1.join)(storeRoot, storeId);
|
|
589
|
+
const existingManifest = readSlurmStageManifest(stageRoot);
|
|
590
|
+
let reused = false;
|
|
591
|
+
let staged = [];
|
|
592
|
+
let errors = [];
|
|
593
|
+
let libCount = 0;
|
|
594
|
+
const reuseExistingStore = (existingManifest &&
|
|
595
|
+
existingManifest.fingerprint === fingerprint &&
|
|
596
|
+
existingManifest.mode === stageMode &&
|
|
597
|
+
existingManifest.store_id === storeId &&
|
|
598
|
+
(0, fs_1.existsSync)(storeDir));
|
|
599
|
+
if (!reuseExistingStore && !(0, fs_1.existsSync)(storeDir)) {
|
|
600
|
+
const tempStoreDir = (0, path_1.join)(storeRoot, `.tmp-${storeId}-${(0, crypto_1.randomBytes)(6).toString('hex')}`);
|
|
601
|
+
removeSlurmPath(tempStoreDir);
|
|
602
|
+
(0, fs_1.mkdirSync)(tempStoreDir, { recursive: true, mode: 0o700 });
|
|
418
603
|
try {
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
604
|
+
if (!proxyOnly) {
|
|
605
|
+
const binDir = (0, path_1.join)(tempStoreDir, 'bin');
|
|
606
|
+
const libDir = (0, path_1.join)(tempStoreDir, 'lib');
|
|
607
|
+
const etcDir = (0, path_1.join)(tempStoreDir, 'etc');
|
|
608
|
+
(0, fs_1.mkdirSync)(binDir, { recursive: true, mode: 0o700 });
|
|
609
|
+
(0, fs_1.mkdirSync)(libDir, { recursive: true, mode: 0o700 });
|
|
610
|
+
(0, fs_1.mkdirSync)(etcDir, { recursive: true, mode: 0o700 });
|
|
611
|
+
for (const [cmd, hostPath] of hostPaths) {
|
|
612
|
+
const target = (0, path_1.join)(binDir, cmd);
|
|
613
|
+
try {
|
|
614
|
+
(0, fs_1.copyFileSync)(hostPath, target);
|
|
615
|
+
try {
|
|
616
|
+
(0, fs_1.chmodSync)(target, 0o755);
|
|
617
|
+
}
|
|
618
|
+
catch { /* ignore */ }
|
|
619
|
+
staged.push(cmd);
|
|
620
|
+
}
|
|
621
|
+
catch (err) {
|
|
622
|
+
skipped.push(cmd);
|
|
623
|
+
errors.push(`Failed to stage ${cmd} from ${hostPath}: ${err?.message ?? String(err)}`);
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
const libsToCopy = new Set();
|
|
627
|
+
for (const cmd of staged) {
|
|
628
|
+
const hostPath = hostPaths.get(cmd);
|
|
629
|
+
if (!hostPath)
|
|
630
|
+
continue;
|
|
631
|
+
collectLinkedLibs(hostPath, libsToCopy);
|
|
632
|
+
}
|
|
633
|
+
if (slurmConfPath) {
|
|
634
|
+
try {
|
|
635
|
+
const pluginDirInContainer = `${SLURM_STAGE_ROOT_IN_CONTAINER}/plugins`;
|
|
636
|
+
let rewritten = rewritePluginDir(slurmConfText, pluginDirInContainer);
|
|
637
|
+
rewritten = rewriteDaemonUsers(rewritten);
|
|
638
|
+
const stagedPluginRoot = (0, path_1.join)(tempStoreDir, 'plugins');
|
|
639
|
+
(0, fs_1.mkdirSync)(stagedPluginRoot, { recursive: true, mode: 0o700 });
|
|
640
|
+
if (pluginDir) {
|
|
641
|
+
try {
|
|
642
|
+
(0, fs_1.cpSync)(pluginDir, stagedPluginRoot, { recursive: true, force: true });
|
|
643
|
+
for (const so of listSharedObjects(stagedPluginRoot)) {
|
|
644
|
+
collectLinkedLibs(so, libsToCopy);
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
catch (err) {
|
|
648
|
+
errors.push(`Failed to stage SLURM plugins from ${pluginDir}: ${err?.message ?? String(err)}`);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
else {
|
|
652
|
+
errors.push('Could not detect a host SLURM plugin directory to stage. ' +
|
|
653
|
+
'In-container SLURM commands may fail if plugins are required.');
|
|
654
|
+
}
|
|
655
|
+
(0, startup_stage_lock_js_1.writeTextFileAtomic)((0, path_1.join)(etcDir, 'slurm.conf'), `${rewritten.replace(/\n?$/, '\n')}`, { mode: 0o644 });
|
|
656
|
+
}
|
|
657
|
+
catch (err) {
|
|
658
|
+
errors.push(`Failed to stage slurm.conf from ${slurmConfPath}: ${err?.message ?? String(err)}`);
|
|
659
|
+
}
|
|
660
|
+
}
|
|
661
|
+
for (const lib of libsToCopy) {
|
|
662
|
+
const target = (0, path_1.join)(libDir, (0, path_1.basename)(lib));
|
|
663
|
+
if ((0, fs_1.existsSync)(target))
|
|
664
|
+
continue;
|
|
665
|
+
copyFileBestEffort(lib, target);
|
|
666
|
+
}
|
|
667
|
+
libCount = libsToCopy.size;
|
|
668
|
+
}
|
|
669
|
+
if (hostProxySocketInContainer) {
|
|
670
|
+
try {
|
|
671
|
+
(0, startup_stage_lock_js_1.writeTextFileAtomic)((0, path_1.join)(tempStoreDir, 'host-proxy-client.mjs'), `${buildHostProxyClientScript()}\n`, { mode: 0o755 });
|
|
672
|
+
}
|
|
673
|
+
catch (err) {
|
|
674
|
+
errors.push(`Failed to write host SLURM proxy client: ${err?.message ?? String(err)}`);
|
|
675
|
+
}
|
|
426
676
|
}
|
|
677
|
+
(0, fs_1.renameSync)(tempStoreDir, storeDir);
|
|
427
678
|
}
|
|
428
679
|
catch (err) {
|
|
429
|
-
|
|
680
|
+
removeSlurmPath(tempStoreDir);
|
|
681
|
+
throw err;
|
|
430
682
|
}
|
|
431
683
|
}
|
|
432
684
|
else {
|
|
433
|
-
|
|
434
|
-
'In-container SLURM commands may fail if plugins are required.');
|
|
685
|
+
reused = true;
|
|
435
686
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
687
|
+
if (reused && existingManifest) {
|
|
688
|
+
staged = [...existingManifest.commands];
|
|
689
|
+
errors = [...existingManifest.errors];
|
|
690
|
+
libCount = existingManifest.lib_count;
|
|
691
|
+
}
|
|
692
|
+
else if (reused) {
|
|
693
|
+
staged = proxyOnly ? [] : [...hostCommands];
|
|
694
|
+
}
|
|
695
|
+
const effectiveSkipped = reused && existingManifest
|
|
696
|
+
? [...new Set([...skipped, ...existingManifest.skipped])]
|
|
697
|
+
: [...new Set(skipped)];
|
|
698
|
+
ensureStageStoreLink(stageRoot, storeDir);
|
|
699
|
+
const wrapperCommands = proxyOnly
|
|
700
|
+
? hostCommands
|
|
701
|
+
: (installMissingWrappers ? commands : staged);
|
|
702
|
+
cleanupSlurmWrappers(wrapperDir, wrapperCommands, [...DEFAULT_SLURM_COMMANDS, ...commands]);
|
|
703
|
+
for (const cmd of wrapperCommands) {
|
|
704
|
+
const wrapperPath = (0, path_1.join)(wrapperDir, cmd);
|
|
705
|
+
try {
|
|
706
|
+
const script = buildSlurmWrapperScript(cmd, SLURM_STAGE_ROOT_IN_CONTAINER, hostProxySocketInContainer, { proxyOnly });
|
|
707
|
+
(0, startup_stage_lock_js_1.writeTextFileAtomic)(wrapperPath, `${script.replace(/\n?$/, '\n')}`, { mode: 0o755 });
|
|
708
|
+
}
|
|
709
|
+
catch (err) {
|
|
710
|
+
errors.push(`Failed to write wrapper for ${cmd}: ${err?.message ?? String(err)}`);
|
|
440
711
|
}
|
|
441
712
|
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
713
|
+
const manifest = {
|
|
714
|
+
schema_version: SLURM_STAGE_SCHEMA_VERSION,
|
|
715
|
+
fingerprint,
|
|
716
|
+
mode: stageMode,
|
|
717
|
+
host: (0, os_1.hostname)(),
|
|
718
|
+
requested_commands: [...commands],
|
|
719
|
+
host_commands: [...hostCommands],
|
|
720
|
+
commands: [...staged],
|
|
721
|
+
skipped: effectiveSkipped,
|
|
722
|
+
errors: [...errors],
|
|
723
|
+
slurm_conf: slurmConfFingerprint,
|
|
724
|
+
plugin_dir: pluginDirFingerprint,
|
|
725
|
+
host_proxy_socket: hostProxySocketInContainer,
|
|
726
|
+
lib_count: libCount,
|
|
727
|
+
store_id: storeId,
|
|
728
|
+
stage_root_in_container: SLURM_STAGE_ROOT_IN_CONTAINER,
|
|
729
|
+
staged_at: new Date().toISOString(),
|
|
730
|
+
notes: proxyOnly
|
|
731
|
+
? 'Host SLURM CLI routed exclusively through the LabGate host proxy.'
|
|
732
|
+
: reused
|
|
733
|
+
? 'Host SLURM CLI stage reused from cached manifest and fingerprint.'
|
|
734
|
+
: 'Host SLURM CLI staged by LabGate for in-container use.',
|
|
735
|
+
};
|
|
736
|
+
try {
|
|
737
|
+
(0, startup_stage_lock_js_1.writeTextFileAtomic)((0, path_1.join)(stageRoot, SLURM_STAGE_MANIFEST_FILE), JSON.stringify(manifest, null, 2) + '\n', { mode: 0o644 });
|
|
446
738
|
}
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
// installs wrappers only for successfully staged commands.
|
|
463
|
-
const stageRootInContainer = '/home/sandbox/.labgate/slurm';
|
|
464
|
-
if (hostProxySocketInContainer) {
|
|
465
|
-
try {
|
|
466
|
-
(0, fs_1.writeFileSync)((0, path_1.join)(stageRoot, 'host-proxy-client.mjs'), buildHostProxyClientScript(), {
|
|
467
|
-
encoding: 'utf-8',
|
|
468
|
-
mode: 0o755,
|
|
469
|
-
});
|
|
470
|
-
}
|
|
471
|
-
catch (err) {
|
|
472
|
-
errors.push(`Failed to write host SLURM proxy client: ${err?.message ?? String(err)}`);
|
|
473
|
-
}
|
|
474
|
-
}
|
|
475
|
-
const wrapperCommands = installMissingWrappers ? commands : staged;
|
|
476
|
-
for (const cmd of wrapperCommands) {
|
|
477
|
-
const wrapperPath = (0, path_1.join)(wrapperDir, cmd);
|
|
478
|
-
try {
|
|
479
|
-
const script = buildSlurmWrapperScript(cmd, stageRootInContainer, hostProxySocketInContainer);
|
|
480
|
-
(0, fs_1.writeFileSync)(wrapperPath, script, { encoding: 'utf-8', mode: 0o755 });
|
|
481
|
-
}
|
|
482
|
-
catch (err) {
|
|
483
|
-
errors.push(`Failed to write wrapper for ${cmd}: ${err?.message ?? String(err)}`);
|
|
484
|
-
}
|
|
739
|
+
catch {
|
|
740
|
+
// Best effort only.
|
|
741
|
+
}
|
|
742
|
+
return {
|
|
743
|
+
ok: proxyOnly ? hostCommands.length > 0 : staged.length > 0,
|
|
744
|
+
mode: stageMode,
|
|
745
|
+
reused,
|
|
746
|
+
hostCommands,
|
|
747
|
+
staged,
|
|
748
|
+
skipped: effectiveSkipped,
|
|
749
|
+
errors,
|
|
750
|
+
slurmConfPath: slurmConfFingerprint?.path ?? null,
|
|
751
|
+
stageRoot,
|
|
752
|
+
};
|
|
753
|
+
});
|
|
485
754
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
755
|
+
catch (err) {
|
|
756
|
+
return {
|
|
757
|
+
ok: false,
|
|
758
|
+
mode: proxyOnly ? 'proxy-only' : 'staged',
|
|
759
|
+
reused: false,
|
|
760
|
+
hostCommands,
|
|
761
|
+
staged: [],
|
|
762
|
+
skipped: [...new Set(skipped)],
|
|
763
|
+
errors: [`SLURM passthrough staging failed: ${err?.message ?? String(err)}`],
|
|
764
|
+
slurmConfPath: slurmConfFingerprint?.path ?? null,
|
|
765
|
+
stageRoot,
|
|
496
766
|
};
|
|
497
|
-
(0, fs_1.writeFileSync)((0, path_1.join)(stageRoot, 'manifest.json'), JSON.stringify(manifest, null, 2) + '\n', 'utf-8');
|
|
498
767
|
}
|
|
499
|
-
catch {
|
|
500
|
-
// ignore
|
|
501
|
-
}
|
|
502
|
-
return {
|
|
503
|
-
ok: staged.length > 0,
|
|
504
|
-
staged,
|
|
505
|
-
skipped,
|
|
506
|
-
errors,
|
|
507
|
-
slurmConfPath,
|
|
508
|
-
stageRoot,
|
|
509
|
-
};
|
|
510
768
|
}
|
|
511
769
|
//# sourceMappingURL=slurm-cli-passthrough.js.map
|