nebula-notebook 0.1.1 → 0.2.1
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 +110 -16
- package/dist/assets/errorwidget-Wy5ajPiM.js +5 -0
- package/dist/assets/fa-brands-400-CEJbCg16.woff +0 -0
- package/dist/assets/fa-brands-400-CSYNqBb_.ttf +0 -0
- package/dist/assets/fa-brands-400-DnkPfk3o.eot +0 -0
- package/dist/assets/fa-brands-400-UxlILjvJ.woff2 +0 -0
- package/dist/assets/fa-brands-400-cH1MgKbP.svg +3717 -0
- package/dist/assets/fa-regular-400-BhTwtT8w.eot +0 -0
- package/dist/assets/fa-regular-400-D1vz6WBx.ttf +0 -0
- package/dist/assets/fa-regular-400-DFnMcJPd.woff +0 -0
- package/dist/assets/fa-regular-400-DGzu1beS.woff2 +0 -0
- package/dist/assets/fa-regular-400-gwj8Pxq-.svg +801 -0
- package/dist/assets/fa-solid-900-B4ZZ7kfP.svg +5034 -0
- package/dist/assets/fa-solid-900-B6Axprfb.eot +0 -0
- package/dist/assets/fa-solid-900-BUswJgRo.woff2 +0 -0
- package/dist/assets/fa-solid-900-DOXgCApm.woff +0 -0
- package/dist/assets/fa-solid-900-mxuxnBEa.ttf +0 -0
- package/dist/assets/index-B_Bs5koo.js +1 -0
- package/dist/assets/index-BtWv4MIT.css +7 -0
- package/dist/assets/index-DK4nS74r.js +829 -0
- package/dist/assets/index-DeQN03nW.js +81 -0
- package/dist/assets/index-DzjpmUak.css +32 -0
- package/dist/assets/index-g6MjT-VG.js +1 -0
- package/dist/assets/services-shim-BirHVsih.js +33 -0
- package/dist/assets/viewlist-uomDf7I7.js +1 -0
- package/dist/assets/widgets-X7J3NxEn.css +1 -0
- package/dist/index.html +2 -2
- package/node-server/dist/auth/auth-middleware.js +22 -4
- package/node-server/dist/auth/auth-service.js +37 -7
- package/node-server/dist/cluster/client-registration.d.ts +12 -1
- package/node-server/dist/cluster/client-registration.js +38 -2
- package/node-server/dist/cluster/kernel-proxy.js +43 -12
- package/node-server/dist/cluster/server-registry.d.ts +17 -0
- package/node-server/dist/cluster/server-registry.js +57 -7
- package/node-server/dist/discovery/conda-locations.d.ts +73 -0
- package/node-server/dist/discovery/conda-locations.js +427 -0
- package/node-server/dist/discovery/discovery-service.d.ts +73 -4
- package/node-server/dist/discovery/discovery-service.js +211 -127
- package/node-server/dist/discovery/types.d.ts +0 -1
- package/node-server/dist/discovery/types.js +1 -2
- package/node-server/dist/environment.d.ts +12 -0
- package/node-server/dist/environment.js +104 -0
- package/node-server/dist/fs/fs-service.d.ts +86 -8
- package/node-server/dist/fs/fs-service.js +640 -99
- package/node-server/dist/fs/notebook-formats/percent.d.ts +25 -0
- package/node-server/dist/fs/notebook-formats/percent.js +286 -0
- package/node-server/dist/fs/notebook-formats/qmd.d.ts +29 -0
- package/node-server/dist/fs/notebook-formats/qmd.js +307 -0
- package/node-server/dist/fs/notebook-formats/registry.d.ts +12 -0
- package/node-server/dist/fs/notebook-formats/registry.js +77 -0
- package/node-server/dist/fs/notebook-formats/types.d.ts +37 -0
- package/node-server/dist/fs/notebook-formats/types.js +13 -0
- package/node-server/dist/fs/types.d.ts +9 -0
- package/node-server/dist/fs/types.js +8 -0
- package/node-server/dist/idle-exit.d.ts +52 -0
- package/node-server/dist/idle-exit.js +83 -0
- package/node-server/dist/index.js +198 -8
- package/node-server/dist/kernel/kernel-service.d.ts +130 -2
- package/node-server/dist/kernel/kernel-service.js +834 -64
- package/node-server/dist/kernel/kernelspec.d.ts +27 -0
- package/node-server/dist/kernel/kernelspec.js +75 -4
- package/node-server/dist/notebook/headless-handler.d.ts +12 -0
- package/node-server/dist/notebook/headless-handler.js +125 -29
- package/node-server/dist/notebook/operation-router.js +7 -2
- package/node-server/dist/notebook/undoRedoManager.d.ts +12 -1
- package/node-server/dist/notebook/undoRedoManager.js +49 -18
- package/node-server/dist/output/display-data.js +2 -0
- package/node-server/dist/private-tmp.d.ts +14 -0
- package/node-server/dist/private-tmp.js +70 -0
- package/node-server/dist/routes/autocomplete.d.ts +19 -0
- package/node-server/dist/routes/autocomplete.js +294 -0
- package/node-server/dist/routes/cluster.js +2 -2
- package/node-server/dist/routes/compute.d.ts +8 -0
- package/node-server/dist/routes/compute.js +136 -0
- package/node-server/dist/routes/fs.js +23 -6
- package/node-server/dist/routes/kernel.js +156 -3
- package/node-server/dist/routes/notebook.js +37 -3
- package/node-server/dist/routes/python.d.ts +1 -2
- package/node-server/dist/routes/python.js +139 -3
- package/node-server/dist/scheduler/allocation-service.d.ts +63 -0
- package/node-server/dist/scheduler/allocation-service.js +285 -0
- package/node-server/dist/scheduler/job-template.d.ts +30 -0
- package/node-server/dist/scheduler/job-template.js +85 -0
- package/node-server/dist/scheduler/mock-scheduler.d.ts +30 -0
- package/node-server/dist/scheduler/mock-scheduler.js +125 -0
- package/node-server/dist/scheduler/slurm-scheduler.d.ts +31 -0
- package/node-server/dist/scheduler/slurm-scheduler.js +406 -0
- package/node-server/dist/scheduler/types.d.ts +118 -0
- package/node-server/dist/scheduler/types.js +8 -0
- package/node-server/dist/scheduler/util.d.ts +7 -0
- package/node-server/dist/scheduler/util.js +20 -0
- package/node-server/dist/terminal/agent-registry.d.ts +87 -0
- package/node-server/dist/terminal/agent-registry.js +246 -0
- package/node-server/dist/terminal/binding-store.d.ts +57 -0
- package/node-server/dist/terminal/binding-store.js +0 -0
- package/node-server/dist/terminal/pty-manager.d.ts +23 -6
- package/node-server/dist/terminal/pty-manager.js +138 -19
- package/node-server/dist/terminal/server.js +155 -23
- package/node-server/dist/terminal/types.d.ts +2 -0
- package/node-server/dist/update-check.d.ts +20 -0
- package/node-server/dist/update-check.js +114 -0
- package/node-server/package.json +2 -0
- package/node_modules/nebula-autocomplete/README.md +145 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.d.ts +69 -0
- package/node_modules/nebula-autocomplete/dist/backends/claude.js +449 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.d.ts +45 -0
- package/node_modules/nebula-autocomplete/dist/backends/codex.js +108 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.d.ts +16 -0
- package/node_modules/nebula-autocomplete/dist/client/fetcher.js +41 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.d.ts +43 -0
- package/node_modules/nebula-autocomplete/dist/codemirror/ghostText.js +175 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.d.ts +17 -0
- package/node_modules/nebula-autocomplete/dist/core/engine.js +135 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/core/lru.js +26 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.d.ts +12 -0
- package/node_modules/nebula-autocomplete/dist/core/prompt.js +0 -0
- package/node_modules/nebula-autocomplete/dist/core/text.d.ts +53 -0
- package/node_modules/nebula-autocomplete/dist/core/text.js +169 -0
- package/node_modules/nebula-autocomplete/dist/index.d.ts +7 -0
- package/node_modules/nebula-autocomplete/dist/index.js +5 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.d.ts +40 -0
- package/node_modules/nebula-autocomplete/dist/server/fastify.js +52 -0
- package/node_modules/nebula-autocomplete/dist/transport.d.ts +55 -0
- package/node_modules/nebula-autocomplete/dist/transport.js +66 -0
- package/node_modules/nebula-autocomplete/dist/types.d.ts +103 -0
- package/node_modules/nebula-autocomplete/dist/types.js +1 -0
- package/node_modules/nebula-autocomplete/package.json +58 -0
- package/node_modules/nebula-autocomplete/server.d.ts +3 -0
- package/node_modules/nebula-autocomplete/server.js +3 -0
- package/package.json +18 -5
- package/scripts/postinstall.cjs +25 -5
- package/dist/assets/index-BvrHu37J.js +0 -648
- package/dist/assets/index-Dfj_xsKU.css +0 -32
- package/node-server/dist/llm/index.d.ts +0 -5
- package/node-server/dist/llm/index.js +0 -21
- package/node-server/dist/llm/llm-service.d.ts +0 -77
- package/node-server/dist/llm/llm-service.js +0 -454
- package/node-server/dist/llm/types.d.ts +0 -40
- package/node-server/dist/llm/types.js +0 -15
- package/node-server/dist/routes/llm.d.ts +0 -8
- package/node-server/dist/routes/llm.js +0 -105
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Registry for plain-text notebook format adapters.
|
|
4
|
+
*
|
|
5
|
+
* .ipynb is intentionally NOT registered here: dispatch sites check .ipynb
|
|
6
|
+
* first and fall through to the existing Jupyter JSON code path untouched.
|
|
7
|
+
*/
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.getFormatAdapter = getFormatAdapter;
|
|
43
|
+
exports.defaultKernelForPath = defaultKernelForPath;
|
|
44
|
+
exports.isTextNotebookPath = isTextNotebookPath;
|
|
45
|
+
exports.isNotebookPath = isNotebookPath;
|
|
46
|
+
const path = __importStar(require("path"));
|
|
47
|
+
const percent_1 = require("./percent");
|
|
48
|
+
const qmd_1 = require("./qmd");
|
|
49
|
+
const ADAPTERS = [percent_1.percentAdapter, qmd_1.qmdAdapter];
|
|
50
|
+
const BY_EXTENSION = new Map();
|
|
51
|
+
for (const adapter of ADAPTERS) {
|
|
52
|
+
for (const ext of adapter.extensions)
|
|
53
|
+
BY_EXTENSION.set(ext, adapter);
|
|
54
|
+
}
|
|
55
|
+
function getFormatAdapter(filePath) {
|
|
56
|
+
return BY_EXTENSION.get(path.extname(filePath).toLowerCase()) ?? null;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Default kernel for a text-notebook file that does not declare one in its
|
|
60
|
+
* header, keyed by extension. Falls back to python3 for unknown extensions.
|
|
61
|
+
* (.qmd resolves its kernel from the first code fence's language instead.)
|
|
62
|
+
*/
|
|
63
|
+
const DEFAULT_KERNEL_BY_EXTENSION = {
|
|
64
|
+
'.py': 'python3',
|
|
65
|
+
'.r': 'ir',
|
|
66
|
+
'.jl': 'julia',
|
|
67
|
+
};
|
|
68
|
+
function defaultKernelForPath(filePath) {
|
|
69
|
+
return DEFAULT_KERNEL_BY_EXTENSION[path.extname(filePath).toLowerCase()] ?? 'python3';
|
|
70
|
+
}
|
|
71
|
+
function isTextNotebookPath(filePath) {
|
|
72
|
+
return BY_EXTENSION.has(path.extname(filePath).toLowerCase());
|
|
73
|
+
}
|
|
74
|
+
/** True for any path Nebula can treat as a notebook (.ipynb or text format). */
|
|
75
|
+
function isNotebookPath(filePath) {
|
|
76
|
+
return path.extname(filePath).toLowerCase() === '.ipynb' || isTextNotebookPath(filePath);
|
|
77
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Plain-text notebook format adapters (jupytext percent .py, Quarto .qmd).
|
|
3
|
+
*
|
|
4
|
+
* These formats are "notebooks that don't serialize outputs": the in-memory
|
|
5
|
+
* model is the same NebulaCell[] the rest of the system uses; only the
|
|
6
|
+
* on-disk representation differs. Outputs, execution counts, and UI state
|
|
7
|
+
* are never written to disk for these formats — that is the point of them.
|
|
8
|
+
*
|
|
9
|
+
* .ipynb is deliberately NOT an adapter: every dispatch site checks .ipynb
|
|
10
|
+
* first and falls into the existing, unmodified Jupyter JSON code path.
|
|
11
|
+
*/
|
|
12
|
+
import { NebulaCell } from '../types';
|
|
13
|
+
export interface ParsedTextNotebook {
|
|
14
|
+
/** Cells with outputs: [], executionCount: null. Ids come from in-file
|
|
15
|
+
* markers when present, else the positional `cell-${i}` fallback (the
|
|
16
|
+
* same degraded mode legacy .ipynb files without nebula_id use). */
|
|
17
|
+
cells: NebulaCell[];
|
|
18
|
+
/** Normalized notebook metadata: kernelspec?, nebula?, plus opaque
|
|
19
|
+
* format-specific keys the serializer round-trips. */
|
|
20
|
+
metadata: Record<string, unknown>;
|
|
21
|
+
/** Kernelspec name if the file declares one, else null (caller falls back
|
|
22
|
+
* to the existing env-default resolution). */
|
|
23
|
+
kernelspecName: string | null;
|
|
24
|
+
}
|
|
25
|
+
export interface NotebookFormatAdapter {
|
|
26
|
+
name: 'percent' | 'qmd';
|
|
27
|
+
/** Lowercased extensions including the dot, e.g. ['.py']. */
|
|
28
|
+
extensions: string[];
|
|
29
|
+
capabilities: {
|
|
30
|
+
storesOutputs: false;
|
|
31
|
+
/** Whether cell ids can persist in the file for ALL cell types.
|
|
32
|
+
* percent: true. qmd: code cells only (prose has no metadata slot). */
|
|
33
|
+
storesCellIds: boolean;
|
|
34
|
+
};
|
|
35
|
+
parse(text: string): ParsedTextNotebook;
|
|
36
|
+
serialize(cells: NebulaCell[], metadata: Record<string, unknown>, kernelName?: string): string;
|
|
37
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Plain-text notebook format adapters (jupytext percent .py, Quarto .qmd).
|
|
4
|
+
*
|
|
5
|
+
* These formats are "notebooks that don't serialize outputs": the in-memory
|
|
6
|
+
* model is the same NebulaCell[] the rest of the system uses; only the
|
|
7
|
+
* on-disk representation differs. Outputs, execution counts, and UI state
|
|
8
|
+
* are never written to disk for these formats — that is the point of them.
|
|
9
|
+
*
|
|
10
|
+
* .ipynb is deliberately NOT an adapter: every dispatch site checks .ipynb
|
|
11
|
+
* first and falls into the existing, unmodified Jupyter JSON code path.
|
|
12
|
+
*/
|
|
13
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -85,7 +85,16 @@ export interface NotebookCellsResponse {
|
|
|
85
85
|
export interface SaveNotebookResult {
|
|
86
86
|
success: boolean;
|
|
87
87
|
mtime: number;
|
|
88
|
+
/** Client sent outputs-unchanged sentinels the server couldn't resolve — it must retry with a full payload. */
|
|
89
|
+
needsFull?: boolean;
|
|
88
90
|
}
|
|
91
|
+
/**
|
|
92
|
+
* Sentinel the client puts in place of a code cell's outputs when they are
|
|
93
|
+
* unchanged since its last successful save. The server re-uses the outputs
|
|
94
|
+
* already in the file (matched by nebula_id), so autosaves don't re-upload
|
|
95
|
+
* megabytes of unchanged base64 images over slow uplinks.
|
|
96
|
+
*/
|
|
97
|
+
export declare const OUTPUTS_UNCHANGED_SENTINEL = "__nebula-outputs-unchanged-v1__";
|
|
89
98
|
export interface JupyterCellMetadata {
|
|
90
99
|
nebula_id?: string;
|
|
91
100
|
scrolled?: boolean;
|
|
@@ -3,3 +3,11 @@
|
|
|
3
3
|
* Filesystem Service Types
|
|
4
4
|
*/
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.OUTPUTS_UNCHANGED_SENTINEL = void 0;
|
|
7
|
+
/**
|
|
8
|
+
* Sentinel the client puts in place of a code cell's outputs when they are
|
|
9
|
+
* unchanged since its last successful save. The server re-uses the outputs
|
|
10
|
+
* already in the file (matched by nebula_id), so autosaves don't re-upload
|
|
11
|
+
* megabytes of unchanged base64 images over slow uplinks.
|
|
12
|
+
*/
|
|
13
|
+
exports.OUTPUTS_UNCHANGED_SENTINEL = '__nebula-outputs-unchanged-v1__';
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idle auto-release for scheduler-launched client servers.
|
|
3
|
+
*
|
|
4
|
+
* Opt-in per allocation: when the compute-node client server is started with
|
|
5
|
+
* NEBULA_IDLE_EXIT_MINUTES=<n>, it watches its own kernel and terminal
|
|
6
|
+
* activity and exits its process after n idle minutes. The batch job then
|
|
7
|
+
* completes and the allocation ends naturally — no scancel plumbing, no
|
|
8
|
+
* scheduler-side state.
|
|
9
|
+
*
|
|
10
|
+
* "Idle" means all of:
|
|
11
|
+
* - no kernel session is busy (or still starting up), AND
|
|
12
|
+
* - the most recent kernel activity (execution start/finish, restart) is
|
|
13
|
+
* older than the timeout, AND
|
|
14
|
+
* - the most recent terminal (pty) activity is older than the timeout.
|
|
15
|
+
*
|
|
16
|
+
* The monitor's own start time is the initial activity baseline, so a fresh
|
|
17
|
+
* allocation gets the full timeout before anything has run on it.
|
|
18
|
+
*/
|
|
19
|
+
/**
|
|
20
|
+
* Pure idleness decision — should the client server exit now?
|
|
21
|
+
*
|
|
22
|
+
* @param nowMs current time (ms since epoch)
|
|
23
|
+
* @param lastKernelActivityMs most recent kernel activity (ms), or null if no signal
|
|
24
|
+
* @param anyKernelBusy true when any kernel session is 'busy' or 'starting'
|
|
25
|
+
* @param lastPtyActivityMs most recent terminal activity (ms), or null if no signal
|
|
26
|
+
* @param timeoutMinutes idle timeout; <= 0 disables (never exit)
|
|
27
|
+
*/
|
|
28
|
+
export declare function shouldIdleExit(nowMs: number, lastKernelActivityMs: number | null, anyKernelBusy: boolean, lastPtyActivityMs: number | null, timeoutMinutes: number): boolean;
|
|
29
|
+
export interface IdleExitMonitorOptions {
|
|
30
|
+
/** Idle minutes before self-exit (from NEBULA_IDLE_EXIT_MINUTES). */
|
|
31
|
+
timeoutMinutes: number;
|
|
32
|
+
/** Kernel-side activity snapshot (kernelService.getIdleSnapshot). */
|
|
33
|
+
getKernelSnapshot: () => {
|
|
34
|
+
anyBusy: boolean;
|
|
35
|
+
lastActivityMs: number | null;
|
|
36
|
+
};
|
|
37
|
+
/** Most recent terminal activity across pty sessions (null = none). */
|
|
38
|
+
getLastPtyActivityMs: () => number | null;
|
|
39
|
+
/** Called once when ~WARN_LEAD_MINUTES remain (re-armed if activity resumes). */
|
|
40
|
+
onWarn: (minutesLeft: number) => void;
|
|
41
|
+
/** Called once when the idle timeout is reached. */
|
|
42
|
+
onIdleExit: () => void | Promise<void>;
|
|
43
|
+
/** Check interval; default 60s. Exposed for tests. */
|
|
44
|
+
intervalMs?: number;
|
|
45
|
+
/** Activity baseline; default Date.now() (start of monitoring). */
|
|
46
|
+
startedAtMs?: number;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Start the periodic idleness check. Returns a stop function.
|
|
50
|
+
* The interval is unref()ed so it never keeps an exiting process alive.
|
|
51
|
+
*/
|
|
52
|
+
export declare function startIdleExitMonitor(opts: IdleExitMonitorOptions): () => void;
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Idle auto-release for scheduler-launched client servers.
|
|
4
|
+
*
|
|
5
|
+
* Opt-in per allocation: when the compute-node client server is started with
|
|
6
|
+
* NEBULA_IDLE_EXIT_MINUTES=<n>, it watches its own kernel and terminal
|
|
7
|
+
* activity and exits its process after n idle minutes. The batch job then
|
|
8
|
+
* completes and the allocation ends naturally — no scancel plumbing, no
|
|
9
|
+
* scheduler-side state.
|
|
10
|
+
*
|
|
11
|
+
* "Idle" means all of:
|
|
12
|
+
* - no kernel session is busy (or still starting up), AND
|
|
13
|
+
* - the most recent kernel activity (execution start/finish, restart) is
|
|
14
|
+
* older than the timeout, AND
|
|
15
|
+
* - the most recent terminal (pty) activity is older than the timeout.
|
|
16
|
+
*
|
|
17
|
+
* The monitor's own start time is the initial activity baseline, so a fresh
|
|
18
|
+
* allocation gets the full timeout before anything has run on it.
|
|
19
|
+
*/
|
|
20
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
21
|
+
exports.shouldIdleExit = shouldIdleExit;
|
|
22
|
+
exports.startIdleExitMonitor = startIdleExitMonitor;
|
|
23
|
+
/** How far before the exit we log the "run anything to keep it" warning. */
|
|
24
|
+
const WARN_LEAD_MINUTES = 5;
|
|
25
|
+
/**
|
|
26
|
+
* Pure idleness decision — should the client server exit now?
|
|
27
|
+
*
|
|
28
|
+
* @param nowMs current time (ms since epoch)
|
|
29
|
+
* @param lastKernelActivityMs most recent kernel activity (ms), or null if no signal
|
|
30
|
+
* @param anyKernelBusy true when any kernel session is 'busy' or 'starting'
|
|
31
|
+
* @param lastPtyActivityMs most recent terminal activity (ms), or null if no signal
|
|
32
|
+
* @param timeoutMinutes idle timeout; <= 0 disables (never exit)
|
|
33
|
+
*/
|
|
34
|
+
function shouldIdleExit(nowMs, lastKernelActivityMs, anyKernelBusy, lastPtyActivityMs, timeoutMinutes) {
|
|
35
|
+
if (!Number.isFinite(timeoutMinutes) || timeoutMinutes <= 0)
|
|
36
|
+
return false;
|
|
37
|
+
if (anyKernelBusy)
|
|
38
|
+
return false;
|
|
39
|
+
const last = Math.max(lastKernelActivityMs ?? 0, lastPtyActivityMs ?? 0);
|
|
40
|
+
if (last <= 0)
|
|
41
|
+
return false; // no baseline at all — never guess
|
|
42
|
+
return nowMs - last >= timeoutMinutes * 60_000;
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Start the periodic idleness check. Returns a stop function.
|
|
46
|
+
* The interval is unref()ed so it never keeps an exiting process alive.
|
|
47
|
+
*/
|
|
48
|
+
function startIdleExitMonitor(opts) {
|
|
49
|
+
const intervalMs = opts.intervalMs ?? 60_000;
|
|
50
|
+
const startedAt = opts.startedAtMs ?? Date.now();
|
|
51
|
+
let warned = false;
|
|
52
|
+
let exiting = false;
|
|
53
|
+
const tick = () => {
|
|
54
|
+
if (exiting)
|
|
55
|
+
return;
|
|
56
|
+
const now = Date.now();
|
|
57
|
+
const kernel = opts.getKernelSnapshot();
|
|
58
|
+
// The monitor start counts as activity so a fresh, never-used allocation
|
|
59
|
+
// still gets the full timeout before releasing itself.
|
|
60
|
+
const lastKernel = Math.max(kernel.lastActivityMs ?? 0, startedAt);
|
|
61
|
+
const lastPty = opts.getLastPtyActivityMs();
|
|
62
|
+
if (shouldIdleExit(now, lastKernel, kernel.anyBusy, lastPty, opts.timeoutMinutes)) {
|
|
63
|
+
exiting = true;
|
|
64
|
+
clearInterval(timer);
|
|
65
|
+
void opts.onIdleExit();
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
// Warn once when within WARN_LEAD_MINUTES of the cutoff; re-arm on activity.
|
|
69
|
+
const last = Math.max(lastKernel, lastPty ?? 0);
|
|
70
|
+
const idleMinutes = (now - last) / 60_000;
|
|
71
|
+
const warnAt = opts.timeoutMinutes - WARN_LEAD_MINUTES;
|
|
72
|
+
if (kernel.anyBusy || idleMinutes < warnAt) {
|
|
73
|
+
warned = false;
|
|
74
|
+
}
|
|
75
|
+
else if (!warned && warnAt > 0) {
|
|
76
|
+
warned = true;
|
|
77
|
+
opts.onWarn(Math.max(1, Math.round(opts.timeoutMinutes - idleMinutes)));
|
|
78
|
+
}
|
|
79
|
+
};
|
|
80
|
+
const timer = setInterval(tick, intervalMs);
|
|
81
|
+
timer.unref?.();
|
|
82
|
+
return () => clearInterval(timer);
|
|
83
|
+
}
|
|
@@ -59,6 +59,7 @@ const fs = __importStar(require("fs"));
|
|
|
59
59
|
const os = __importStar(require("os"));
|
|
60
60
|
const crypto = __importStar(require("crypto"));
|
|
61
61
|
const crypto_1 = require("crypto");
|
|
62
|
+
const zlib = __importStar(require("zlib"));
|
|
62
63
|
// Import routes
|
|
63
64
|
const kernel_1 = __importStar(require("./routes/kernel"));
|
|
64
65
|
const fs_1 = __importDefault(require("./routes/fs"));
|
|
@@ -67,17 +68,28 @@ const python_1 = __importDefault(require("./routes/python"));
|
|
|
67
68
|
const auth_1 = __importDefault(require("./routes/auth"));
|
|
68
69
|
const cluster_1 = __importDefault(require("./routes/cluster"));
|
|
69
70
|
const resources_1 = __importDefault(require("./routes/resources"));
|
|
71
|
+
const compute_1 = __importDefault(require("./routes/compute"));
|
|
72
|
+
const autocomplete_1 = __importDefault(require("./routes/autocomplete"));
|
|
70
73
|
// Import cluster
|
|
71
74
|
const server_registry_1 = require("./cluster/server-registry");
|
|
72
75
|
const cluster_secret_1 = require("./cluster/cluster-secret");
|
|
73
76
|
const client_registration_1 = require("./cluster/client-registration");
|
|
77
|
+
// Import scheduler (SLURM-backed compute allocations)
|
|
78
|
+
const allocation_service_1 = require("./scheduler/allocation-service");
|
|
79
|
+
const slurm_scheduler_1 = require("./scheduler/slurm-scheduler");
|
|
80
|
+
const mock_scheduler_1 = require("./scheduler/mock-scheduler");
|
|
74
81
|
// Import auth
|
|
75
82
|
const auth_2 = require("./auth");
|
|
76
83
|
const fs_service_1 = require("./fs/fs-service");
|
|
84
|
+
const update_check_1 = require("./update-check");
|
|
85
|
+
const environment_1 = require("./environment");
|
|
77
86
|
// Import terminal routes (existing)
|
|
78
87
|
const server_1 = require("./terminal/server");
|
|
79
88
|
// Import notebook WebSocket
|
|
80
89
|
const notebook_websocket_1 = require("./notebook/notebook-websocket");
|
|
90
|
+
// Idle auto-release (client mode, opt-in via NEBULA_IDLE_EXIT_MINUTES)
|
|
91
|
+
const idle_exit_1 = require("./idle-exit");
|
|
92
|
+
const pty_manager_1 = require("./terminal/pty-manager");
|
|
81
93
|
const PORT = process.env.PORT || process.env.NODE_SERVER_PORT || 3000;
|
|
82
94
|
const DEV_MODE = process.env.DEV_MODE === 'true' || process.argv.includes('--dev');
|
|
83
95
|
const BODY_LIMIT = process.env.NEBULA_BODY_LIMIT ||
|
|
@@ -231,6 +243,22 @@ async function createApp() {
|
|
|
231
243
|
});
|
|
232
244
|
console.log('[Server] HTTP/1.1 mode (no TLS)');
|
|
233
245
|
}
|
|
246
|
+
// Gzipped JSON uploads (Content-Type: application/gzip). The client
|
|
247
|
+
// compresses large bodies (notebook autosaves) so slow-upload tunnels
|
|
248
|
+
// aren't saturated for seconds per save; bodyLimit applies to the
|
|
249
|
+
// COMPRESSED size, which is the wire cost we care about.
|
|
250
|
+
fastify.addContentTypeParser('application/gzip', { parseAs: 'buffer' }, (_req, body, done) => {
|
|
251
|
+
zlib.gunzip(body, (err, buf) => {
|
|
252
|
+
if (err)
|
|
253
|
+
return done(err);
|
|
254
|
+
try {
|
|
255
|
+
done(null, JSON.parse(buf.toString('utf-8')));
|
|
256
|
+
}
|
|
257
|
+
catch (e) {
|
|
258
|
+
done(e);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
});
|
|
234
262
|
// Request timing hook
|
|
235
263
|
fastify.addHook('onResponse', (request, reply, done) => {
|
|
236
264
|
const duration = reply.elapsedTime;
|
|
@@ -239,9 +267,46 @@ async function createApp() {
|
|
|
239
267
|
}
|
|
240
268
|
done();
|
|
241
269
|
});
|
|
242
|
-
//
|
|
270
|
+
// Event-loop stall detector: a 100ms heartbeat that logs when it fires late.
|
|
271
|
+
// Any [perf] line here means the WHOLE server (terminal WS forwarding
|
|
272
|
+
// included) was frozen that long by synchronous work — the prime suspect
|
|
273
|
+
// whenever "everything hangs for a moment". Correlate timestamps with
|
|
274
|
+
// [Slow Request] lines to find the culprit route.
|
|
275
|
+
{
|
|
276
|
+
let last = process.hrtime.bigint();
|
|
277
|
+
const timer = setInterval(() => {
|
|
278
|
+
const now = process.hrtime.bigint();
|
|
279
|
+
const stallMs = Number(now - last) / 1e6 - 100;
|
|
280
|
+
last = now;
|
|
281
|
+
if (stallMs > 150) {
|
|
282
|
+
console.warn(`[perf] event-loop stall ~${Math.round(stallMs)}ms at ${new Date().toISOString()}`);
|
|
283
|
+
}
|
|
284
|
+
}, 100);
|
|
285
|
+
timer.unref(); // never keep the process alive for this
|
|
286
|
+
}
|
|
287
|
+
// Register CORS. Allowlist instead of '*': cross-origin BROWSER pages lose
|
|
288
|
+
// API access (drive-by protection), while nothing we ship is affected —
|
|
289
|
+
// the terminal/agent/MCP are non-browser clients (no Origin header, CORS
|
|
290
|
+
// doesn't apply), the served UI is same-origin, ssh port-forwards land on
|
|
291
|
+
// localhost, and vite dev is a localhost origin. Extra origins for exotic
|
|
292
|
+
// serving setups: NEBULA_ALLOWED_ORIGINS=https://a.com,https://b.com
|
|
293
|
+
const extraOrigins = (process.env.NEBULA_ALLOWED_ORIGINS || '')
|
|
294
|
+
.split(',').map((s) => s.trim()).filter(Boolean);
|
|
243
295
|
await fastify.register(cors_1.default, {
|
|
244
|
-
origin:
|
|
296
|
+
origin: (origin, cb) => {
|
|
297
|
+
if (!origin)
|
|
298
|
+
return cb(null, true); // non-browser client or same-origin
|
|
299
|
+
if (extraOrigins.includes(origin))
|
|
300
|
+
return cb(null, true);
|
|
301
|
+
try {
|
|
302
|
+
const { hostname } = new URL(origin);
|
|
303
|
+
if (hostname === 'localhost' || hostname === '::1' || hostname === '127.0.0.1' || hostname.startsWith('127.')) {
|
|
304
|
+
return cb(null, true);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
catch { /* malformed Origin — treat as disallowed */ }
|
|
308
|
+
cb(null, false); // no CORS headers -> browser blocks the cross-origin page
|
|
309
|
+
},
|
|
245
310
|
credentials: true,
|
|
246
311
|
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
|
|
247
312
|
allowedHeaders: ['Content-Type', 'Authorization', 'X-API-Key', 'X-API-Provider'],
|
|
@@ -271,11 +336,14 @@ async function createApp() {
|
|
|
271
336
|
fastify.get('/api/health', async (_request, reply) => {
|
|
272
337
|
// Use configured root directory (from .nebula-config.json or fallback to cwd)
|
|
273
338
|
const rootDir = fs_service_1.fsService.normalizePath('~');
|
|
339
|
+
const update = (0, update_check_1.getUpdateInfo)();
|
|
274
340
|
return reply.send({
|
|
275
341
|
status: 'ok',
|
|
276
|
-
version:
|
|
342
|
+
version: update.current,
|
|
277
343
|
ready: kernel_1.kernelService.isReady,
|
|
278
344
|
cwd: rootDir,
|
|
345
|
+
update,
|
|
346
|
+
environment: (0, environment_1.getEnvironment)(),
|
|
279
347
|
});
|
|
280
348
|
});
|
|
281
349
|
fastify.get('/api/ready', async (_request, reply) => {
|
|
@@ -307,10 +375,12 @@ async function createApp() {
|
|
|
307
375
|
});
|
|
308
376
|
// API routes (protected)
|
|
309
377
|
await fastify.register(kernel_1.default, { prefix: '/api' });
|
|
378
|
+
await fastify.register(autocomplete_1.default, { prefix: '/api' });
|
|
310
379
|
await fastify.register(fs_1.default, { prefix: '/api' });
|
|
311
380
|
await fastify.register(notebook_1.default, { prefix: '/api' });
|
|
312
381
|
await fastify.register(python_1.default, { prefix: '/api' });
|
|
313
382
|
await fastify.register(cluster_1.default, { prefix: '/api' });
|
|
383
|
+
await fastify.register(compute_1.default, { prefix: '/api' });
|
|
314
384
|
await fastify.register(resources_1.default, { prefix: '/api/resources' });
|
|
315
385
|
// Terminal routes (registered directly on the app, not under /api prefix)
|
|
316
386
|
await fastify.register(server_1.setupTerminalRoutes);
|
|
@@ -446,8 +516,9 @@ async function main() {
|
|
|
446
516
|
else {
|
|
447
517
|
console.log('[Auth] 2FA configured and ready');
|
|
448
518
|
}
|
|
449
|
-
// Set local server ID from hostname
|
|
450
|
-
|
|
519
|
+
// Set local server ID from hostname (NEBULA_HOST overrides the advertised name
|
|
520
|
+
// when the OS hostname isn't the reachable/desired identity — e.g. demos).
|
|
521
|
+
const localServerId = `${process.env.NEBULA_HOST || os.hostname()}:${PORT}`;
|
|
451
522
|
server_registry_1.serverRegistry.setLocalServerId(localServerId);
|
|
452
523
|
console.log(`[Cluster] Local server ID: ${localServerId}`);
|
|
453
524
|
const serverInstanceId = (0, crypto_1.randomUUID)();
|
|
@@ -468,6 +539,35 @@ async function main() {
|
|
|
468
539
|
(0, server_1.setupTerminalWebSocket)(server);
|
|
469
540
|
// Setup notebook operations WebSocket
|
|
470
541
|
(0, notebook_websocket_1.setupNotebookWebSocket)(server);
|
|
542
|
+
// Scheduler-backed compute allocations (main server only; detection-gated)
|
|
543
|
+
let schedulerDetected = false;
|
|
544
|
+
if (!CLIENT_MODE) {
|
|
545
|
+
try {
|
|
546
|
+
// NEBULA_SCHEDULER=mock enables a fabricated-data scheduler (no real cluster)
|
|
547
|
+
// for demos/tests; otherwise use the real SLURM scheduler (detection-gated).
|
|
548
|
+
const scheduler = process.env.NEBULA_SCHEDULER === 'mock' ? new mock_scheduler_1.MockScheduler() : new slurm_scheduler_1.SlurmScheduler();
|
|
549
|
+
if (await scheduler.detect()) {
|
|
550
|
+
schedulerDetected = true;
|
|
551
|
+
const ctx = {
|
|
552
|
+
mainUrl: process.env.NEBULA_SCHEDULER_MAIN_URL || `http://${os.hostname()}:${PORT}`,
|
|
553
|
+
secret: process.env.NEBULA_CLUSTER_SECRET,
|
|
554
|
+
nodeBin: process.execPath,
|
|
555
|
+
execArgv: process.execArgv,
|
|
556
|
+
scriptPath: process.argv[1],
|
|
557
|
+
cwd: process.cwd(),
|
|
558
|
+
stateDir: process.env.NEBULA_SCHEDULER_STATE_DIR || path.join(os.homedir(), '.nebula', 'allocations'),
|
|
559
|
+
};
|
|
560
|
+
allocation_service_1.allocationService.init(scheduler, ctx);
|
|
561
|
+
console.log('[Scheduler] SLURM detected — compute allocations enabled');
|
|
562
|
+
}
|
|
563
|
+
else {
|
|
564
|
+
console.log('[Scheduler] No batch scheduler detected — compute allocations disabled');
|
|
565
|
+
}
|
|
566
|
+
}
|
|
567
|
+
catch (err) {
|
|
568
|
+
console.warn('[Scheduler] Init failed:', err instanceof Error ? err.message : String(err));
|
|
569
|
+
}
|
|
570
|
+
}
|
|
471
571
|
if (REATTACH_KERNELS) {
|
|
472
572
|
try {
|
|
473
573
|
const result = await kernel_1.kernelService.reattachOrphanedSessions();
|
|
@@ -480,6 +580,41 @@ async function main() {
|
|
|
480
580
|
console.warn(`[Kernel] Reattach failed: ${message}`);
|
|
481
581
|
}
|
|
482
582
|
}
|
|
583
|
+
// Final "you're up" banner — the one part of startup written for humans.
|
|
584
|
+
// Everything a first-time user needs: where to point the browser, where the
|
|
585
|
+
// files live, whether cluster compute is on, and how to reach a remote
|
|
586
|
+
// server from a laptop. Client-mode servers skip it (no user is watching).
|
|
587
|
+
if (!CLIENT_MODE) {
|
|
588
|
+
const host = os.hostname();
|
|
589
|
+
let rootDir = '~';
|
|
590
|
+
try {
|
|
591
|
+
rootDir = fs_service_1.fsService.getRootDirectory();
|
|
592
|
+
}
|
|
593
|
+
catch { /* keep default */ }
|
|
594
|
+
const lines = [
|
|
595
|
+
'',
|
|
596
|
+
' ──────────────────────────────────────────────────────',
|
|
597
|
+
' Nebula Notebook is running',
|
|
598
|
+
'',
|
|
599
|
+
` Open: http://localhost:${PORT}`,
|
|
600
|
+
` Network: http://${host}:${PORT}`,
|
|
601
|
+
` Files: ${rootDir}`,
|
|
602
|
+
` Compute: ${schedulerDetected
|
|
603
|
+
? 'SLURM detected — allocate compute nodes from the kernel menu'
|
|
604
|
+
: 'no batch scheduler here — kernels run on this machine'}`,
|
|
605
|
+
...(setupNeeded ? [' Sign-in: scan the QR code above with an authenticator app'] : []),
|
|
606
|
+
'',
|
|
607
|
+
' On a remote machine? From your laptop:',
|
|
608
|
+
` ssh -L ${PORT}:localhost:${PORT} ${host} then open http://localhost:${PORT}`,
|
|
609
|
+
' Cluster guide: https://github.com/jzhoulab/nebula-notebook/blob/main/docs/CLUSTER_SETUP.md',
|
|
610
|
+
' ──────────────────────────────────────────────────────',
|
|
611
|
+
'',
|
|
612
|
+
];
|
|
613
|
+
console.log(lines.join('\n'));
|
|
614
|
+
}
|
|
615
|
+
// Daily notify-only update check (NEBULA_NO_UPDATE_CHECK=1 to disable)
|
|
616
|
+
if (!CLIENT_MODE)
|
|
617
|
+
(0, update_check_1.startUpdateChecker)();
|
|
483
618
|
// Graceful shutdown
|
|
484
619
|
let shuttingDown = false;
|
|
485
620
|
const shutdown = async () => {
|
|
@@ -507,10 +642,11 @@ async function main() {
|
|
|
507
642
|
catch (err) {
|
|
508
643
|
console.error('[Server] Error during kernel cleanup:', err);
|
|
509
644
|
}
|
|
510
|
-
// Cleanup cluster registration
|
|
645
|
+
// Cleanup cluster registration + scheduler polling
|
|
511
646
|
try {
|
|
512
647
|
await client_registration_1.clientRegistration.shutdown();
|
|
513
648
|
server_registry_1.serverRegistry.shutdown();
|
|
649
|
+
allocation_service_1.allocationService.shutdown();
|
|
514
650
|
}
|
|
515
651
|
catch (err) {
|
|
516
652
|
console.error('[Server] Error during cluster cleanup:', err);
|
|
@@ -541,12 +677,66 @@ async function main() {
|
|
|
541
677
|
console.log(`[Server] Notebook WebSocket: ${wsProtocol}://localhost:${PORT}/api/notebook/{path}/ws`);
|
|
542
678
|
console.log(`[Server] Terminal WebSocket: ${wsProtocol}://localhost:${PORT}/ws?id={terminal_id}`);
|
|
543
679
|
console.log(`[Server] Root directory: ${fs_service_1.fsService.getRootDirectory()} (change with --workdir)`);
|
|
544
|
-
// Initialize client registration (explicit client mode only)
|
|
680
|
+
// Initialize client registration (explicit client mode only). With PORT=0
|
|
681
|
+
// (scheduler-launched jobs pick an ephemeral port), register the *actual* bound
|
|
682
|
+
// port so the main server proxies kernels to the right place.
|
|
545
683
|
if (CLIENT_MODE) {
|
|
546
|
-
|
|
684
|
+
const addr = fastify.server.address();
|
|
685
|
+
const boundPort = addr && typeof addr === 'object' && addr ? addr.port : Number(PORT);
|
|
686
|
+
const clientServerId = `${os.hostname()}:${boundPort}`;
|
|
687
|
+
server_registry_1.serverRegistry.setLocalServerId(clientServerId);
|
|
688
|
+
kernel_1.kernelService.setServerIdentity(clientServerId, serverInstanceId);
|
|
689
|
+
void client_registration_1.clientRegistration.initFromEnv(boundPort); // async: resolves the controller-routable IP first
|
|
690
|
+
// Opt-in idle auto-release: exit this process after N idle minutes so the
|
|
691
|
+
// batch job completes and the allocation ends naturally (no scancel needed).
|
|
692
|
+
const idleExitMinutes = Math.floor(Number(process.env.NEBULA_IDLE_EXIT_MINUTES) || 0);
|
|
693
|
+
if (idleExitMinutes > 0) {
|
|
694
|
+
console.log(`[IdleExit] Enabled — allocation auto-ends after ${idleExitMinutes} idle minutes`);
|
|
695
|
+
(0, idle_exit_1.startIdleExitMonitor)({
|
|
696
|
+
timeoutMinutes: idleExitMinutes,
|
|
697
|
+
getKernelSnapshot: () => kernel_1.kernelService.getIdleSnapshot(),
|
|
698
|
+
getLastPtyActivityMs: () => {
|
|
699
|
+
const terminals = pty_manager_1.ptyManager.list();
|
|
700
|
+
if (terminals.length === 0)
|
|
701
|
+
return null;
|
|
702
|
+
return Math.max(...terminals.map((t) => t.lastActivity));
|
|
703
|
+
},
|
|
704
|
+
onWarn: (minutesLeft) => {
|
|
705
|
+
console.warn(`[IdleExit] Idle — will end allocation in ${minutesLeft}m; run anything to keep it`);
|
|
706
|
+
},
|
|
707
|
+
onIdleExit: async () => {
|
|
708
|
+
console.log(`[IdleExit] Idle for ${idleExitMinutes} minutes — releasing allocation (exiting)`);
|
|
709
|
+
// Shut kernels down cleanly WITHOUT preservation so nothing tries to
|
|
710
|
+
// resurrect them, then drop the cluster registration and exit 0 —
|
|
711
|
+
// the batch job completes and the allocation ends.
|
|
712
|
+
(0, server_1.cleanupTerminals)();
|
|
713
|
+
try {
|
|
714
|
+
await kernel_1.kernelService.shutdown({ preserveKernels: false });
|
|
715
|
+
}
|
|
716
|
+
catch (err) {
|
|
717
|
+
console.error('[IdleExit] Error during kernel cleanup:', err);
|
|
718
|
+
}
|
|
719
|
+
try {
|
|
720
|
+
await client_registration_1.clientRegistration.shutdown();
|
|
721
|
+
}
|
|
722
|
+
catch { /* best effort — we are exiting anyway */ }
|
|
723
|
+
console.log('[IdleExit] Allocation released — exiting');
|
|
724
|
+
process.exit(0);
|
|
725
|
+
},
|
|
726
|
+
});
|
|
727
|
+
}
|
|
547
728
|
}
|
|
548
729
|
}
|
|
549
730
|
// Run
|
|
731
|
+
// A single stray rejection must not take down every kernel, terminal, and
|
|
732
|
+
// SLURM allocation this process is holding. Log loudly and keep serving —
|
|
733
|
+
// for a state-holding server, dying is strictly worse than degrading.
|
|
734
|
+
process.on('unhandledRejection', (reason) => {
|
|
735
|
+
console.error('[fatal-ish] Unhandled promise rejection (continuing):', reason);
|
|
736
|
+
});
|
|
737
|
+
process.on('uncaughtException', (err) => {
|
|
738
|
+
console.error('[fatal-ish] Uncaught exception (continuing):', err);
|
|
739
|
+
});
|
|
550
740
|
main().catch((err) => {
|
|
551
741
|
console.error('[Server] Fatal error:', err);
|
|
552
742
|
process.exit(1);
|