@damurka/jovian 0.1.0
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/LICENSE +24 -0
- package/README.md +175 -0
- package/docs/api/README.md +55 -0
- package/docs/api/session.md +143 -0
- package/docs/api/types.md +120 -0
- package/docs/architecture/overview.md +218 -0
- package/docs/cpp-usage.md +60 -0
- package/docs/development.md +105 -0
- package/docs/getting-started.md +127 -0
- package/docs/guides/comms.md +70 -0
- package/docs/guides/environments.md +80 -0
- package/docs/guides/history.md +44 -0
- package/docs/guides/interactive-input.md +48 -0
- package/docs/guides/interrupting.md +45 -0
- package/docs/guides/playground.md +33 -0
- package/docs/guides/sessions-lifecycle.md +70 -0
- package/docs/kernels.md +117 -0
- package/docs/protocol.md +135 -0
- package/docs/releasing.md +73 -0
- package/docs/troubleshooting.md +110 -0
- package/lib/execution/execution-queue.d.ts +20 -0
- package/lib/execution/execution-queue.js +256 -0
- package/lib/handlers/display-handler.d.ts +7 -0
- package/lib/handlers/display-handler.js +10 -0
- package/lib/handlers/error-handler.d.ts +7 -0
- package/lib/handlers/error-handler.js +8 -0
- package/lib/handlers/result-handler.d.ts +7 -0
- package/lib/handlers/result-handler.js +10 -0
- package/lib/handlers/stream-handler.d.ts +7 -0
- package/lib/handlers/stream-handler.js +8 -0
- package/lib/index.d.ts +7 -0
- package/lib/index.js +5 -0
- package/lib/messaging/message-parser.d.ts +6 -0
- package/lib/messaging/message-parser.js +33 -0
- package/lib/messaging/message-router.d.ts +14 -0
- package/lib/messaging/message-router.js +41 -0
- package/lib/middleware/index.d.ts +5 -0
- package/lib/middleware/index.js +5 -0
- package/lib/middleware/middleware-chain.d.ts +7 -0
- package/lib/middleware/middleware-chain.js +14 -0
- package/lib/middleware/middleware.d.ts +5 -0
- package/lib/middleware/middleware.js +2 -0
- package/lib/middleware/plugins/logging-plugin.d.ts +6 -0
- package/lib/middleware/plugins/logging-plugin.js +9 -0
- package/lib/middleware/plugins/metrics-plugin.d.ts +8 -0
- package/lib/middleware/plugins/metrics-plugin.js +13 -0
- package/lib/session/comm.d.ts +39 -0
- package/lib/session/comm.js +58 -0
- package/lib/session/native-paths.d.ts +48 -0
- package/lib/session/native-paths.js +108 -0
- package/lib/session/session-manager.d.ts +229 -0
- package/lib/session/session-manager.js +842 -0
- package/lib/session/supervisor-client.d.ts +36 -0
- package/lib/session/supervisor-client.js +147 -0
- package/lib/types/engine.d.ts +269 -0
- package/lib/types/engine.js +2 -0
- package/lib/types/index.d.ts +3 -0
- package/lib/types/index.js +3 -0
- package/lib/types/messages.d.ts +68 -0
- package/lib/types/messages.js +2 -0
- package/lib/utils/logger.d.ts +12 -0
- package/lib/utils/logger.js +58 -0
- package/lib/utils/network.d.ts +11 -0
- package/lib/utils/network.js +50 -0
- package/package.json +57 -0
- package/packages/hera/DESCRIPTION +29 -0
- package/packages/hera/LICENSE +2 -0
- package/packages/hera/LICENSE.md +21 -0
- package/packages/hera/NAMESPACE +32 -0
- package/packages/hera/NEWS.md +7 -0
- package/packages/hera/R/cell_options.R +13 -0
- package/packages/hera/R/comm.R +228 -0
- package/packages/hera/R/completion.R +54 -0
- package/packages/hera/R/execute.R +199 -0
- package/packages/hera/R/inspect.R +73 -0
- package/packages/hera/R/log.R +14 -0
- package/packages/hera/R/mime_bundle.R +65 -0
- package/packages/hera/R/routines.R +86 -0
- package/packages/hera/R/utils.R +32 -0
- package/packages/hera/R/zzz.R +128 -0
- package/packages/hera/man/Comm.Rd +179 -0
- package/packages/hera/man/CommManager.Rd +215 -0
- package/packages/hera/man/View.Rd +22 -0
- package/packages/hera/man/cell_options.Rd +20 -0
- package/packages/hera/man/clear_output.Rd +23 -0
- package/packages/hera/man/complete.Rd +23 -0
- package/packages/hera/man/display_data.Rd +22 -0
- package/packages/hera/man/is_elara.Rd +18 -0
- package/packages/hera/man/mime_bundle.Rd +25 -0
- package/packages/hera/man/mime_types.Rd +22 -0
- package/packages/hera/man/reexports.Rd +16 -0
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { EngineOptions } from '../types/index.js';
|
|
2
|
+
import { Logger } from '../utils/logger.js';
|
|
3
|
+
export interface SessionConnectionInfo {
|
|
4
|
+
sessionId: string;
|
|
5
|
+
httpBase: string;
|
|
6
|
+
wsBase: string;
|
|
7
|
+
}
|
|
8
|
+
export declare function buildSessionOptionsBody(options: Partial<EngineOptions>): Record<string, unknown>;
|
|
9
|
+
export declare class SupervisorClient {
|
|
10
|
+
private child;
|
|
11
|
+
private readyPromise;
|
|
12
|
+
private readonly logger;
|
|
13
|
+
constructor(logger: Logger);
|
|
14
|
+
private ensureStarted;
|
|
15
|
+
private spawnSupervisor;
|
|
16
|
+
createSession(options: EngineOptions): Promise<SessionConnectionInfo>;
|
|
17
|
+
stopSession(info: SessionConnectionInfo): Promise<void>;
|
|
18
|
+
/**
|
|
19
|
+
* Replaces a session's kernel process in place (SessionRegistry::
|
|
20
|
+
* restartSession() on the native side stops the old kernel, spawns a
|
|
21
|
+
* fresh one, and re-registers it under the *same* session id) -- so
|
|
22
|
+
* unlike stopSession(), nothing about `info` changes here. Works both
|
|
23
|
+
* to recover a crashed session and, same as Jupyter's "Restart Kernel",
|
|
24
|
+
* to reset a healthy one.
|
|
25
|
+
*
|
|
26
|
+
* `options`, if given, replaces the R installation this session's next
|
|
27
|
+
* kernel process launches with (rHome/rPath/etc) instead of reusing
|
|
28
|
+
* whatever it was created with -- e.g. switching from R 4.4 to R 4.6
|
|
29
|
+
* for an existing notebook connection on the fly, without needing to
|
|
30
|
+
* close this session and create a new one just to pick a different R.
|
|
31
|
+
*/
|
|
32
|
+
restartSession(info: SessionConnectionInfo, options?: Partial<EngineOptions>): Promise<void>;
|
|
33
|
+
/** Skips graceful per-session shutdown -- only for cleanup on the way out. */
|
|
34
|
+
kill(): void;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=supervisor-client.d.ts.map
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
import { spawn } from 'child_process';
|
|
2
|
+
import { createInterface } from 'readline';
|
|
3
|
+
import { join } from 'path';
|
|
4
|
+
import { bundledHeraSource, ensureExecutable, locateNativeDirectory } from './native-paths.js';
|
|
5
|
+
// Pulled out as its own pure function (rather than inlined in createSession/
|
|
6
|
+
// restartSession) so the exact shape sent to the supervisor's POST /sessions
|
|
7
|
+
// and .../restart bodies is unit-testable without mocking fetch() or
|
|
8
|
+
// spawning a real supervisor process. Undefined fields are dropped by
|
|
9
|
+
// JSON.stringify() (e.g. rHome for a 'python' session), so passing every
|
|
10
|
+
// field unconditionally is harmless -- session_registry.cpp's
|
|
11
|
+
// parseSessionOptions() only reads the ones matching kernelType anyway.
|
|
12
|
+
export function buildSessionOptionsBody(options) {
|
|
13
|
+
return {
|
|
14
|
+
kernelType: options.kernelType,
|
|
15
|
+
rHome: options.rHome,
|
|
16
|
+
rPath: options.rPath,
|
|
17
|
+
rLibs: options.rLibs,
|
|
18
|
+
pandocPath: options.pandocPath,
|
|
19
|
+
heraSrcPath: options.heraSrcPath ?? bundledHeraSource(),
|
|
20
|
+
pythonHome: options.pythonHome,
|
|
21
|
+
pythonPath: options.pythonPath,
|
|
22
|
+
venvPath: options.venvPath,
|
|
23
|
+
workingDirectory: options.workingDirectory
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
// themisto is the kernel supervisor: it's the only process in this system
|
|
27
|
+
// that ever links a native ZMQ
|
|
28
|
+
// binding. It spawns/owns `elara` kernel processes, speaks ZMQ to
|
|
29
|
+
// each of them, and re-exposes sessions over plain HTTP (lifecycle) +
|
|
30
|
+
// WebSocket (execute/interrupt/message streaming) -- so Electron/VS Code's
|
|
31
|
+
// process, where this class runs, never needs a native dependency at all.
|
|
32
|
+
export class SupervisorClient {
|
|
33
|
+
child;
|
|
34
|
+
readyPromise;
|
|
35
|
+
logger;
|
|
36
|
+
constructor(logger) {
|
|
37
|
+
this.logger = logger;
|
|
38
|
+
}
|
|
39
|
+
ensureStarted() {
|
|
40
|
+
if (!this.readyPromise) {
|
|
41
|
+
this.readyPromise = this.spawnSupervisor();
|
|
42
|
+
}
|
|
43
|
+
return this.readyPromise;
|
|
44
|
+
}
|
|
45
|
+
spawnSupervisor() {
|
|
46
|
+
return new Promise((resolve, reject) => {
|
|
47
|
+
const exePath = resolveSupervisorExecutable();
|
|
48
|
+
this.logger.debug(`Spawning supervisor process from: ${exePath}`);
|
|
49
|
+
const child = spawn(exePath, [], { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
50
|
+
this.child = child;
|
|
51
|
+
child.stderr?.on('data', (chunk) => process.stderr.write(chunk));
|
|
52
|
+
const rl = createInterface({ input: child.stdout });
|
|
53
|
+
const onLine = (line) => {
|
|
54
|
+
let message;
|
|
55
|
+
try {
|
|
56
|
+
message = JSON.parse(line);
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
if (message.type === 'supervisorReady' && typeof message.httpPort === 'number' && typeof message.wsPort === 'number') {
|
|
62
|
+
cleanup();
|
|
63
|
+
this.logger.info(`Supervisor ready (pid ${child.pid})`, { httpPort: message.httpPort, wsPort: message.wsPort });
|
|
64
|
+
resolve({ httpPort: message.httpPort, wsPort: message.wsPort });
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const onExit = (code) => {
|
|
68
|
+
cleanup();
|
|
69
|
+
reject(new Error(`Supervisor process exited before it was ready (code ${code})`));
|
|
70
|
+
};
|
|
71
|
+
const cleanup = () => {
|
|
72
|
+
rl.off('line', onLine);
|
|
73
|
+
child.off('exit', onExit);
|
|
74
|
+
};
|
|
75
|
+
rl.on('line', onLine);
|
|
76
|
+
child.once('error', (error) => { cleanup(); reject(error); });
|
|
77
|
+
child.once('exit', onExit);
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
async createSession(options) {
|
|
81
|
+
const { httpPort, wsPort } = await this.ensureStarted();
|
|
82
|
+
const httpBase = `http://127.0.0.1:${httpPort}`;
|
|
83
|
+
const res = await fetch(`${httpBase}/sessions`, {
|
|
84
|
+
method: 'POST',
|
|
85
|
+
headers: { 'content-type': 'application/json' },
|
|
86
|
+
body: JSON.stringify(buildSessionOptionsBody(options))
|
|
87
|
+
});
|
|
88
|
+
const body = await res.json();
|
|
89
|
+
if (!res.ok || !body.sessionId) {
|
|
90
|
+
throw new Error(body.error ?? `Supervisor failed to create session (HTTP ${res.status})`);
|
|
91
|
+
}
|
|
92
|
+
return { sessionId: body.sessionId, httpBase, wsBase: `ws://127.0.0.1:${wsPort}` };
|
|
93
|
+
}
|
|
94
|
+
async stopSession(info) {
|
|
95
|
+
try {
|
|
96
|
+
await fetch(`${info.httpBase}/sessions/${info.sessionId}`, { method: 'DELETE' });
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
this.logger.warn(`Failed to gracefully stop session ${info.sessionId} via supervisor`, error);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* Replaces a session's kernel process in place (SessionRegistry::
|
|
104
|
+
* restartSession() on the native side stops the old kernel, spawns a
|
|
105
|
+
* fresh one, and re-registers it under the *same* session id) -- so
|
|
106
|
+
* unlike stopSession(), nothing about `info` changes here. Works both
|
|
107
|
+
* to recover a crashed session and, same as Jupyter's "Restart Kernel",
|
|
108
|
+
* to reset a healthy one.
|
|
109
|
+
*
|
|
110
|
+
* `options`, if given, replaces the R installation this session's next
|
|
111
|
+
* kernel process launches with (rHome/rPath/etc) instead of reusing
|
|
112
|
+
* whatever it was created with -- e.g. switching from R 4.4 to R 4.6
|
|
113
|
+
* for an existing notebook connection on the fly, without needing to
|
|
114
|
+
* close this session and create a new one just to pick a different R.
|
|
115
|
+
*/
|
|
116
|
+
async restartSession(info, options) {
|
|
117
|
+
const res = await fetch(`${info.httpBase}/sessions/${info.sessionId}/restart`, {
|
|
118
|
+
method: 'POST',
|
|
119
|
+
...(options ? {
|
|
120
|
+
headers: { 'content-type': 'application/json' },
|
|
121
|
+
body: JSON.stringify(buildSessionOptionsBody(options))
|
|
122
|
+
} : {})
|
|
123
|
+
});
|
|
124
|
+
const body = await res.json();
|
|
125
|
+
if (!res.ok || !body.sessionId) {
|
|
126
|
+
throw new Error(body.error ?? `Supervisor failed to restart session ${info.sessionId} (HTTP ${res.status})`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/** Skips graceful per-session shutdown -- only for cleanup on the way out. */
|
|
130
|
+
kill() {
|
|
131
|
+
if (this.child && !this.child.killed) {
|
|
132
|
+
this.logger.warn(`Force-killing supervisor process (pid ${this.child.pid})`);
|
|
133
|
+
this.child.kill();
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
// Finds the supervisor binary: JOVIAN_NATIVE_DIR, else the installed
|
|
138
|
+
// @scope/jovian-<os>-<cpu> package, else a source checkout's
|
|
139
|
+
// dist/native/Release (see native-paths.ts).
|
|
140
|
+
function resolveSupervisorExecutable() {
|
|
141
|
+
const { dir } = locateNativeDirectory();
|
|
142
|
+
// Electron keeps native files outside the asar archive.
|
|
143
|
+
const nativeDir = dir.replace(/\bnode_modules\.asar\b/, 'node_modules.asar.unpacked');
|
|
144
|
+
ensureExecutable(nativeDir);
|
|
145
|
+
return join(nativeDir, process.platform === 'win32' ? 'themisto.exe' : 'themisto');
|
|
146
|
+
}
|
|
147
|
+
//# sourceMappingURL=supervisor-client.js.map
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
import type { JupyterMessage } from './messages.js';
|
|
2
|
+
export type LogLevel = 'trace' | 'debug' | 'info' | 'warn' | 'error';
|
|
3
|
+
export type LoggerFunction = (level: LogLevel, message: string, data?: any) => void;
|
|
4
|
+
export interface EngineOptions {
|
|
5
|
+
/**
|
|
6
|
+
* Which kernel a session runs: 'r' (Elara, the default -- every caller
|
|
7
|
+
* that predates this field keeps behaving exactly as before) or
|
|
8
|
+
* 'python' (Carpo). Selects which set of the fields below the
|
|
9
|
+
* supervisor actually uses (native/src/themisto/session_registry.cpp's
|
|
10
|
+
* SessionOptions::kernelType) and which kernel executable it spawns.
|
|
11
|
+
*/
|
|
12
|
+
kernelType?: 'r' | 'python';
|
|
13
|
+
rHome?: string;
|
|
14
|
+
rPath?: string;
|
|
15
|
+
rLibs?: string;
|
|
16
|
+
/** Directory containing the pandoc binary, for bundled R installs that don't ship it on PATH. */
|
|
17
|
+
pandocPath?: string;
|
|
18
|
+
/**
|
|
19
|
+
* Source directory of the 'hera' R package (packages/hera in this repo).
|
|
20
|
+
* When set, Elara installs it via remotes::install_local() if it is
|
|
21
|
+
* missing or older than these sources (needs the 'remotes' package).
|
|
22
|
+
* There is no default: without it, R sessions use whichever 'hera' is
|
|
23
|
+
* already installed in the library -- install or update it with
|
|
24
|
+
* `npm run hera:install`.
|
|
25
|
+
*/
|
|
26
|
+
heraSrcPath?: string;
|
|
27
|
+
/** Only used when kernelType is 'python' -- Carpo's equivalent of rHome. */
|
|
28
|
+
pythonHome?: string;
|
|
29
|
+
/** Only used when kernelType is 'python' -- Carpo's equivalent of rPath. */
|
|
30
|
+
pythonPath?: string;
|
|
31
|
+
/** Only used when kernelType is 'python' -- not yet consulted by Carpo itself (see carpo::EnvironmentConfig). */
|
|
32
|
+
venvPath?: string;
|
|
33
|
+
/**
|
|
34
|
+
* Directory the kernel process starts in -- what `getwd()` (R) /
|
|
35
|
+
* `os.getcwd()` (Python) report and what relative paths resolve against.
|
|
36
|
+
* Must already exist. Defaults to the supervisor's own working directory
|
|
37
|
+
* (i.e. the calling process's), which is rarely what you want for a
|
|
38
|
+
* notebook/project: set it to the project or document folder.
|
|
39
|
+
*/
|
|
40
|
+
workingDirectory?: string;
|
|
41
|
+
queueSize?: number;
|
|
42
|
+
enableLogging?: boolean;
|
|
43
|
+
enableMetrics?: boolean;
|
|
44
|
+
logger?: LoggerFunction;
|
|
45
|
+
}
|
|
46
|
+
export type EngineState = 'idle' | 'starting' | 'running' | 'stopping' | 'stopped' | 'error';
|
|
47
|
+
export interface ExecutionOptions {
|
|
48
|
+
silent?: boolean;
|
|
49
|
+
storeHistory?: boolean;
|
|
50
|
+
allowStdin?: boolean;
|
|
51
|
+
/**
|
|
52
|
+
* When this execution fails, abort every execute() still waiting behind
|
|
53
|
+
* it in the queue instead of running them (Jupyter's stop_on_error) --
|
|
54
|
+
* their results come back with `aborted: true` and nothing having run.
|
|
55
|
+
* Also forwarded to the kernel in the execute_request itself.
|
|
56
|
+
*/
|
|
57
|
+
stopOnError?: boolean;
|
|
58
|
+
/**
|
|
59
|
+
* Expressions to evaluate in the kernel right after the code runs, as
|
|
60
|
+
* {name: expression} (Jupyter's user_expressions). Only evaluated when
|
|
61
|
+
* the code succeeded; each result -- or its own error -- comes back in
|
|
62
|
+
* `ExecutionResult.userExpressions` under the same name.
|
|
63
|
+
*/
|
|
64
|
+
userExpressions?: Record<string, string>;
|
|
65
|
+
/**
|
|
66
|
+
* Milliseconds to wait for the execution to finish before giving up
|
|
67
|
+
* (default 30000; 0 = no timeout, for calls meant to run indefinitely
|
|
68
|
+
* such as a Shiny app).
|
|
69
|
+
*/
|
|
70
|
+
timeout?: number;
|
|
71
|
+
/**
|
|
72
|
+
* When the timeout fires, also send the kernel an interrupt (default
|
|
73
|
+
* true) so it stops the code instead of carrying on with work no one is
|
|
74
|
+
* waiting for -- which would otherwise block everything queued behind it.
|
|
75
|
+
* Set false to leave the kernel running after a timeout.
|
|
76
|
+
*/
|
|
77
|
+
interruptOnTimeout?: boolean;
|
|
78
|
+
}
|
|
79
|
+
/** One evaluated user expression: its rich value, or the error evaluating it raised. */
|
|
80
|
+
export type UserExpressionResult = {
|
|
81
|
+
status: 'ok';
|
|
82
|
+
data: Record<string, any>;
|
|
83
|
+
metadata: Record<string, any>;
|
|
84
|
+
} | {
|
|
85
|
+
status: 'error';
|
|
86
|
+
ename: string;
|
|
87
|
+
evalue: string;
|
|
88
|
+
traceback: string[];
|
|
89
|
+
};
|
|
90
|
+
export interface ExecutionResult {
|
|
91
|
+
success: boolean;
|
|
92
|
+
output: JupyterMessage[];
|
|
93
|
+
error?: Error;
|
|
94
|
+
executionCount?: number;
|
|
95
|
+
/** The execute_reply's own status; 'aborted' means it never ran (see ExecutionOptions.stopOnError). */
|
|
96
|
+
status?: 'ok' | 'error' | 'aborted';
|
|
97
|
+
/** True when this execution was skipped because an earlier one failed with stopOnError. */
|
|
98
|
+
aborted?: boolean;
|
|
99
|
+
/** Results of ExecutionOptions.userExpressions, by name. */
|
|
100
|
+
userExpressions?: Record<string, UserExpressionResult>;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* One past execute() call's full record: the code that ran, its kernel-
|
|
104
|
+
* reported execution count (if the reply included one -- a silent execution
|
|
105
|
+
* never gets one), and every iopub message it produced (stream/
|
|
106
|
+
* execute_result/display_data/error/status/...), in arrival order. Kept by
|
|
107
|
+
* Session.getHistory() for the life of that Session object -- purely
|
|
108
|
+
* in-memory, gone once the Session (or its owning process) does, same as
|
|
109
|
+
* everything else client-side. See Session.getHistory()'s own doc comment
|
|
110
|
+
* for why this exists and how it differs from queryKernelHistory().
|
|
111
|
+
*/
|
|
112
|
+
export interface ExecutionHistoryEntry {
|
|
113
|
+
code: string;
|
|
114
|
+
executionCount?: number;
|
|
115
|
+
time: number;
|
|
116
|
+
messages: JupyterMessage[];
|
|
117
|
+
/**
|
|
118
|
+
* True when this execution printed so much that the oldest stream
|
|
119
|
+
* (stdout/stderr) messages were dropped to bound memory: the entry keeps
|
|
120
|
+
* the most recent ~500 KB of stream text and everything else (results,
|
|
121
|
+
* display data, errors) in full.
|
|
122
|
+
*/
|
|
123
|
+
truncated?: boolean;
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Options for Session.queryKernelHistory(), mirroring Jupyter's real
|
|
127
|
+
* history_request wire message (KernelCore::historyRequest() ->
|
|
128
|
+
* HistoryManager::processRequest(), native/src/adrastea/core/history/) --
|
|
129
|
+
* see that function's own field reads for exactly which of these apply to
|
|
130
|
+
* which histAccessType.
|
|
131
|
+
*/
|
|
132
|
+
export interface KernelHistoryOptions {
|
|
133
|
+
/** Defaults to 'tail' -- the n most recent executions. */
|
|
134
|
+
histAccessType?: 'tail' | 'range' | 'search';
|
|
135
|
+
/** Include each entry's output alongside its input. Defaults to false -- the kernel doesn't actually record output today either way, so this currently only ever comes back empty. */
|
|
136
|
+
output?: boolean;
|
|
137
|
+
raw?: boolean;
|
|
138
|
+
/** Max entries to return ('tail'/'search'). Defaults to 100. */
|
|
139
|
+
n?: number;
|
|
140
|
+
/** 'range' only. */
|
|
141
|
+
session?: number;
|
|
142
|
+
start?: number;
|
|
143
|
+
stop?: number;
|
|
144
|
+
/** 'search' only -- a glob pattern (*, ?). */
|
|
145
|
+
pattern?: string;
|
|
146
|
+
unique?: boolean;
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* One entry from a real history_reply: [session, line_number, input], or
|
|
150
|
+
* [session, line_number, [input, output]] when `output: true` was requested
|
|
151
|
+
* (see KernelHistoryOptions.output's own caveat -- output is currently
|
|
152
|
+
* always "").
|
|
153
|
+
*/
|
|
154
|
+
export type KernelHistoryEntry = [number, number, string | [string, string]];
|
|
155
|
+
/** The supervisor's latest view of a kernel's heartbeat -- see Session.status(). */
|
|
156
|
+
export interface HeartbeatInfo {
|
|
157
|
+
/** False until the first ping has been answered. */
|
|
158
|
+
hasPong: boolean;
|
|
159
|
+
/** Round trip of the most recent answered ping, in ms. */
|
|
160
|
+
rttMs: number;
|
|
161
|
+
/** How long ago that answer arrived, in ms. */
|
|
162
|
+
sinceLastPongMs: number;
|
|
163
|
+
/** Pings in a row that went unanswered (0 = healthy). */
|
|
164
|
+
misses: number;
|
|
165
|
+
}
|
|
166
|
+
/** What the supervisor knows about one session's kernel process. */
|
|
167
|
+
export interface SessionStatusInfo {
|
|
168
|
+
sessionId: string;
|
|
169
|
+
status: 'starting' | 'ready' | 'stopped' | 'crashed';
|
|
170
|
+
kernelType: 'r' | 'python';
|
|
171
|
+
workingDirectory: string;
|
|
172
|
+
/** OS process id of the kernel; 0 when it is not running. */
|
|
173
|
+
pid: number;
|
|
174
|
+
/** Resident memory in bytes, or null where it can't be read. */
|
|
175
|
+
memoryBytes: number | null;
|
|
176
|
+
heartbeat: HeartbeatInfo | null;
|
|
177
|
+
}
|
|
178
|
+
export interface ShinyAppOptions {
|
|
179
|
+
/** Directory containing the Shiny app (server.R/ui.R or app.R). */
|
|
180
|
+
appDir: string;
|
|
181
|
+
/** Defaults to an OS-assigned free port. */
|
|
182
|
+
port?: number;
|
|
183
|
+
/** Defaults to '127.0.0.1'. */
|
|
184
|
+
host?: string;
|
|
185
|
+
/** Defaults to false -- the caller decides how/where to display the app. */
|
|
186
|
+
launchBrowser?: boolean;
|
|
187
|
+
/** Max time to wait for the app to start accepting connections, in ms. Defaults to 10000. */
|
|
188
|
+
readyTimeout?: number;
|
|
189
|
+
/**
|
|
190
|
+
* Environment variables to set (via Sys.setenv()) in the R session
|
|
191
|
+
* before launching the app -- e.g. rmncah's app.R reads
|
|
192
|
+
* CDSUITE_SHINY_NAME/CDSUITE_SHINY_VERSION/CDSUITE_SHINY_SELECTED_FILE/
|
|
193
|
+
* CDSUITE_SHINY_LOCALE via Sys.getenv(). Applied only for the duration
|
|
194
|
+
* of this R session (not the OS process), and only take effect for code
|
|
195
|
+
* that reads them after runApp() starts, since Sys.setenv() itself runs
|
|
196
|
+
* synchronously right before it in the same execute() call.
|
|
197
|
+
*/
|
|
198
|
+
env?: Record<string, string>;
|
|
199
|
+
}
|
|
200
|
+
export interface ShinyAppHandle {
|
|
201
|
+
host: string;
|
|
202
|
+
port: number;
|
|
203
|
+
url: string;
|
|
204
|
+
/**
|
|
205
|
+
* Resolves with the R-side execute_reply once the app stops (e.g. via
|
|
206
|
+
* interrupt/restart) or crashes -- shiny::runApp() blocks the R session
|
|
207
|
+
* for as long as the app is running, so this does NOT resolve just
|
|
208
|
+
* because the app started successfully. Await `createShiny()` itself
|
|
209
|
+
* for that.
|
|
210
|
+
*/
|
|
211
|
+
done: Promise<ExecutionResult>;
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Reply contents for the Jupyter request/reply pairs Session exposes as
|
|
215
|
+
* methods -- see Session.complete()/inspect()/isComplete()/kernelInfo()/
|
|
216
|
+
* commInfo()/interrupt(). All of them carry the standard `status`.
|
|
217
|
+
*/
|
|
218
|
+
export interface CompleteReplyContent {
|
|
219
|
+
status: 'ok' | 'error';
|
|
220
|
+
matches: string[];
|
|
221
|
+
cursor_start: number;
|
|
222
|
+
cursor_end: number;
|
|
223
|
+
metadata: Record<string, any>;
|
|
224
|
+
}
|
|
225
|
+
export interface InspectReplyContent {
|
|
226
|
+
status: 'ok' | 'error';
|
|
227
|
+
found: boolean;
|
|
228
|
+
data: Record<string, any>;
|
|
229
|
+
metadata: Record<string, any>;
|
|
230
|
+
}
|
|
231
|
+
export interface IsCompleteReplyContent {
|
|
232
|
+
status: 'complete' | 'incomplete' | 'invalid' | 'unknown';
|
|
233
|
+
indent?: string;
|
|
234
|
+
}
|
|
235
|
+
export interface KernelInfoReplyContent {
|
|
236
|
+
status: 'ok' | 'error';
|
|
237
|
+
protocol_version: string;
|
|
238
|
+
implementation: string;
|
|
239
|
+
implementation_version: string;
|
|
240
|
+
language_info: {
|
|
241
|
+
name: string;
|
|
242
|
+
version: string;
|
|
243
|
+
mimetype: string;
|
|
244
|
+
file_extension: string;
|
|
245
|
+
pygments_lexer?: string;
|
|
246
|
+
codemirror_mode?: string | Record<string, any>;
|
|
247
|
+
nbconvert_exporter?: string;
|
|
248
|
+
};
|
|
249
|
+
banner: string;
|
|
250
|
+
help_links?: Array<{
|
|
251
|
+
text: string;
|
|
252
|
+
url: string;
|
|
253
|
+
}>;
|
|
254
|
+
}
|
|
255
|
+
export interface CommInfoReplyContent {
|
|
256
|
+
status: 'ok' | 'error';
|
|
257
|
+
/** comm_id -> {target_name} for every comm currently open in the kernel. */
|
|
258
|
+
comms: Record<string, {
|
|
259
|
+
target_name: string;
|
|
260
|
+
}>;
|
|
261
|
+
}
|
|
262
|
+
export interface InterruptReplyContent {
|
|
263
|
+
status: 'ok' | 'error';
|
|
264
|
+
}
|
|
265
|
+
export interface ShutdownReplyContent {
|
|
266
|
+
status: 'ok' | 'error';
|
|
267
|
+
restart: boolean;
|
|
268
|
+
}
|
|
269
|
+
//# sourceMappingURL=engine.d.ts.map
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
export type MessageTopic = 'stream' | 'execute_result' | 'display_data' | 'error' | 'execute_reply' | 'comm_open' | 'comm_msg' | 'comm_close' | 'status' | string;
|
|
2
|
+
export type MessageChannel = 'iopub' | 'shell' | 'stdin' | 'control';
|
|
3
|
+
export interface JupyterMessage<T = any> {
|
|
4
|
+
topic: MessageTopic;
|
|
5
|
+
msgType: string;
|
|
6
|
+
channel: MessageChannel;
|
|
7
|
+
parentMsgId: string;
|
|
8
|
+
content: T;
|
|
9
|
+
timestamp: number;
|
|
10
|
+
raw: string;
|
|
11
|
+
}
|
|
12
|
+
export interface StreamContent {
|
|
13
|
+
name: 'stdout' | 'stderr';
|
|
14
|
+
text: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ExecuteResultContent {
|
|
17
|
+
execution_count: number;
|
|
18
|
+
data: {
|
|
19
|
+
'text/plain'?: string | string[];
|
|
20
|
+
'text/html'?: string;
|
|
21
|
+
'image/png'?: string;
|
|
22
|
+
[key: string]: any;
|
|
23
|
+
};
|
|
24
|
+
metadata: Record<string, any>;
|
|
25
|
+
}
|
|
26
|
+
export interface ErrorContent {
|
|
27
|
+
ename: string;
|
|
28
|
+
evalue: string;
|
|
29
|
+
traceback: string[];
|
|
30
|
+
}
|
|
31
|
+
export interface DisplayDataContent {
|
|
32
|
+
data: Record<string, any>;
|
|
33
|
+
metadata: Record<string, any>;
|
|
34
|
+
}
|
|
35
|
+
export interface InputRequestContent {
|
|
36
|
+
prompt: string;
|
|
37
|
+
password: boolean;
|
|
38
|
+
}
|
|
39
|
+
export type ExecutionState = 'busy' | 'idle' | 'starting';
|
|
40
|
+
export interface StatusContent {
|
|
41
|
+
execution_state: ExecutionState;
|
|
42
|
+
}
|
|
43
|
+
export interface ExecuteInputContent {
|
|
44
|
+
code: string;
|
|
45
|
+
execution_count: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ClearOutputContent {
|
|
48
|
+
wait: boolean;
|
|
49
|
+
}
|
|
50
|
+
export interface UpdateDisplayDataContent extends DisplayDataContent {
|
|
51
|
+
transient?: {
|
|
52
|
+
display_id?: string;
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
export interface CommOpenContent {
|
|
56
|
+
comm_id: string;
|
|
57
|
+
target_name: string;
|
|
58
|
+
data: Record<string, any>;
|
|
59
|
+
}
|
|
60
|
+
export interface CommMsgContent {
|
|
61
|
+
comm_id: string;
|
|
62
|
+
data: Record<string, any>;
|
|
63
|
+
}
|
|
64
|
+
export interface CommCloseContent {
|
|
65
|
+
comm_id: string;
|
|
66
|
+
data: Record<string, any>;
|
|
67
|
+
}
|
|
68
|
+
//# sourceMappingURL=messages.d.ts.map
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { LoggerFunction } from '../types/index.js';
|
|
2
|
+
export declare function timestamp(): string;
|
|
3
|
+
export declare class Logger {
|
|
4
|
+
private customLogger?;
|
|
5
|
+
constructor(customLogger?: LoggerFunction);
|
|
6
|
+
trace(message: string, data?: any): void;
|
|
7
|
+
debug(message: string, data?: any): void;
|
|
8
|
+
info(message: string, data?: any): void;
|
|
9
|
+
warn(message: string, data?: any): void;
|
|
10
|
+
error(message: string, error?: any): void;
|
|
11
|
+
}
|
|
12
|
+
//# sourceMappingURL=logger.d.ts.map
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
// Matches the timestamp format used throughout VS Code's own logs
|
|
2
|
+
// (exthost.log, output channels, etc.): "YYYY-MM-DD HH:mm:ss.mmm". Kept
|
|
3
|
+
// local rather than pulled from a date library so this has no runtime
|
|
4
|
+
// dependency beyond what's already here. Exported for the handful of call
|
|
5
|
+
// sites (e.g. session-worker.ts) that log before a Logger instance exists.
|
|
6
|
+
export function timestamp() {
|
|
7
|
+
const now = new Date();
|
|
8
|
+
const pad = (n, width = 2) => String(n).padStart(width, '0');
|
|
9
|
+
return `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ` +
|
|
10
|
+
`${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}.${pad(now.getMilliseconds(), 3)}`;
|
|
11
|
+
}
|
|
12
|
+
export class Logger {
|
|
13
|
+
customLogger;
|
|
14
|
+
constructor(customLogger) {
|
|
15
|
+
this.customLogger = customLogger;
|
|
16
|
+
}
|
|
17
|
+
trace(message, data) {
|
|
18
|
+
if (this.customLogger) {
|
|
19
|
+
this.customLogger('trace', message, data);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
console.log(`${timestamp()} [trace] ${message}`, data ? data : '');
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
debug(message, data) {
|
|
26
|
+
if (this.customLogger) {
|
|
27
|
+
this.customLogger('debug', message, data);
|
|
28
|
+
}
|
|
29
|
+
else {
|
|
30
|
+
console.log(`${timestamp()} [debug] ${message}`, data ? data : '');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
info(message, data) {
|
|
34
|
+
if (this.customLogger) {
|
|
35
|
+
this.customLogger('info', message, data);
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
38
|
+
console.log(`${timestamp()} [info] ${message}`, data ? data : '');
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
warn(message, data) {
|
|
42
|
+
if (this.customLogger) {
|
|
43
|
+
this.customLogger('warn', message, data);
|
|
44
|
+
}
|
|
45
|
+
else {
|
|
46
|
+
console.warn(`${timestamp()} [warn] ${message}`, data ? data : '');
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
error(message, error) {
|
|
50
|
+
if (this.customLogger) {
|
|
51
|
+
this.customLogger('error', message, error);
|
|
52
|
+
}
|
|
53
|
+
else {
|
|
54
|
+
console.error(`${timestamp()} [error] ${message}`, error ? error : '');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
//# sourceMappingURL=logger.js.map
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Asks the OS for an available TCP port by binding to port 0 and reading
|
|
3
|
+
* back what got assigned.
|
|
4
|
+
*/
|
|
5
|
+
export declare function findFreePort(host?: string): Promise<number>;
|
|
6
|
+
/**
|
|
7
|
+
* Polls `host:port` until something accepts a TCP connection, or rejects
|
|
8
|
+
* once `timeoutMs` elapses without one.
|
|
9
|
+
*/
|
|
10
|
+
export declare function waitForPort(host: string, port: number, timeoutMs: number): Promise<void>;
|
|
11
|
+
//# sourceMappingURL=network.d.ts.map
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import * as net from 'net';
|
|
2
|
+
/**
|
|
3
|
+
* Asks the OS for an available TCP port by binding to port 0 and reading
|
|
4
|
+
* back what got assigned.
|
|
5
|
+
*/
|
|
6
|
+
export function findFreePort(host = '127.0.0.1') {
|
|
7
|
+
return new Promise((resolve, reject) => {
|
|
8
|
+
const server = net.createServer();
|
|
9
|
+
server.unref();
|
|
10
|
+
server.once('error', reject);
|
|
11
|
+
server.listen(0, host, () => {
|
|
12
|
+
const address = server.address();
|
|
13
|
+
const port = typeof address === 'object' && address ? address.port : undefined;
|
|
14
|
+
server.close(() => {
|
|
15
|
+
if (port) {
|
|
16
|
+
resolve(port);
|
|
17
|
+
}
|
|
18
|
+
else {
|
|
19
|
+
reject(new Error('Failed to allocate a free port'));
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
});
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Polls `host:port` until something accepts a TCP connection, or rejects
|
|
27
|
+
* once `timeoutMs` elapses without one.
|
|
28
|
+
*/
|
|
29
|
+
export function waitForPort(host, port, timeoutMs) {
|
|
30
|
+
const deadline = Date.now() + timeoutMs;
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
const attempt = () => {
|
|
33
|
+
const socket = net.connect({ host, port }, () => {
|
|
34
|
+
socket.destroy();
|
|
35
|
+
resolve();
|
|
36
|
+
});
|
|
37
|
+
socket.once('error', () => {
|
|
38
|
+
socket.destroy();
|
|
39
|
+
if (Date.now() >= deadline) {
|
|
40
|
+
reject(new Error(`Timed out waiting for ${host}:${port} to accept connections`));
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
setTimeout(attempt, 150);
|
|
44
|
+
}
|
|
45
|
+
});
|
|
46
|
+
};
|
|
47
|
+
attempt();
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
//# sourceMappingURL=network.js.map
|